diff --git a/libs/@local/hashql/compiletest/src/pipeline.rs b/libs/@local/hashql/compiletest/src/pipeline.rs index 8b18e299072..e347acb4e28 100644 --- a/libs/@local/hashql/compiletest/src/pipeline.rs +++ b/libs/@local/hashql/compiletest/src/pipeline.rs @@ -31,12 +31,7 @@ use hashql_mir::{ body::Body, context::MirContext, def::{DefId, DefIdSlice, DefIdVec}, - pass::{ - Changed, GlobalAnalysisPass as _, GlobalTransformPass as _, GlobalTransformState, - analysis::SizeEstimationAnalysis, - execution::{ExecutionAnalysis, ExecutionAnalysisResidual}, - transform::{Inline, InlineConfig, PostInline, PreInline}, - }, + pass::{self, LowerConfig, execution::ExecutionAnalysisResidual}, reify::ReifyContext, }; use hashql_syntax_jexpr::span::Span; @@ -189,9 +184,11 @@ impl<'heap> Pipeline<'heap> { bodies: &mut bodies, mir: &mut mir_context, hir: &hir_context, + scratch: &self.scratch, }; let entry = tri!(hashql_mir::reify::from_hir(node, &mut reify_context)); + self.scratch.reset(); // drain the context, because we're going to re-create it self.diagnostics.extend( @@ -218,24 +215,14 @@ impl<'heap> Pipeline<'heap> { bodies: &mut DefIdSlice>, ) -> Result<(), BoxedDiagnostic<'static, SpanId>> { let mut context = MirContext::new(&self.env, interner); - let mut state = GlobalTransformState::new_in(&*bodies, self.heap); - - self.scratch.reset(); - - let mut pass = PreInline::new_in(&mut self.scratch); - let _: Changed = pass.run(&mut context, &mut state, bodies); - self.scratch.reset(); - - let mut pass = Inline::new_in(InlineConfig::default(), &mut self.scratch); - let _: Changed = pass.run(&mut context, &mut state, bodies); - self.scratch.reset(); - let mut pass = PostInline::new_in(&mut self.scratch); - let _: Changed = pass.run(&mut context, &mut state, bodies); - self.scratch.reset(); - - let status = context.diagnostics.generalize().boxed().into_status(()); - process_status(&mut self.diagnostics, status)?; + let result = pass::lower( + &mut context, + &mut self.scratch, + bodies, + &LowerConfig::default(), + ); + process_status(&mut self.diagnostics, result)?; Ok(()) } @@ -262,20 +249,8 @@ impl<'heap> Pipeline<'heap> { > { let mut context = MirContext::new(&self.env, interner); - let mut pass = SizeEstimationAnalysis::new_in(&self.scratch); - pass.run(&mut context, bodies); - let footprints = pass.finish(); - self.scratch.reset(); - - let pass = ExecutionAnalysis { - footprints: &footprints, - scratch: &mut self.scratch, - }; - let analysis = pass.run_all_in(&mut context, bodies, self.heap); - self.scratch.reset(); - - let status = context.diagnostics.generalize().boxed().into_status(()); - process_status(&mut self.diagnostics, status)?; + let status = pass::place(&mut context, &mut self.scratch, bodies); + let analysis = process_status(&mut self.diagnostics, status)?; Ok(analysis) } diff --git a/libs/@local/hashql/compiletest/src/suite/eval_postgres.rs b/libs/@local/hashql/compiletest/src/suite/eval_postgres.rs index 721796be1e9..7cd0bd37246 100644 --- a/libs/@local/hashql/compiletest/src/suite/eval_postgres.rs +++ b/libs/@local/hashql/compiletest/src/suite/eval_postgres.rs @@ -122,7 +122,7 @@ impl Suite for EvalPostgres { &interner, &bodies, &analysis, - context.heap, + heap, &mut scratch, ); scratch.reset(); diff --git a/libs/@local/hashql/compiletest/src/suite/mir_interpret.rs b/libs/@local/hashql/compiletest/src/suite/mir_interpret.rs new file mode 100644 index 00000000000..6d7bb4aed7b --- /dev/null +++ b/libs/@local/hashql/compiletest/src/suite/mir_interpret.rs @@ -0,0 +1,66 @@ +use hashql_core::r#type::environment::Environment; +use hashql_diagnostics::Diagnostic; +use hashql_mir::{ + intern::Interner, + interpret::{CallStack, Inputs, Runtime, RuntimeConfig}, +}; + +use super::{ + RunContext, Suite, SuiteDiagnostic, + mir_pass_transform_post_inline::mir_pass_transform_post_inline, + mir_pass_transform_pre_inline::TextRenderer, +}; + +pub(crate) struct MirInterpret; + +impl Suite for MirInterpret { + fn name(&self) -> &'static str { + "mir/interpret" + } + + fn description(&self) -> &'static str { + "Run the interpreter on the MIR" + } + + fn secondary_file_extensions(&self) -> &[&str] { + &["mir"] + } + + fn run<'heap>( + &self, + RunContext { + heap, + diagnostics, + secondary_outputs, + .. + }: RunContext<'_, 'heap>, + expr: hashql_ast::node::expr::Expr<'heap>, + ) -> Result { + let mut environment = Environment::new(heap); + let interner = Interner::new(heap); + + let mut buffer = Vec::new(); + + let (root, bodies, _) = mir_pass_transform_post_inline( + heap, + expr, + &interner, + TextRenderer::new(&mut buffer), + &mut environment, + diagnostics, + )?; + + secondary_outputs.insert("mir", String::from_utf8_lossy_owned(buffer)); + + let inputs = Inputs::new(); + let mut runtime = Runtime::new(RuntimeConfig::default(), &bodies, &inputs); + let callstack = CallStack::new(&runtime, root, []); + + let output = runtime + .run(callstack, |_| unimplemented!()) + .map_err(Diagnostic::generalize) + .map_err(Diagnostic::boxed)?; + + Ok(format!("{output:#?}")) + } +} diff --git a/libs/@local/hashql/compiletest/src/suite/mir_pass_analysis_data_dependency.rs b/libs/@local/hashql/compiletest/src/suite/mir_pass_analysis_data_dependency.rs index d249c5cbc15..8d10d49f139 100644 --- a/libs/@local/hashql/compiletest/src/suite/mir_pass_analysis_data_dependency.rs +++ b/libs/@local/hashql/compiletest/src/suite/mir_pass_analysis_data_dependency.rs @@ -38,7 +38,8 @@ impl Suite for MirPassAnalysisDataDependency { let mut buffer = Vec::new(); - let (root, mut bodies) = mir_reify(heap, expr, &interner, &mut environment, diagnostics)?; + let (root, mut bodies, _) = + mir_reify(heap, expr, &interner, &mut environment, diagnostics)?; writeln!(buffer, "{}\n", Header::new("MIR")).expect("should be able to write to buffer"); mir_format_text(heap, &environment, &mut buffer, root, &bodies); diff --git a/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_cfg_simplify.rs b/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_cfg_simplify.rs index fa54cc8f4f7..a4e19a5d7cf 100644 --- a/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_cfg_simplify.rs +++ b/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_cfg_simplify.rs @@ -49,7 +49,8 @@ pub(crate) fn mir_pass_transform_cfg_simplify<'heap>( environment: &mut Environment<'heap>, diagnostics: &mut Vec, ) -> Result<(DefId, DefIdVec>, Scratch), SuiteDiagnostic> { - let (root, mut bodies) = mir_reify(heap, expr, interner, environment, diagnostics)?; + let (root, mut bodies, mut scratch) = + mir_reify(heap, expr, interner, environment, diagnostics)?; render(heap, environment, root, &bodies); @@ -59,7 +60,6 @@ pub(crate) fn mir_pass_transform_cfg_simplify<'heap>( interner, diagnostics: DiagnosticIssues::new(), }; - let mut scratch = Scratch::new(); let mut pass = CfgSimplify::new_in(&mut scratch); for body in bodies.as_mut_slice() { diff --git a/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_pre_inline.rs b/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_pre_inline.rs index 25c2c906b5e..1386621c949 100644 --- a/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_pre_inline.rs +++ b/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_pre_inline.rs @@ -168,7 +168,7 @@ pub(crate) fn mir_pass_transform_pre_inline<'heap>( environment: &mut Environment<'heap>, diagnostics: &mut Vec, ) -> Result<(DefId, DefIdVec>, Scratch), SuiteDiagnostic> { - let (root, mut bodies) = mir_reify(heap, expr, interner, environment, diagnostics)?; + let (root, mut bodies, _) = mir_reify(heap, expr, interner, environment, diagnostics)?; render.render( &mut RenderContext { diff --git a/libs/@local/hashql/compiletest/src/suite/mir_reify.rs b/libs/@local/hashql/compiletest/src/suite/mir_reify.rs index ce16f3b4aa6..6e813cae28a 100644 --- a/libs/@local/hashql/compiletest/src/suite/mir_reify.rs +++ b/libs/@local/hashql/compiletest/src/suite/mir_reify.rs @@ -8,7 +8,7 @@ use std::{ use error_stack::ReportSink; use hashql_ast::node::expr::Expr; use hashql_core::{ - heap::Heap, + heap::{Heap, ResetAllocator as _, Scratch}, id::IdVec, module::ModuleRegistry, pretty::Formatter, @@ -32,7 +32,8 @@ pub(crate) fn mir_reify<'heap>( interner: &Interner<'heap>, environment: &mut Environment<'heap>, diagnostics: &mut Vec, -) -> Result<(DefId, DefIdVec>), SuiteDiagnostic> { +) -> Result<(DefId, DefIdVec>, Scratch), SuiteDiagnostic> { + let mut scratch = Scratch::new(); let registry = ModuleRegistry::new(environment); let hir_interner = hashql_hir::intern::Interner::new(heap); let mut hir_context = HirContext::new(&hir_interner, ®istry); @@ -66,11 +67,13 @@ pub(crate) fn mir_reify<'heap>( bodies: &mut bodies, mir: &mut mir_context, hir: &hir_context, + scratch: &scratch, }, ), )?; + scratch.reset(); - Ok((root, bodies)) + Ok((root, bodies, scratch)) } pub(crate) fn mir_format_text<'heap>( @@ -208,7 +211,7 @@ impl Suite for MirReifySuite { let mut environment = Environment::new(heap); let interner = Interner::new(heap); - let (root, bodies) = mir_reify(heap, expr, &interner, &mut environment, diagnostics)?; + let (root, bodies, _) = mir_reify(heap, expr, &interner, &mut environment, diagnostics)?; let mut buffer = Vec::new(); mir_format_text(heap, &environment, &mut buffer, root, &bodies); diff --git a/libs/@local/hashql/compiletest/src/suite/mod.rs b/libs/@local/hashql/compiletest/src/suite/mod.rs index 6326eeeeebb..ba00443e0f7 100644 --- a/libs/@local/hashql/compiletest/src/suite/mod.rs +++ b/libs/@local/hashql/compiletest/src/suite/mod.rs @@ -21,6 +21,7 @@ mod hir_lower_normalization; mod hir_lower_specialization; mod hir_lower_thunking; mod hir_reify; +mod mir_interpret; mod mir_pass_analysis_data_dependency; mod mir_pass_transform_administrative_reduction; mod mir_pass_transform_cfg_simplify; @@ -59,7 +60,7 @@ use self::{ hir_lower_normalization::HirLowerNormalizationSuite, hir_lower_specialization::HirLowerSpecializationSuite, hir_lower_thunking::HirLowerThunkingSuite, hir_reify::HirReifySuite, - mir_pass_analysis_data_dependency::MirPassAnalysisDataDependency, + mir_interpret::MirInterpret, mir_pass_analysis_data_dependency::MirPassAnalysisDataDependency, mir_pass_transform_administrative_reduction::MirPassTransformAdministrativeReduction, mir_pass_transform_cfg_simplify::MirPassTransformCfgSimplify, mir_pass_transform_dse::MirPassTransformDse, @@ -163,6 +164,7 @@ const SUITES: &[&dyn Suite] = &[ &HirLowerTypeInferenceIntrinsicsSuite, &HirLowerTypeInferenceSuite, &HirReifySuite, + &MirInterpret, &MirPassAnalysisDataDependency, &MirPassTransformAdministrativeReduction, &MirPassTransformCfgSimplify, diff --git a/libs/@local/hashql/core/src/graph/linked.rs b/libs/@local/hashql/core/src/graph/linked.rs index 14341a654ec..6494bd62ad1 100644 --- a/libs/@local/hashql/core/src/graph/linked.rs +++ b/libs/@local/hashql/core/src/graph/linked.rs @@ -50,6 +50,7 @@ use alloc::alloc::Global; use core::{ alloc::Allocator, + fmt, ops::{Index, IndexMut}, }; @@ -213,7 +214,7 @@ impl Edge { /// } /// # else { unreachable!() } /// ``` -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct LinkedGraph { /// All nodes in the graph, indexed by [`NodeId`]. nodes: IdVec, A>, @@ -541,6 +542,17 @@ impl Default for LinkedGraph { } } +impl core::fmt::Debug + for LinkedGraph +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("LinkedGraph") + .field("nodes", &self.nodes) + .field("edges", &self.edges) + .finish() + } +} + impl IndexMut for LinkedGraph { fn index_mut(&mut self, index: NodeId) -> &mut Self::Output { &mut self.nodes[index] diff --git a/libs/@local/hashql/eval/src/orchestrator/error.rs b/libs/@local/hashql/eval/src/orchestrator/error.rs index b19b096192e..14b5a1c87e8 100644 --- a/libs/@local/hashql/eval/src/orchestrator/error.rs +++ b/libs/@local/hashql/eval/src/orchestrator/error.rs @@ -1,12 +1,22 @@ -//! Errors that occur while fulfilling [`GraphRead`] suspensions. +//! Error types for the orchestration layer. //! -//! These are internal runtime errors: failures in compiled query execution, -//! row decoding, or parameter encoding. The user wrote HashQL, not SQL; if -//! the bridge fails, it indicates a bug in the compiler or runtime. +//! The orchestrator sits between the MIR interpreter and external data sources +//! (PostgreSQL). Errors fall into two families: +//! +//! - **Interpreter errors**: failures in the MIR interpreter itself (type invariant violations, +//! control flow errors, etc.). These are produced by the interpreter and forwarded through the +//! orchestrator. +//! - **Bridge errors**: failures while fulfilling [`GraphRead`] suspensions (query execution, row +//! decoding, parameter encoding). The user wrote HashQL, not SQL; if the bridge fails, it +//! indicates a bug in the compiler or runtime. +//! +//! [`OrchestratorDiagnosticCategory`] unifies both families under a single +//! category hierarchy so that downstream consumers (the eval crate) see one +//! coherent diagnostic type from the orchestration layer. //! //! [`GraphRead`]: hashql_mir::body::terminator::GraphRead -use alloc::string::String; +use alloc::{borrow::Cow, string::String}; use hashql_core::{ pretty::{Formatter, RenderOptions}, @@ -15,15 +25,15 @@ use hashql_core::{ r#type::{TypeFormatter, TypeFormatterOptions, TypeId, environment::Environment}, }; use hashql_diagnostics::{ - Diagnostic, Label, category::TerminalDiagnosticCategory, diagnostic::Message, - severity::Severity, + Diagnostic, Label, + category::{DiagnosticCategory, TerminalDiagnosticCategory}, + diagnostic::Message, + severity::Critical, }; use hashql_mir::{ body::{basic_block::BasicBlockId, local::Local}, def::DefId, - interpret::error::{ - InterpretDiagnostic, InterpretDiagnosticCategory, SuspensionDiagnosticCategory, - }, + interpret::error::InterpretDiagnosticCategory, }; use super::{Indexed, codec::JsonValueKind}; @@ -89,8 +99,64 @@ const VALUE_SERIALIZATION: TerminalDiagnosticCategory = TerminalDiagnosticCatego name: "Value Serialization", }; -const fn category(terminal: &'static TerminalDiagnosticCategory) -> InterpretDiagnosticCategory { - InterpretDiagnosticCategory::Suspension(SuspensionDiagnosticCategory(terminal)) +/// Type alias for orchestrator diagnostics. +/// +/// The default severity kind is [`Critical`]. +pub type OrchestratorDiagnostic = + Diagnostic; + +/// Diagnostic subcategory for errors that occur while fulfilling a suspension. +/// +/// Wraps a [`TerminalDiagnosticCategory`] that identifies the specific bridge +/// failure (query execution, row hydration, parameter encoding, etc.). +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct BridgeDiagnosticCategory(pub &'static TerminalDiagnosticCategory); + +impl DiagnosticCategory for BridgeDiagnosticCategory { + fn id(&self) -> Cow<'_, str> { + Cow::Borrowed("bridge") + } + + fn name(&self) -> Cow<'_, str> { + Cow::Borrowed("Bridge") + } + + fn subcategory(&self) -> Option<&dyn DiagnosticCategory> { + Some(self.0) + } +} + +/// Top-level diagnostic category for the orchestration layer. +/// +/// Unifies interpreter errors (forwarded from the MIR interpreter) and bridge +/// errors (failures while fulfilling suspensions) under a single hierarchy. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum OrchestratorDiagnosticCategory { + /// An error produced by the MIR interpreter itself. + Interpret(InterpretDiagnosticCategory), + /// An error from the bridge while fulfilling a suspension. + Bridge(BridgeDiagnosticCategory), +} + +impl DiagnosticCategory for OrchestratorDiagnosticCategory { + fn id(&self) -> Cow<'_, str> { + Cow::Borrowed("orchestrator") + } + + fn name(&self) -> Cow<'_, str> { + Cow::Borrowed("Orchestrator") + } + + fn subcategory(&self) -> Option<&dyn DiagnosticCategory> { + match self { + Self::Interpret(cat) => Some(cat), + Self::Bridge(cat) => Some(cat), + } + } +} + +const fn category(terminal: &'static TerminalDiagnosticCategory) -> OrchestratorDiagnosticCategory { + OrchestratorDiagnosticCategory::Bridge(BridgeDiagnosticCategory(terminal)) } /// Errors that occur while decoding a JSON value into a typed [`Value`]. @@ -356,7 +422,7 @@ pub enum BridgeError<'heap> { } impl<'heap> BridgeError<'heap> { - pub fn into_diagnostic(self, span: SpanId, env: &Environment<'heap>) -> InterpretDiagnostic { + pub fn into_diagnostic(self, span: SpanId, env: &Environment<'heap>) -> OrchestratorDiagnostic { match self { Self::QueryExecution { sql, source } => query_execution(span, &sql, &source), Self::RowHydration { column, source } => row_hydration(span, column, &source), @@ -388,8 +454,12 @@ impl<'heap> BridgeError<'heap> { } } -fn query_execution(span: SpanId, sql: &str, error: &tokio_postgres::Error) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(category(&QUERY_EXECUTION), Severity::Bug).primary( +fn query_execution( + span: SpanId, + sql: &str, + error: &tokio_postgres::Error, +) -> OrchestratorDiagnostic { + let mut diagnostic = Diagnostic::new(category(&QUERY_EXECUTION), Critical::BUG).primary( Label::new(span, "compiled query was rejected by the database"), ); @@ -411,9 +481,9 @@ fn row_hydration( value: column, }: Indexed, source: &tokio_postgres::Error, -) -> InterpretDiagnostic { +) -> OrchestratorDiagnostic { let mut diagnostic = - Diagnostic::new(category(&ROW_HYDRATION), Severity::Bug).primary(Label::new( + Diagnostic::new(category(&ROW_HYDRATION), Critical::BUG).primary(Label::new( span, format!("cannot decode result column {index} ({column})"), )); @@ -429,7 +499,7 @@ fn row_hydration( /// Adds notes describing a [`DecodeError`] to a diagnostic. fn add_decode_error_notes( - diagnostic: &mut InterpretDiagnostic, + diagnostic: &mut OrchestratorDiagnostic, source: &DecodeError<'_>, env: &Environment<'_>, ) { @@ -543,9 +613,9 @@ fn value_deserialization( }: Indexed, source: &DecodeError<'_>, env: &Environment<'_>, -) -> InterpretDiagnostic { +) -> OrchestratorDiagnostic { let mut diagnostic = - Diagnostic::new(category(&VALUE_DESERIALIZATION), Severity::Bug).primary(Label::new( + Diagnostic::new(category(&VALUE_DESERIALIZATION), Critical::BUG).primary(Label::new( span, format!("cannot deserialize result column {index} ({column})"), )); @@ -565,8 +635,8 @@ fn continuation_deserialization( local: Local, source: &DecodeError<'_>, env: &Environment<'_>, -) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(category(&CONTINUATION_DESERIALIZATION), Severity::Bug) +) -> OrchestratorDiagnostic { + let mut diagnostic = Diagnostic::new(category(&CONTINUATION_DESERIALIZATION), Critical::BUG) .primary(Label::new( span, format!("cannot deserialize continuation local {local} in definition {body}"), @@ -582,9 +652,13 @@ fn continuation_deserialization( diagnostic } -fn invalid_continuation_block_id(span: SpanId, body: DefId, block_id: i32) -> InterpretDiagnostic { +fn invalid_continuation_block_id( + span: SpanId, + body: DefId, + block_id: i32, +) -> OrchestratorDiagnostic { let mut diagnostic = - Diagnostic::new(category(&INVALID_CONTINUATION_BLOCK_ID), Severity::Bug).primary( + Diagnostic::new(category(&INVALID_CONTINUATION_BLOCK_ID), Critical::BUG).primary( Label::new(span, "continuation returned an invalid block ID"), ); @@ -599,8 +673,8 @@ fn invalid_continuation_block_id(span: SpanId, body: DefId, block_id: i32) -> In diagnostic } -fn invalid_continuation_local(span: SpanId, body: DefId, local: i32) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(category(&INVALID_CONTINUATION_LOCAL), Severity::Bug) +fn invalid_continuation_local(span: SpanId, body: DefId, local: i32) -> OrchestratorDiagnostic { + let mut diagnostic = Diagnostic::new(category(&INVALID_CONTINUATION_LOCAL), Critical::BUG) .primary(Label::new(span, "continuation returned an invalid local")); diagnostic.add_message(Message::note(format!( @@ -618,9 +692,9 @@ fn parameter_encoding( span: SpanId, parameter: usize, error: &(dyn core::error::Error + Send + Sync), -) -> InterpretDiagnostic { +) -> OrchestratorDiagnostic { let mut diagnostic = - Diagnostic::new(category(&PARAMETER_ENCODING), Severity::Bug).primary(Label::new( + Diagnostic::new(category(&PARAMETER_ENCODING), Critical::BUG).primary(Label::new( span, format!( "cannot encode parameter ${} for the database", @@ -637,8 +711,8 @@ fn parameter_encoding( diagnostic } -fn query_lookup(span: SpanId, body: DefId, block: BasicBlockId) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(category(&QUERY_LOOKUP), Severity::Bug).primary( +fn query_lookup(span: SpanId, body: DefId, block: BasicBlockId) -> OrchestratorDiagnostic { + let mut diagnostic = Diagnostic::new(category(&QUERY_LOOKUP), Critical::BUG).primary( Label::new(span, "no compiled query found for this data access"), ); @@ -653,8 +727,8 @@ fn query_lookup(span: SpanId, body: DefId, block: BasicBlockId) -> InterpretDiag diagnostic } -fn incomplete_continuation(span: SpanId, body: DefId, field: &str) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(category(&INCOMPLETE_CONTINUATION), Severity::Bug) +fn incomplete_continuation(span: SpanId, body: DefId, field: &str) -> OrchestratorDiagnostic { + let mut diagnostic = Diagnostic::new(category(&INCOMPLETE_CONTINUATION), Critical::BUG) .primary(Label::new( span, "continuation state is missing required columns", @@ -671,8 +745,8 @@ fn incomplete_continuation(span: SpanId, body: DefId, field: &str) -> InterpretD diagnostic } -fn missing_execution_residual(span: SpanId, body: DefId) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(category(&MISSING_EXECUTION_RESIDUAL), Severity::Bug) +fn missing_execution_residual(span: SpanId, body: DefId) -> OrchestratorDiagnostic { + let mut diagnostic = Diagnostic::new(category(&MISSING_EXECUTION_RESIDUAL), Critical::BUG) .primary(Label::new( span, "no execution residual found for this definition", @@ -689,8 +763,8 @@ fn missing_execution_residual(span: SpanId, body: DefId) -> InterpretDiagnostic diagnostic } -fn invalid_filter_return(span: SpanId, body: DefId) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(category(&INVALID_FILTER_RETURN), Severity::Bug) +fn invalid_filter_return(span: SpanId, body: DefId) -> OrchestratorDiagnostic { + let mut diagnostic = Diagnostic::new(category(&INVALID_FILTER_RETURN), Critical::BUG) .primary(Label::new(span, "filter body returned a non-boolean value")); diagnostic.add_message(Message::note(format!( @@ -704,8 +778,8 @@ fn invalid_filter_return(span: SpanId, body: DefId) -> InterpretDiagnostic { diagnostic } -fn value_serialization(span: SpanId, error: &serde_json::Error) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(category(&VALUE_SERIALIZATION), Severity::Bug) +fn value_serialization(span: SpanId, error: &serde_json::Error) -> OrchestratorDiagnostic { + let mut diagnostic = Diagnostic::new(category(&VALUE_SERIALIZATION), Critical::BUG) .primary(Label::new(span, "cannot serialize runtime value to JSON")); diagnostic.add_message(Message::note(format!("serialization failed: {error}"))); diff --git a/libs/@local/hashql/eval/src/orchestrator/mod.rs b/libs/@local/hashql/eval/src/orchestrator/mod.rs index 978193b83ba..56e7acb85e2 100644 --- a/libs/@local/hashql/eval/src/orchestrator/mod.rs +++ b/libs/@local/hashql/eval/src/orchestrator/mod.rs @@ -30,9 +30,9 @@ //! validates them, then flushes the decoded state into the interpreter's callstack. //! - `request`: per-suspension-type handlers (currently [`GraphRead`]). //! - `tail`: result accumulation strategies (currently collection into a list). -//! - `error`: error types for all failure modes in the bridge. All variants use `Severity::Bug` -//! because the user wrote HashQL, not SQL: if the bridge fails, the compiler or runtime produced -//! something invalid. +//! - `error`: diagnostic category hierarchy ([`OrchestratorDiagnosticCategory`]) and bridge error +//! types. Bridge errors use `Severity::Bug` because the user wrote HashQL, not SQL: if the bridge +//! fails, the compiler or runtime produced something invalid. //! //! [`GraphRead`]: hashql_mir::body::terminator::GraphRead //! [`Suspension`]: hashql_mir::interpret::suspension::Suspension @@ -48,19 +48,21 @@ use hashql_mir::{ def::DefId, interpret::{ CallStack, Inputs, Runtime, RuntimeConfig, RuntimeError, - error::InterpretDiagnostic, suspension::{Continuation, Suspension}, value::Value, }, }; use tokio_postgres::Client; -pub use self::events::{AppendEventLog, Event, EventLog}; use self::{error::BridgeError, request::GraphReadOrchestrator}; +pub use self::{ + error::{OrchestratorDiagnostic, OrchestratorDiagnosticCategory}, + events::{AppendEventLog, Event, EventLog}, +}; use crate::{context::EvalContext, postgres::PreparedQueries}; pub mod codec; -pub(crate) mod error; +pub mod error; mod events; mod partial; mod postgres; @@ -164,7 +166,7 @@ impl<'ctx, 'heap, C, E: EventLog, A: Allocator> Orchestrator<'_, 'ctx, 'heap, C, /// /// # Errors /// - /// Returns an [`InterpretDiagnostic`] if the interpreter fails or any + /// Returns an [`OrchestratorDiagnostic`] if the interpreter fails or any /// suspension cannot be fulfilled (database errors, decoding failures, /// filter evaluation failures). /// @@ -177,7 +179,7 @@ impl<'ctx, 'heap, C, E: EventLog, A: Allocator> Orchestrator<'_, 'ctx, 'heap, C, args: impl IntoIterator, IntoIter: ExactSizeIterator>, alloc: L, - ) -> Result, InterpretDiagnostic> + ) -> Result, OrchestratorDiagnostic> where C: AsRef, { @@ -209,16 +211,18 @@ impl<'ctx, 'heap, C, E: EventLog, A: Allocator> Orchestrator<'_, 'ctx, 'heap, C, } }; - Err( - error.into_diagnostic(callstack.unwind().map(|(_, span)| span), |suspension| { + Err(error.into_diagnostic_with( + callstack.unwind().map(|(_, span)| span), + |suspension| { let span = callstack .unwind() .next() .map_or(self.context.bodies[body].span, |(_, span)| span); suspension.into_diagnostic(span, self.context.env) - }), - ) + }, + OrchestratorDiagnosticCategory::Interpret, + )) } /// Convenience wrapper around [`run_in`](Self::run_in) that uses the @@ -226,14 +230,14 @@ impl<'ctx, 'heap, C, E: EventLog, A: Allocator> Orchestrator<'_, 'ctx, 'heap, C, /// /// # Errors /// - /// Returns an [`InterpretDiagnostic`] on failure. See + /// Returns an [`OrchestratorDiagnostic`] on failure. See /// [`run_in`](Self::run_in). pub async fn run( &self, inputs: &Inputs<'heap, Global>, body: DefId, args: impl IntoIterator, IntoIter: ExactSizeIterator>, - ) -> Result, InterpretDiagnostic> + ) -> Result, OrchestratorDiagnostic> where C: AsRef, { diff --git a/libs/@local/hashql/eval/src/orchestrator/partial.rs b/libs/@local/hashql/eval/src/orchestrator/partial.rs index ef81274e199..385c86aa51b 100644 --- a/libs/@local/hashql/eval/src/orchestrator/partial.rs +++ b/libs/@local/hashql/eval/src/orchestrator/partial.rs @@ -243,7 +243,7 @@ impl<'heap, A: Allocator> PartialEncodings<'heap, A> { let mut builder: StructBuilder<'heap, A, 1> = StructBuilder::new(); self.vectors.finish_in(&mut builder, sym::vectors); - let value = Value::Struct(builder.finish(interner, alloc.clone())); + let value = Value::Struct(builder.finish(&interner.symbols, alloc.clone())); Value::Opaque(Opaque::new( sym::path::EntityEncodings, Rc::new_in(value, alloc), @@ -286,7 +286,7 @@ impl<'heap, A: Allocator> PartialLinkEntityId<'heap, A> { )), ); - let inner = Value::Struct(builder.finish(interner, alloc.clone())); + let inner = Value::Struct(builder.finish(&interner.symbols, alloc.clone())); Value::Opaque(Opaque::new(sym::path::EntityId, Rc::new_in(inner, alloc))) } } @@ -315,7 +315,7 @@ impl<'heap, A: Allocator> PartialProvenance<'heap, A> { self.inferred.finish_in(&mut builder, sym::inferred); self.edition.finish_in(&mut builder, sym::edition); - let value = Value::Struct(builder.finish(interner, alloc.clone())); + let value = Value::Struct(builder.finish(&interner.symbols, alloc.clone())); Value::Opaque(Opaque::new( sym::path::EntityProvenance, @@ -350,7 +350,7 @@ impl<'heap, A: Allocator> PartialTemporalVersioning<'heap, A> { self.transaction_time .finish_in(&mut builder, sym::transaction_time); - let value = Value::Struct(builder.finish(interner, alloc.clone())); + let value = Value::Struct(builder.finish(&interner.symbols, alloc.clone())); Value::Opaque(Opaque::new( sym::path::TemporalMetadata, Rc::new_in(value, alloc), @@ -390,7 +390,7 @@ impl<'heap, A: Allocator> PartialEntityId<'heap, A> { self.draft_id .finish_in(&mut builder, sym::draft_id, alloc.clone()); - let value = Value::Struct(builder.finish(interner, alloc.clone())); + let value = Value::Struct(builder.finish(&interner.symbols, alloc.clone())); Value::Opaque(Opaque::new(sym::path::EntityId, Rc::new_in(value, alloc))) } } @@ -425,7 +425,7 @@ impl<'heap, A: Allocator> PartialRecordId<'heap, A> { .finish_in(&mut builder, sym::entity_id); self.edition_id.finish_in(&mut builder, sym::edition_id); - let value = Value::Struct(builder.finish(interner, alloc.clone())); + let value = Value::Struct(builder.finish(&interner.symbols, alloc.clone())); Value::Opaque(Opaque::new(sym::path::RecordId, Rc::new_in(value, alloc))) } } @@ -483,7 +483,7 @@ impl<'heap, A: Allocator> PartialLinkData<'heap, A> { self.right_entity_provenance .finish_in(&mut builder, sym::right_entity_provenance); - let value = Value::Struct(builder.finish(interner, alloc.clone())); + let value = Value::Struct(builder.finish(&interner.symbols, alloc.clone())); Value::Opaque(Opaque::new(sym::path::LinkData, Rc::new_in(value, alloc))) } @@ -561,7 +561,7 @@ impl<'heap, A: Allocator> PartialMetadata<'heap, A> { self.property_metadata .finish_in(&mut builder, sym::property_metadata); - let value = Value::Struct(builder.finish(interner, alloc.clone())); + let value = Value::Struct(builder.finish(&interner.symbols, alloc.clone())); Value::Opaque(Opaque::new( sym::path::EntityMetadata, Rc::new_in(value, alloc), @@ -618,7 +618,7 @@ impl<'heap, A: Allocator> PartialEntity<'heap, A> { .map(|partial| partial.finish_in(interner, alloc.clone())) .finish_in(&mut builder, sym::encodings); - let value = Value::Struct(builder.finish(interner, alloc.clone())); + let value = Value::Struct(builder.finish(&interner.symbols, alloc.clone())); Value::Opaque(Opaque::new(sym::path::Entity, Rc::new_in(value, alloc))) } diff --git a/libs/@local/hashql/eval/tests/orchestrator/execution.rs b/libs/@local/hashql/eval/tests/orchestrator/execution.rs index c26eff6549a..09f3dcd74cf 100644 --- a/libs/@local/hashql/eval/tests/orchestrator/execution.rs +++ b/libs/@local/hashql/eval/tests/orchestrator/execution.rs @@ -146,6 +146,7 @@ fn run_impl<'heap>( let value = runtime .block_on(orchestrator.run(inputs, entry, [])) + .map_err(Diagnostic::generalize) .map_err(Diagnostic::boxed)?; Ok((value, event_log.take())) diff --git a/libs/@local/hashql/mir/src/body/operand.rs b/libs/@local/hashql/mir/src/body/operand.rs index 4190f286b7d..4dc7f6dc794 100644 --- a/libs/@local/hashql/mir/src/body/operand.rs +++ b/libs/@local/hashql/mir/src/body/operand.rs @@ -60,12 +60,14 @@ impl<'heap> Operand<'heap> { } impl From for Operand<'_> { + #[inline] fn from(value: !) -> Self { value } } impl From for Operand<'_> { + #[inline] fn from(local: Local) -> Self { Operand::Place(Place::local(local)) } diff --git a/libs/@local/hashql/mir/src/error.rs b/libs/@local/hashql/mir/src/error.rs index a11414cb575..847414be7f5 100644 --- a/libs/@local/hashql/mir/src/error.rs +++ b/libs/@local/hashql/mir/src/error.rs @@ -3,8 +3,11 @@ use alloc::borrow::Cow; use hashql_core::span::SpanId; use hashql_diagnostics::{Diagnostic, DiagnosticCategory, DiagnosticIssues, Severity}; -use crate::pass::{ - execution::PlacementDiagnosticCategory, transform::error::TransformationDiagnosticCategory, +use crate::{ + pass::{ + execution::PlacementDiagnosticCategory, transform::error::TransformationDiagnosticCategory, + }, + reify::ReifyDiagnosticCategory, }; pub type MirDiagnostic = Diagnostic; @@ -12,6 +15,7 @@ pub type MirDiagnosticIssues = DiagnosticIssues Option<&dyn DiagnosticCategory> { match self { + Self::Reify(category) => Some(category), Self::Placement(category) => Some(category), Self::Transformation(category) => Some(category), } diff --git a/libs/@local/hashql/mir/src/interpret/error.rs b/libs/@local/hashql/mir/src/interpret/error.rs index 10fe9e2c79b..f4c4b0b17b7 100644 --- a/libs/@local/hashql/mir/src/interpret/error.rs +++ b/libs/@local/hashql/mir/src/interpret/error.rs @@ -14,7 +14,7 @@ use hashql_diagnostics::{ Diagnostic, Label, category::{DiagnosticCategory, TerminalDiagnosticCategory}, diagnostic::Message, - severity::Severity, + severity::Critical, }; use super::value::{Int, Ptr, Value, ValueTypeName}; @@ -26,26 +26,8 @@ use crate::body::{ /// Type alias for interpreter diagnostics. /// -/// The default severity kind is [`Severity`], which allows any severity level. -pub type InterpretDiagnostic = Diagnostic; - -/// Diagnostic subcategory for errors that occur while fulfilling a suspension. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub struct SuspensionDiagnosticCategory(pub &'static TerminalDiagnosticCategory); - -impl DiagnosticCategory for SuspensionDiagnosticCategory { - fn id(&self) -> Cow<'_, str> { - Cow::Borrowed("suspension") - } - - fn name(&self) -> Cow<'_, str> { - Cow::Borrowed("Suspension") - } - - fn subcategory(&self) -> Option<&dyn DiagnosticCategory> { - Some(self.0) - } -} +/// The default severity kind is [`Critical`]. +pub type InterpretDiagnostic = Diagnostic; // Terminal categories for ICEs const LOCAL_ACCESS: TerminalDiagnosticCategory = TerminalDiagnosticCategory { @@ -105,8 +87,6 @@ pub enum InterpretDiagnosticCategory { RuntimeLimit, /// Required input not provided. InputResolution, - /// Error from fulfilling a suspension (e.g. database query failure). - Suspension(SuspensionDiagnosticCategory), } impl DiagnosticCategory for InterpretDiagnosticCategory { @@ -127,7 +107,6 @@ impl DiagnosticCategory for InterpretDiagnosticCategory { Self::BoundsCheck => Some(&BOUNDS_CHECK), Self::RuntimeLimit => Some(&RUNTIME_LIMIT), Self::InputResolution => Some(&INPUT_RESOLUTION), - Self::Suspension(category) => Some(category), } } } @@ -383,20 +362,49 @@ pub enum RuntimeError<'heap, E, A: Allocator> { } impl RuntimeError<'_, E, A> { - /// Converts this runtime error into a diagnostic using the provided callstack. + /// Converts this runtime error into an [`InterpretDiagnostic`] 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( + /// + /// `on_suspension` converts the suspension payload `E` into a diagnostic. + /// When `E = !` (no suspension possible), the closure is never invoked. + /// + /// For callers that need a different output category (e.g. an orchestrator + /// wrapping interpreter errors), use [`into_diagnostic_with`](Self::into_diagnostic_with). + pub fn into_diagnostic( + self, + callstack: impl IntoIterator, + on_suspension: impl FnOnce(E) -> InterpretDiagnostic, + ) -> InterpretDiagnostic + where + Critical: Into, + { + self.into_diagnostic_with(callstack, on_suspension, core::convert::identity) + } + + /// Converts this runtime error into a diagnostic, lifting the category + /// through `map_category`. + /// + /// Like [`into_diagnostic`](Self::into_diagnostic), but allows the caller + /// to embed [`InterpretDiagnosticCategory`] inside a broader category + /// hierarchy. The `on_suspension` closure produces a diagnostic with the + /// same output category `C`. + pub fn into_diagnostic_with( self, callstack: impl IntoIterator, - on_suspension: impl FnOnce(E) -> InterpretDiagnostic, - ) -> InterpretDiagnostic { + on_suspension: impl FnOnce(E) -> Diagnostic, + on_otherwise: impl FnOnce(InterpretDiagnosticCategory) -> C, + ) -> Diagnostic + where + Critical: Into, + { let mut spans = callstack.into_iter(); let primary_span = spans.next().unwrap_or(SpanId::SYNTHETIC); - let mut diagnostic = self.make_diagnostic(primary_span, on_suspension); + let mut diagnostic = self.make_diagnostic(primary_span, on_suspension, on_otherwise); // Add callstack frames as secondary labels for span in spans { @@ -406,39 +414,85 @@ impl RuntimeError<'_, E, A> { diagnostic } - fn make_diagnostic( + fn make_diagnostic( self, span: SpanId, - on_suspension: impl FnOnce(E) -> InterpretDiagnostic, - ) -> InterpretDiagnostic { + on_suspension: impl FnOnce(E) -> Diagnostic, + on_otherwise: impl FnOnce(InterpretDiagnosticCategory) -> C, + ) -> Diagnostic + where + Critical: Into, + { 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::UninitializedLocal { local, decl } => uninitialized_local(span, local, decl) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::InvalidIndexType { base, index } => invalid_index_type(span, &base, &index) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::InvalidSubscriptType { base } => invalid_subscript_type(span, &base) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::InvalidProjectionType { base } => invalid_projection_type(span, &base) + .map_category(on_otherwise) + .map_severity(Into::into), Self::InvalidProjectionByNameType { base } => { invalid_projection_by_name_type(span, &base) + .map_category(on_otherwise) + .map_severity(Into::into) } - Self::UnknownField { base, field } => unknown_field(span, &base, field), - Self::UnknownFieldByName { base, field } => unknown_field_by_name(span, &base, field), + Self::UnknownField { base, field } => unknown_field(span, &base, field) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::UnknownFieldByName { base, field } => unknown_field_by_name(span, &base, field) + .map_category(on_otherwise) + .map_severity(Into::into), Self::StructFieldLengthMismatch { values, fields } => { struct_field_length_mismatch(span, values, fields) + .map_category(on_otherwise) + .map_severity(Into::into) } - 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), - Self::IntegerOverflow { operation } => integer_overflow(span, operation), + Self::InvalidDiscriminantType { r#type } => invalid_discriminant_type(span, &r#type) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::InvalidDiscriminant { value } => invalid_discriminant(span, value) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::UnreachableReached => unreachable_reached(span) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::BinaryTypeMismatch(mismatch) => binary_type_mismatch(span, *mismatch) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::UnaryTypeMismatch(mismatch) => unary_type_mismatch(span, *mismatch) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::ApplyNonPointer { r#type } => apply_non_pointer(span, &r#type) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::CallstackEmpty => callstack_empty(span) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::OutOfRange { length, index } => out_of_range(span, length, index) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::InputNotFound { name } => input_not_found(span, name) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::RecursionLimitExceeded { limit } => recursion_limit_exceeded(span, limit) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::IntegerOverflow { operation } => integer_overflow(span, operation) + .map_category(on_otherwise) + .map_severity(Into::into), Self::UnexpectedValueType { expected, actual } => { unexpected_value_type(span, &expected, &actual) + .map_category(on_otherwise) + .map_severity(Into::into) } - Self::InvalidConstructor { name } => invalid_constructor(span, name), + Self::InvalidConstructor { name } => invalid_constructor(span, name) + .map_category(on_otherwise) + .map_severity(Into::into), Self::Suspension(suspension) => on_suspension(suspension), } } @@ -505,7 +559,7 @@ fn uninitialized_local(span: SpanId, local: Local, decl: LocalDecl) -> Interpret }); let mut diagnostic = - Diagnostic::new(InterpretDiagnosticCategory::LocalAccess, Severity::Bug).primary( + Diagnostic::new(InterpretDiagnosticCategory::LocalAccess, Critical::BUG).primary( Label::new(span, format!("local `{name}` used before initialization")), ); @@ -524,7 +578,7 @@ fn uninitialized_local(span: SpanId, local: Local, decl: LocalDecl) -> Interpret fn invalid_index_type(span: SpanId, base: &TypeName, index: &TypeName) -> InterpretDiagnostic { let mut diagnostic = - Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug).primary( + Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Critical::BUG).primary( Label::new(span, format!("cannot index `{base}` with `{index}`")), ); @@ -536,7 +590,7 @@ fn invalid_index_type(span: SpanId, base: &TypeName, index: &TypeName) -> Interp } fn invalid_subscript_type(span: SpanId, base: &TypeName) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug) + let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Critical::BUG) .primary(Label::new(span, format!("cannot subscript `{base}`"))); diagnostic.add_message(Message::help( @@ -548,7 +602,7 @@ fn invalid_subscript_type(span: SpanId, base: &TypeName) -> InterpretDiagnostic fn invalid_projection_type(span: SpanId, base: &TypeName) -> InterpretDiagnostic { let mut diagnostic = - Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug).primary( + Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Critical::BUG).primary( Label::new(span, format!("cannot project field from `{base}`")), ); @@ -561,7 +615,7 @@ fn invalid_projection_type(span: SpanId, base: &TypeName) -> InterpretDiagnostic fn invalid_projection_by_name_type(span: SpanId, base: &TypeName) -> InterpretDiagnostic { let mut diagnostic = - Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug).primary( + Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Critical::BUG).primary( Label::new(span, format!("cannot project named field from `{base}`")), ); @@ -573,7 +627,7 @@ fn invalid_projection_by_name_type(span: SpanId, base: &TypeName) -> InterpretDi } fn unknown_field(span: SpanId, base: &TypeName, field: FieldIndex) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug) + let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Critical::BUG) .primary(Label::new( span, format!("field index {field} does not exist on `{base}`"), @@ -588,7 +642,7 @@ fn unknown_field(span: SpanId, base: &TypeName, field: FieldIndex) -> InterpretD fn unknown_field_by_name(span: SpanId, base: &TypeName, field: Symbol) -> InterpretDiagnostic { let mut diagnostic = - Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug).primary( + Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Critical::BUG).primary( Label::new(span, format!("field `{field}` does not exist on `{base}`")), ); @@ -600,7 +654,7 @@ fn unknown_field_by_name(span: SpanId, base: &TypeName, field: Symbol) -> Interp } fn invalid_discriminant_type(span: SpanId, r#type: &TypeName) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug) + let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Critical::BUG) .primary(Label::new( span, format!("switch discriminant has type `{type}`, expected `Integer`"), @@ -623,7 +677,7 @@ fn binary_type_mismatch( rhs, }: BinaryTypeMismatch, ) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug) + let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Critical::BUG) .primary(Label::new( span, format!( @@ -653,7 +707,7 @@ fn unary_type_mismatch( value, }: UnaryTypeMismatch, ) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug) + let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Critical::BUG) .primary(Label::new( span, format!("cannot apply `{}` to `{}`", op.as_str(), value.type_name()), @@ -670,7 +724,7 @@ fn unary_type_mismatch( fn apply_non_pointer(span: SpanId, r#type: &TypeName) -> InterpretDiagnostic { let mut diagnostic = - Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug).primary( + Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Critical::BUG).primary( Label::new(span, format!("cannot call `{type}` as a function")), ); @@ -687,7 +741,7 @@ fn unexpected_value_type( actual: &TypeName, ) -> InterpretDiagnostic { let mut diagnostic = - Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug).primary( + Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Critical::BUG).primary( Label::new(span, format!("expected `{expected}`, found `{actual}`")), ); @@ -700,7 +754,7 @@ fn unexpected_value_type( fn invalid_constructor(span: SpanId, name: Symbol) -> InterpretDiagnostic { let mut diagnostic = - Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug).primary( + Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Critical::BUG).primary( Label::new(span, format!("unrecognized opaque constructor `{name}`")), ); @@ -718,7 +772,7 @@ fn invalid_constructor(span: SpanId, name: Symbol) -> InterpretDiagnostic { fn struct_field_length_mismatch(span: SpanId, values: usize, fields: usize) -> InterpretDiagnostic { let mut diagnostic = Diagnostic::new( InterpretDiagnosticCategory::StructuralInvariant, - Severity::Bug, + Critical::BUG, ) .primary(Label::new( span, @@ -737,7 +791,7 @@ fn struct_field_length_mismatch(span: SpanId, values: usize, fields: usize) -> I // ============================================================================= fn invalid_discriminant(span: SpanId, value: Int) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::ControlFlow, Severity::Bug) + let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::ControlFlow, Critical::BUG) .primary(Label::new( span, format!("switch discriminant `{value}` has no matching branch"), @@ -751,7 +805,7 @@ fn invalid_discriminant(span: SpanId, value: Int) -> InterpretDiagnostic { } fn unreachable_reached(span: SpanId) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::ControlFlow, Severity::Bug) + let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::ControlFlow, Critical::BUG) .primary(Label::new(span, "reached unreachable code")); diagnostic.add_message(Message::help( @@ -763,7 +817,7 @@ fn unreachable_reached(span: SpanId) -> InterpretDiagnostic { #[coverage(off)] fn callstack_empty(span: SpanId) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::ControlFlow, Severity::Bug) + let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::ControlFlow, Critical::BUG) .primary(Label::new(span, "attempted to step with empty callstack")); diagnostic.add_message(Message::help( @@ -778,7 +832,7 @@ fn callstack_empty(span: SpanId) -> InterpretDiagnostic { // ============================================================================= fn out_of_range(span: SpanId, length: usize, index: Int) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::BoundsCheck, Severity::Error) + let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::BoundsCheck, Critical::ERROR) .primary(Label::new( span, format!("index `{index}` is out of bounds for length {length}"), @@ -796,7 +850,7 @@ fn out_of_range(span: SpanId, length: usize, index: Int) -> InterpretDiagnostic fn input_not_found(span: SpanId, name: Symbol) -> InterpretDiagnostic { let mut diagnostic = Diagnostic::new( InterpretDiagnosticCategory::InputResolution, - Severity::Error, + Critical::ERROR, ) .primary(Label::new(span, format!("input `{name}` not found"))); @@ -811,7 +865,7 @@ fn input_not_found(span: SpanId, name: Symbol) -> InterpretDiagnostic { fn recursion_limit_exceeded(span: SpanId, limit: usize) -> InterpretDiagnostic { let mut diagnostic = - Diagnostic::new(InterpretDiagnosticCategory::RuntimeLimit, Severity::Error).primary( + Diagnostic::new(InterpretDiagnosticCategory::RuntimeLimit, Critical::ERROR).primary( Label::new(span, format!("recursion limit of {limit} exceeded")), ); @@ -824,7 +878,7 @@ fn recursion_limit_exceeded(span: SpanId, limit: usize) -> InterpretDiagnostic { fn integer_overflow(span: SpanId, operation: &str) -> InterpretDiagnostic { let mut diagnostic = - Diagnostic::new(InterpretDiagnosticCategory::RuntimeLimit, Severity::Error).primary( + Diagnostic::new(InterpretDiagnosticCategory::RuntimeLimit, Critical::ERROR).primary( Label::new( span, format!("integer {operation} produced a result outside the supported range"), diff --git a/libs/@local/hashql/mir/src/interpret/inputs.rs b/libs/@local/hashql/mir/src/interpret/inputs.rs index 134567cee8c..5b8d282831e 100644 --- a/libs/@local/hashql/mir/src/interpret/inputs.rs +++ b/libs/@local/hashql/mir/src/interpret/inputs.rs @@ -94,6 +94,7 @@ impl Inputs<'_> { } impl Default for Inputs<'_> { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/interpret/locals.rs b/libs/@local/hashql/mir/src/interpret/locals.rs index f35dc1dccf4..41e88e0c9d7 100644 --- a/libs/@local/hashql/mir/src/interpret/locals.rs +++ b/libs/@local/hashql/mir/src/interpret/locals.rs @@ -323,7 +323,11 @@ impl<'ctx, 'heap, A: Allocator> Locals<'ctx, 'heap, A> { // SAFETY: We have just filled the slice with values, and no errors have occurred. let values = unsafe { values.assume_init() }; - Ok(Value::Struct(Struct::new_unchecked(fields, values))) + // SAFETY: `fields` comes from `AggregateKind::Struct`, where MIR construction + // guarantees sorted field order. `values` has the same length by construction. + Ok(Value::Struct(unsafe { + Struct::new_unchecked(fields, values) + })) } /// Constructs an aggregate value (tuple, struct, list, dict, opaque, closure). diff --git a/libs/@local/hashql/mir/src/interpret/runtime.rs b/libs/@local/hashql/mir/src/interpret/runtime.rs index 8d827100982..5238308e9a5 100644 --- a/libs/@local/hashql/mir/src/interpret/runtime.rs +++ b/libs/@local/hashql/mir/src/interpret/runtime.rs @@ -294,6 +294,7 @@ pub struct RuntimeConfig { } impl Default for RuntimeConfig { + #[inline] fn default() -> Self { Self { recursion_limit: 1024, diff --git a/libs/@local/hashql/mir/src/interpret/suspension/temporal.rs b/libs/@local/hashql/mir/src/interpret/suspension/temporal.rs index 5a9f27e6659..55875f4dd34 100644 --- a/libs/@local/hashql/mir/src/interpret/suspension/temporal.rs +++ b/libs/@local/hashql/mir/src/interpret/suspension/temporal.rs @@ -17,12 +17,14 @@ use crate::interpret::value::Int; pub struct Timestamp(Int); impl From for Timestamp { + #[inline] fn from(value: Int) -> Self { Self(value) } } impl From for Int { + #[inline] fn from(value: Timestamp) -> Self { value.0 } diff --git a/libs/@local/hashql/mir/src/interpret/tests.rs b/libs/@local/hashql/mir/src/interpret/tests.rs index a14cb7d88c1..75a8ec6ec8f 100644 --- a/libs/@local/hashql/mir/src/interpret/tests.rs +++ b/libs/@local/hashql/mir/src/interpret/tests.rs @@ -1735,6 +1735,7 @@ fn ice_struct_field_length_mismatch() { /// /// where `pinned` = `Opaque(TransactionTime, Opaque(Timestamp, Integer(pinned_ms)))` and /// `variable` wraps an interval with inclusive start and unbounded end. +#[expect(unsafe_code)] fn make_temporal_axes<'heap>( interner: &Interner<'heap>, pinned_ms: i128, @@ -1770,7 +1771,9 @@ fn make_temporal_axes<'heap>( // Interval(Struct { start, end }) let interval_fields = interner.symbols.intern_slice(&[sym::end, sym::start]); - let interval_struct = Struct::new_unchecked(interval_fields, Rc::new([end_bound, start_bound])); + // SAFETY: e is before s in the alphabetical order + let interval_struct = + unsafe { Struct::new_unchecked(interval_fields, Rc::new([end_bound, start_bound])) }; let interval = Value::Opaque(Opaque::new( sym::path::Interval, Rc::new(Value::Struct(interval_struct)), @@ -1781,7 +1784,8 @@ fn make_temporal_axes<'heap>( // PinnedTransactionTimeTemporalAxes(Struct { pinned, variable }) let axes_fields = interner.symbols.intern_slice(&[sym::pinned, sym::variable]); - let axes_struct = Struct::new_unchecked(axes_fields, Rc::new([pinned, variable])); + // SAFETY: p is before v in the alphabetical order + let axes_struct = unsafe { Struct::new_unchecked(axes_fields, Rc::new([pinned, variable])) }; Value::Opaque(Opaque::new( sym::path::PinnedTransactionTimeTemporalAxes, diff --git a/libs/@local/hashql/mir/src/interpret/value/dict.rs b/libs/@local/hashql/mir/src/interpret/value/dict.rs index 7f3e9cbeb8b..286564ef75c 100644 --- a/libs/@local/hashql/mir/src/interpret/value/dict.rs +++ b/libs/@local/hashql/mir/src/interpret/value/dict.rs @@ -5,6 +5,31 @@ use core::{alloc::Allocator, cmp}; use super::Value; /// An ordered dictionary mapping values to values. +/// +/// Keys are ordered by [`Value`]'s [`Ord`] implementation. +/// +/// # Examples +/// +/// ``` +/// # #![feature(allocator_api)] +/// # extern crate alloc; +/// use alloc::alloc::Global; +/// +/// use hashql_mir::interpret::value::{Dict, Value}; +/// +/// let mut dict: Dict<'_, Global> = Dict::new(); +/// dict.insert(Value::Integer(1.into()), Value::Integer(100.into())); +/// dict.insert(Value::Integer(2.into()), Value::Integer(200.into())); +/// +/// assert_eq!(dict.len(), 2); +/// assert_eq!( +/// dict.get(&Value::Integer(1.into())), +/// Some(&Value::Integer(100.into())), +/// ); +/// +/// // Missing keys return None +/// assert_eq!(dict.get(&Value::Integer(99.into())), None); +/// ``` #[derive(Debug, Clone)] pub struct Dict<'heap, A: Allocator> { inner: rpds::RedBlackTreeMap, Value<'heap, A>>, @@ -12,6 +37,19 @@ pub struct Dict<'heap, A: Allocator> { impl<'heap, A: Allocator> Dict<'heap, A> { /// Creates a new empty dictionary. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::Dict; + /// + /// let dict: Dict<'_, Global> = Dict::new(); + /// assert!(dict.is_empty()); + /// ``` + #[inline] #[must_use] pub fn new() -> Self { Self { @@ -20,18 +58,71 @@ impl<'heap, A: Allocator> Dict<'heap, A> { } /// Returns the number of key-value pairs in the dictionary. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{Dict, Value}; + /// + /// let mut dict: Dict<'_, Global> = Dict::new(); + /// assert_eq!(dict.len(), 0); + /// + /// dict.insert(Value::Integer(1.into()), Value::Unit); + /// assert_eq!(dict.len(), 1); + /// ``` + #[inline] #[must_use] pub fn len(&self) -> usize { self.inner.size() } /// Returns `true` if the dictionary contains no elements. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{Dict, Value}; + /// + /// let mut dict: Dict<'_, Global> = Dict::new(); + /// assert!(dict.is_empty()); + /// + /// dict.insert(Value::Integer(1.into()), Value::Unit); + /// assert!(!dict.is_empty()); + /// ``` + #[inline] #[must_use] pub fn is_empty(&self) -> bool { self.inner.is_empty() } /// Inserts a key-value pair into the dictionary. + /// + /// If the key already exists, the value is replaced. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{Dict, Value}; + /// + /// let mut dict: Dict<'_, Global> = Dict::new(); + /// dict.insert(Value::Integer(1.into()), Value::Integer(100.into())); + /// dict.insert(Value::Integer(1.into()), Value::Integer(200.into())); + /// + /// assert_eq!(dict.len(), 1); + /// assert_eq!( + /// dict.get(&Value::Integer(1.into())), + /// Some(&Value::Integer(200.into())), + /// ); + /// ``` pub fn insert(&mut self, key: Value<'heap, A>, value: Value<'heap, A>) where A: Clone, @@ -40,12 +131,48 @@ impl<'heap, A: Allocator> Dict<'heap, A> { } /// Returns a reference to the value associated with the `key`. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{Dict, Value}; + /// + /// let mut dict: Dict<'_, Global> = Dict::new(); + /// dict.insert(Value::Integer(1.into()), Value::Integer(100.into())); + /// + /// assert_eq!( + /// dict.get(&Value::Integer(1.into())), + /// Some(&Value::Integer(100.into())) + /// ); + /// assert_eq!(dict.get(&Value::Integer(2.into())), None); + /// ``` #[must_use] pub fn get(&self, key: &Value<'heap, A>) -> Option<&Value<'heap, A>> { self.inner.get(key) } - /// Returns a mutable reference to the value for `key`, inserting [`Value::Unit`] if absent. + /// Returns a mutable reference to the value for `key`, inserting + /// [`Value::Unit`] if absent. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{Dict, Value}; + /// + /// let mut dict: Dict<'_, Global> = Dict::new(); + /// + /// // Accessing a missing key inserts Unit, then returns a mutable reference + /// let key = Value::Integer(1.into()); + /// *dict.get_mut(&key) = Value::Integer(42.into()); + /// + /// assert_eq!(dict.get(&key), Some(&Value::Integer(42.into()))); + /// ``` pub fn get_mut(&mut self, key: &Value<'heap, A>) -> &mut Value<'heap, A> where A: Clone, @@ -58,6 +185,28 @@ impl<'heap, A: Allocator> Dict<'heap, A> { } /// Returns an iterator over key-value pairs. + /// + /// Pairs are yielded in key order. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{Dict, Value}; + /// + /// let mut dict: Dict<'_, Global> = Dict::new(); + /// dict.insert(Value::Integer(2.into()), Value::Unit); + /// dict.insert(Value::Integer(1.into()), Value::Unit); + /// + /// let keys: Vec<_> = dict.iter().map(|(k, _)| k.clone()).collect(); + /// // Keys are yielded in sorted order + /// assert_eq!( + /// keys, + /// vec![Value::Integer(1.into()), Value::Integer(2.into())] + /// ); + /// ``` #[must_use] pub fn iter( &self, @@ -91,6 +240,7 @@ impl Ord for Dict<'_, A> { } impl Default for Dict<'_, A> { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/interpret/value/list.rs b/libs/@local/hashql/mir/src/interpret/value/list.rs index ce457e1ad3f..4c19f87111e 100644 --- a/libs/@local/hashql/mir/src/interpret/value/list.rs +++ b/libs/@local/hashql/mir/src/interpret/value/list.rs @@ -5,6 +5,41 @@ use core::{alloc::Allocator, cmp}; use super::{Int, Value}; /// An ordered list of values. +/// +/// Supports negative indexing: `-1` is the last element, `-2` is +/// second-to-last, and so on (see [`get`](Self::get)). +/// +/// # Examples +/// +/// ``` +/// # #![feature(allocator_api)] +/// # extern crate alloc; +/// use alloc::alloc::Global; +/// +/// use hashql_mir::interpret::value::{Int, List, Value}; +/// +/// let mut list: List<'_, Global> = List::new(); +/// list.push_back(Value::Integer(10.into())); +/// list.push_back(Value::Integer(20.into())); +/// list.push_back(Value::Integer(30.into())); +/// +/// // Forward indexing +/// assert_eq!(list.get(Int::from(0_i32)), Some(&Value::Integer(10.into()))); +/// +/// // Negative indexing counts from the end +/// assert_eq!( +/// list.get(Int::from(-1_i32)), +/// Some(&Value::Integer(30.into())) +/// ); +/// assert_eq!( +/// list.get(Int::from(-3_i32)), +/// Some(&Value::Integer(10.into())) +/// ); +/// +/// // Out of bounds +/// assert_eq!(list.get(Int::from(3_i32)), None); +/// assert_eq!(list.get(Int::from(-4_i32)), None); +/// ``` #[derive(Debug, Clone)] pub struct List<'heap, A: Allocator> { inner: rpds::Vector>, @@ -12,6 +47,19 @@ pub struct List<'heap, A: Allocator> { impl<'heap, A: Allocator> List<'heap, A> { /// Creates a new empty list. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::List; + /// + /// let list: List<'_, Global> = List::new(); + /// assert!(list.is_empty()); + /// ``` + #[inline] #[must_use] pub fn new() -> Self { Self { @@ -20,18 +68,66 @@ impl<'heap, A: Allocator> List<'heap, A> { } /// Returns the number of elements in the list. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{List, Value}; + /// + /// let mut list: List<'_, Global> = List::new(); + /// assert_eq!(list.len(), 0); + /// + /// list.push_back(Value::Integer(1.into())); + /// assert_eq!(list.len(), 1); + /// ``` + #[inline] #[must_use] pub fn len(&self) -> usize { self.inner.len() } /// Returns `true` if the list contains no elements. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{List, Value}; + /// + /// let mut list: List<'_, Global> = List::new(); + /// assert!(list.is_empty()); + /// + /// list.push_back(Value::Unit); + /// assert!(!list.is_empty()); + /// ``` + #[inline] #[must_use] pub fn is_empty(&self) -> bool { self.inner.is_empty() } /// Appends a value to the end of the list. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{Int, List, Value}; + /// + /// let mut list: List<'_, Global> = List::new(); + /// list.push_back(Value::Integer(10.into())); + /// list.push_back(Value::Integer(20.into())); + /// + /// assert_eq!(list.get(Int::from(0_i32)), Some(&Value::Integer(10.into()))); + /// assert_eq!(list.get(Int::from(1_i32)), Some(&Value::Integer(20.into()))); + /// ``` pub fn push_back(&mut self, value: Value<'heap, A>) where A: Clone, @@ -40,6 +136,29 @@ impl<'heap, A: Allocator> List<'heap, A> { } /// Returns a reference to the element at the given `index`. + /// + /// Supports negative indexing: `-1` is the last element, `-2` is + /// second-to-last, etc. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{Int, List, Value}; + /// + /// let mut list: List<'_, Global> = List::new(); + /// list.push_back(Value::Integer(10.into())); + /// list.push_back(Value::Integer(20.into())); + /// + /// assert_eq!(list.get(Int::from(0_i32)), Some(&Value::Integer(10.into()))); + /// assert_eq!( + /// list.get(Int::from(-1_i32)), + /// Some(&Value::Integer(20.into())) + /// ); + /// assert_eq!(list.get(Int::from(2_i32)), None); + /// ``` #[must_use] pub fn get(&self, index: Int) -> Option<&Value<'heap, A>> { let index = isize::try_from(index.as_int()).ok()?; @@ -59,6 +178,23 @@ impl<'heap, A: Allocator> List<'heap, A> { /// Returns a mutable reference to the element at the given `index`. /// /// Supports negative indexing: `-1` is the last element, `-2` is second-to-last, etc. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{Int, List, Value}; + /// + /// let mut list: List<'_, Global> = List::new(); + /// list.push_back(Value::Integer(1.into())); + /// list.push_back(Value::Integer(2.into())); + /// + /// // Mutate the last element via negative index + /// *list.get_mut(Int::from(-1_i32)).unwrap() = Value::Integer(99.into()); + /// assert_eq!(list.get(Int::from(1_i32)), Some(&Value::Integer(99.into()))); + /// ``` pub fn get_mut(&mut self, index: Int) -> Option<&mut Value<'heap, A>> where A: Clone, @@ -78,6 +214,22 @@ impl<'heap, A: Allocator> List<'heap, A> { } /// Returns an iterator over the list's elements. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{List, Value}; + /// + /// let mut list: List<'_, Global> = List::new(); + /// list.push_back(Value::Integer(1.into())); + /// list.push_back(Value::Integer(2.into())); + /// + /// let values: Vec<_> = list.iter().collect(); + /// assert_eq!(values.len(), 2); + /// ``` #[must_use] pub fn iter(&self) -> impl ExactSizeIterator> + DoubleEndedIterator { self.inner.iter() @@ -108,6 +260,7 @@ impl Ord for List<'_, A> { } impl Default for List<'_, A> { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/interpret/value/mod.rs b/libs/@local/hashql/mir/src/interpret/value/mod.rs index aba5483c64c..6ed8e74d1a0 100644 --- a/libs/@local/hashql/mir/src/interpret/value/mod.rs +++ b/libs/@local/hashql/mir/src/interpret/value/mod.rs @@ -125,6 +125,29 @@ pub enum Value<'heap, A: Allocator = Global> { impl<'heap, A: Allocator> Value<'heap, A> { const UNIT: Self = Self::Unit; + /// Returns a displayable representation of this value's runtime type. + /// + /// Primitives produce their type name (`"Integer"`, `"String"`), + /// aggregates include their structure (`"(x: Integer, y: String)"` + /// for structs, `"(Integer, String)"` for tuples), and opaques + /// include their wrapper name (`"UserId(Integer)"`). + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::Value; + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// + /// assert_eq!(Value::<'_, Global>::Unit.type_name().to_string(), "()"); + /// assert_eq!( + /// Value::<'_, Global>::Integer(42.into()) + /// .type_name() + /// .to_string(), + /// "Integer" + /// ); + /// ``` pub fn type_name(&self) -> ValueTypeName<'_, 'heap, A> { ValueTypeName::from(self) } @@ -146,13 +169,45 @@ impl<'heap, A: Allocator> Value<'heap, A> { /// Indexes into this value using another value as the index. /// - /// For lists, the index must be an integer. For dicts, any value can be used as a key. + /// For lists, the index must be an integer (supports negative indexing). + /// For dicts, any value can be used as a key. /// Returns [`Value::Unit`] if the index is not found. /// /// # Errors /// /// Returns an error if this value is not subscriptable (not a list or dict), /// or if the index type is invalid for the collection type. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{Dict, Int, List, Value}; + /// + /// // List subscript with integer index + /// let mut list: List<'_, Global> = List::new(); + /// list.push_back(Value::Integer(10.into())); + /// let list_val = Value::List(list); + /// + /// let result = list_val.subscript::<()>(&Value::Integer(0.into())).unwrap(); + /// assert_eq!(result, &Value::Integer(10.into())); + /// + /// // Dict subscript with any key type + /// let mut dict: Dict<'_, Global> = Dict::new(); + /// dict.insert(Value::Integer(1.into()), Value::Integer(100.into())); + /// let dict_val = Value::Dict(dict); + /// + /// let result = dict_val.subscript::<()>(&Value::Integer(1.into())).unwrap(); + /// assert_eq!(result, &Value::Integer(100.into())); + /// + /// // Missing key returns Unit + /// let result = dict_val + /// .subscript::<()>(&Value::Integer(99.into())) + /// .unwrap(); + /// assert_eq!(result, &Value::Unit); + /// ``` #[inline] pub fn subscript<'this, 'index, E>( &'this self, @@ -226,11 +281,31 @@ impl<'heap, A: Allocator> Value<'heap, A> { /// Projects a field from this value by index. /// - /// Works on structs and tuples. + /// Works on structs, tuples, and opaques (projects through the wrapper). /// /// # Errors /// /// Returns an error if this value is not projectable or the field index is invalid. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::{ + /// body::place::FieldIndex, + /// interpret::value::{Tuple, Value}, + /// }; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let values: Rc<[Value]> = Rc::from(vec![Value::Integer(10.into()), Value::Integer(20.into())]); + /// let tuple = Value::Tuple(Tuple::new(values).unwrap()); + /// + /// let field = tuple.project::<()>(FieldIndex::new(1)).unwrap(); + /// assert_eq!(field, &Value::Integer(20.into())); + /// + /// // Out-of-bounds index returns an error + /// assert!(tuple.project::<()>(FieldIndex::new(5)).is_err()); + /// ``` #[inline] pub fn project<'this, E>( &'this self, @@ -308,11 +383,37 @@ impl<'heap, A: Allocator> Value<'heap, A> { /// Projects a field from this value by name. /// - /// Only works on structs. + /// Only works on structs and opaques (projects through the wrapper). /// /// # Errors /// /// Returns an error if this value is not a struct or the field name is not found. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// # use hashql_core::heap::Heap; + /// # use hashql_mir::intern::Interner; + /// use hashql_mir::interpret::value::{StructBuilder, Value}; + /// + /// let heap = Heap::new(); + /// let interner = Interner::new(&heap); + /// + /// let mut builder = StructBuilder::<'_, Global, 1>::new(); + /// let name = heap.intern_symbol("x"); + /// builder.push(name, Value::Integer(42.into())); + /// let s = Value::Struct(builder.finish(&interner.symbols, Global)); + /// + /// let field = s.project_by_name::<()>(name).unwrap(); + /// assert_eq!(field, &Value::Integer(42.into())); + /// + /// // Unknown field returns an error + /// let unknown = heap.intern_symbol("z"); + /// assert!(s.project_by_name::<()>(unknown).is_err()); + /// ``` pub fn project_by_name<'this, E>( &'this self, index: Symbol<'heap>, diff --git a/libs/@local/hashql/mir/src/interpret/value/num.rs b/libs/@local/hashql/mir/src/interpret/value/num.rs index 5ec17d7a585..f30fcc22b13 100644 --- a/libs/@local/hashql/mir/src/interpret/value/num.rs +++ b/libs/@local/hashql/mir/src/interpret/value/num.rs @@ -14,7 +14,20 @@ use crate::macros::{forward_ref_binop, forward_ref_unop}; /// A floating-point number value with total ordering semantics. /// /// Wraps an [`f64`] and implements [`Ord`] using [`f64::total_cmp`], which follows -/// the IEEE 754 `totalOrder` predicate. +/// the IEEE 754 `totalOrder` predicate. Negative zero and positive zero are +/// treated as equal. +/// +/// # Examples +/// +/// ``` +/// use hashql_mir::interpret::value::Num; +/// +/// let n = Num::from(3.14); +/// assert_eq!(n.as_f64(), 3.14); +/// +/// // Negative and positive zero are equal +/// assert_eq!(Num::from(-0.0), Num::from(0.0)); +/// ``` #[derive(Debug, Copy, Clone)] pub struct Num { value: f64, @@ -22,6 +35,16 @@ pub struct Num { impl Num { /// Returns the underlying [`f64`] value. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::Num; + /// + /// let n = Num::from(2.5); + /// assert_eq!(n.as_f64(), 2.5); + /// ``` + #[inline] #[must_use] pub const fn as_f64(self) -> f64 { self.value diff --git a/libs/@local/hashql/mir/src/interpret/value/opaque.rs b/libs/@local/hashql/mir/src/interpret/value/opaque.rs index dfbae62c0af..8c23bd33c78 100644 --- a/libs/@local/hashql/mir/src/interpret/value/opaque.rs +++ b/libs/@local/hashql/mir/src/interpret/value/opaque.rs @@ -11,11 +11,28 @@ use hashql_core::symbol::Symbol; use super::Value; -/// An opaque wrapper around a value. +/// An opaque wrapper around a [`Value`]. /// -/// Wraps a value with a named type tag, representing nominal types or -/// newtype wrappers. The name distinguishes different opaque types even -/// when their underlying values are structurally identical. +/// Pairs a [`Symbol`] name with an inner value to represent nominal types +/// (newtypes, branded types). Two opaques with the same inner value but +/// different names are not equal. +/// +/// # Examples +/// +/// ``` +/// use hashql_mir::interpret::value::{Opaque, Value}; +/// # use hashql_core::heap::Heap; +/// # extern crate alloc; +/// # use alloc::rc::Rc; +/// +/// let heap = Heap::new(); +/// let name = heap.intern_symbol("UserId"); +/// let inner = Rc::new(Value::Integer(42.into())); +/// +/// let opaque = Opaque::new(name, inner); +/// assert_eq!(opaque.name().as_str(), "UserId"); +/// assert_eq!(opaque.value(), &Value::Integer(42.into())); +/// ``` #[derive(Debug, Clone)] pub struct Opaque<'heap, A: Allocator> { name: Symbol<'heap>, @@ -24,6 +41,23 @@ pub struct Opaque<'heap, A: Allocator> { impl<'heap, A: Allocator> Opaque<'heap, A> { /// Creates a new opaque value with the given `name` and wrapped `value`. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::{Opaque, Value}; + /// # use hashql_core::heap::Heap; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let heap = Heap::new(); + /// let opaque = Opaque::new( + /// heap.intern_symbol("Meters"), + /// Rc::new(Value::Integer(100.into())), + /// ); + /// assert_eq!(opaque.name().as_str(), "Meters"); + /// ``` + #[inline] #[must_use] pub fn new(name: Symbol<'heap>, value: impl Into, A>>) -> Self { Self { @@ -33,17 +67,68 @@ impl<'heap, A: Allocator> Opaque<'heap, A> { } /// Returns the type name of this opaque value. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::{Opaque, Value}; + /// # use hashql_core::heap::Heap; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let heap = Heap::new(); + /// let opaque = Opaque::new( + /// heap.intern_symbol("UserId"), + /// Rc::new(Value::Integer(1.into())), + /// ); + /// assert_eq!(opaque.name().as_str(), "UserId"); + /// ``` + #[inline] #[must_use] pub const fn name(&self) -> Symbol<'heap> { self.name } /// Returns a reference to the wrapped value. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::{Opaque, Value}; + /// # use hashql_core::heap::Heap; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let heap = Heap::new(); + /// let opaque = Opaque::new( + /// heap.intern_symbol("Tag"), + /// Rc::new(Value::Integer(42.into())), + /// ); + /// assert_eq!(opaque.value(), &Value::Integer(42.into())); + /// ``` + #[inline] #[must_use] pub fn value(&self) -> &Value<'heap, A> { &self.value } + /// Returns a mutable reference to the wrapped value. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::{Opaque, Value}; + /// # use hashql_core::heap::Heap; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let heap = Heap::new(); + /// let name = heap.intern_symbol("Counter"); + /// let mut opaque = Opaque::new(name, Rc::new(Value::Integer(0.into()))); + /// + /// *opaque.value_mut() = Value::Integer(42.into()); + /// assert_eq!(opaque.value(), &Value::Integer(42.into())); + /// ``` #[must_use] pub fn value_mut(&mut self) -> &mut Value<'heap, A> where @@ -52,7 +137,49 @@ impl<'heap, A: Allocator> Opaque<'heap, A> { Rc::make_mut(&mut self.value) } + /// Extracts the inner [`Value`], discarding the name. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::{Opaque, Value}; + /// # use hashql_core::heap::Heap; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let heap = Heap::new(); + /// let name = heap.intern_symbol("UserId"); + /// let opaque = Opaque::new(name, Rc::new(Value::Integer(42.into()))); + /// + /// let inner = opaque.into_value(); + /// assert_eq!(inner, Value::Integer(42.into())); + /// ``` + pub fn into_value(self) -> Value<'heap, A> + where + A: Clone, + { + Rc::unwrap_or_clone(self.value) + } + /// Returns a displayable representation of this opaque type's name. + /// + /// The format is `Name(InnerType)` for most inner values, or + /// `Name(field: Type, ...)` when the inner value is a struct or tuple + /// (parentheses from the inner type are reused). + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::{Opaque, Value}; + /// # use hashql_core::heap::Heap; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let heap = Heap::new(); + /// let name = heap.intern_symbol("UserId"); + /// let opaque = Opaque::new(name, Rc::new(Value::Integer(42.into()))); + /// assert_eq!(opaque.type_name().to_string(), "UserId(Integer)"); + /// ``` pub fn type_name(&self) -> impl Display { fmt::from_fn(|fmt| { // check if the inner type is a struct or tuple, in which case we elide the `()` diff --git a/libs/@local/hashql/mir/src/interpret/value/ptr.rs b/libs/@local/hashql/mir/src/interpret/value/ptr.rs index 9349dc09bbe..32379cdb026 100644 --- a/libs/@local/hashql/mir/src/interpret/value/ptr.rs +++ b/libs/@local/hashql/mir/src/interpret/value/ptr.rs @@ -8,6 +8,16 @@ use crate::def::DefId; /// /// Points to a function definition identified by its [`DefId`]. Used to /// represent first-class functions and closures in the interpreter. +/// +/// # Examples +/// +/// ``` +/// use hashql_mir::{def::DefId, interpret::value::Ptr}; +/// +/// let ptr = Ptr::new(DefId::DICT_INSERT); +/// assert_eq!(ptr.def(), DefId::DICT_INSERT); +/// assert_eq!(ptr.to_string(), format!("*{}", DefId::DICT_INSERT)); +/// ``` #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Ptr { value: DefId, @@ -15,12 +25,32 @@ pub struct Ptr { impl Ptr { /// Creates a new function pointer from a [`DefId`]. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::{def::DefId, interpret::value::Ptr}; + /// + /// let ptr = Ptr::new(DefId::DICT_INSERT); + /// assert_eq!(ptr.def(), DefId::DICT_INSERT); + /// ``` + #[inline] #[must_use] pub const fn new(value: DefId) -> Self { Self { value } } /// Returns the [`DefId`] this pointer references. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::{def::DefId, interpret::value::Ptr}; + /// + /// let ptr = Ptr::from(DefId::DICT_INSERT); + /// assert_eq!(ptr.def(), DefId::DICT_INSERT); + /// ``` + #[inline] #[must_use] pub const fn def(self) -> DefId { self.value @@ -28,6 +58,7 @@ impl Ptr { } impl From for Ptr { + #[inline] fn from(value: DefId) -> Self { Self::new(value) } diff --git a/libs/@local/hashql/mir/src/interpret/value/str.rs b/libs/@local/hashql/mir/src/interpret/value/str.rs index ee211df869d..f077b7bd039 100644 --- a/libs/@local/hashql/mir/src/interpret/value/str.rs +++ b/libs/@local/hashql/mir/src/interpret/value/str.rs @@ -1,9 +1,16 @@ //! String representation for the MIR interpreter. use alloc::{alloc::Global, rc::Rc}; -use core::{alloc::Allocator, cmp, fmt}; +use core::{ + alloc::{AllocError, Allocator}, + cmp, fmt, +}; -use hashql_core::{symbol::Symbol, value::String}; +use hashql_core::{ + heap::{FromIn as _, TryCloneIn}, + symbol::Symbol, + value::String, +}; /// Internal storage for string values. #[derive(Clone)] @@ -43,9 +50,22 @@ impl Ord for StrInner<'_, A> { /// A string value. /// -/// Supports both owned strings (via [`Rc`]) and borrowed interned -/// symbols. This dual representation allows efficient handling of both -/// dynamically created strings and compile-time literals. +/// Use [`as_str`](Self::as_str) to access the content. Strings that +/// originate from a [`Heap`](hashql_core::heap::Heap) can be detached +/// with [`into_owned_in`](Self::into_owned_in) to outlive that heap. +/// +/// # Examples +/// +/// ``` +/// use hashql_mir::interpret::value::Str; +/// # extern crate alloc; +/// # use alloc::rc::Rc; +/// +/// let a = Str::from(Rc::::from("hello")); +/// let b = Str::from(Rc::::from("hello")); +/// assert_eq!(a.as_str(), "hello"); +/// assert_eq!(a, b); +/// ``` #[derive(Clone)] pub struct Str<'heap, A: Allocator = Global> { inner: StrInner<'heap, A>, @@ -53,6 +73,18 @@ pub struct Str<'heap, A: Allocator = Global> { impl Str<'_, A> { /// Returns this string as a string slice. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::Str; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let s = Str::from(Rc::::from("hello")); + /// assert_eq!(s.as_str(), "hello"); + /// ``` + #[inline] #[must_use] pub fn as_str(&self) -> &str { match &self.inner { @@ -61,6 +93,26 @@ impl Str<'_, A> { } } + /// Converts this string into an owned representation with an independent + /// lifetime. + /// + /// The returned [`Str`] is not tied to the original heap, so it can + /// outlive the heap the string was interned on. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// use alloc::alloc::Global; + /// + /// use hashql_mir::interpret::value::Str; + /// # use alloc::rc::Rc; + /// + /// let s = Str::from(Rc::::from("hello")); + /// let owned: Str<'static, Global> = s.into_owned_in(Global); + /// assert_eq!(owned.as_str(), "hello"); + /// ``` pub fn into_owned_in<'lifetime>(self, alloc: A) -> Str<'lifetime, A> { match self.inner { StrInner::Owned(value) => Str { @@ -73,7 +125,16 @@ impl Str<'_, A> { } } +impl<'heap, A: Allocator> From> for Str<'heap, A> { + fn from(value: Symbol<'heap>) -> Self { + Self { + inner: StrInner::Interned(value), + } + } +} + impl<'heap, A: Allocator> From> for Str<'heap, A> { + #[inline] fn from(value: String<'heap>) -> Self { Self { inner: StrInner::Interned(value.as_symbol()), @@ -82,6 +143,7 @@ impl<'heap, A: Allocator> From> for Str<'heap, A> { } impl<'heap, A: Allocator> From<&String<'heap>> for Str<'heap, A> { + #[inline] fn from(value: &String<'heap>) -> Self { Self { inner: StrInner::Interned(value.as_symbol()), @@ -90,6 +152,7 @@ impl<'heap, A: Allocator> From<&String<'heap>> for Str<'heap, A> { } impl From> for Str<'_, A> { + #[inline] fn from(value: Rc) -> Self { Self { inner: StrInner::Owned(value), @@ -125,3 +188,14 @@ impl Ord for Str<'_, A> { inner.cmp(&other.inner) } } + +impl<'heap, A: Allocator, B: Allocator> TryCloneIn for Str<'heap, A> { + type Cloned = Str<'heap, B>; + + fn try_clone_in(&self, allocator: B) -> Result { + match &self.inner { + StrInner::Owned(value) => Ok(Str::from(Rc::from_in(&**value, allocator))), + &StrInner::Interned(symbol) => Ok(Str::from(symbol)), + } + } +} diff --git a/libs/@local/hashql/mir/src/interpret/value/struct.rs b/libs/@local/hashql/mir/src/interpret/value/struct.rs index 4e265b00983..77b2c653f04 100644 --- a/libs/@local/hashql/mir/src/interpret/value/struct.rs +++ b/libs/@local/hashql/mir/src/interpret/value/struct.rs @@ -9,20 +9,54 @@ use core::{ ptr, }; -use hashql_core::{algorithms::co_sort, id::Id as _, intern::Interned, symbol::Symbol}; +use hashql_core::{ + algorithms::co_sort, + id::Id as _, + intern::{InternSet, Interned}, + symbol::Symbol, +}; use super::Value; -use crate::{body::place::FieldIndex, intern::Interner}; +use crate::body::place::FieldIndex; /// A named-field struct value. /// -/// Contains field names (interned symbols) and their corresponding values. -/// Field order is preserved and significant for comparison. +/// Contains field names (interned [`Symbol`]s) and their corresponding values. +/// Fields are sorted by symbol and accessed by name or positional index. +/// +/// [`StructBuilder`] is the ergonomic way to construct structs, as it +/// handles sorting and interning automatically. /// /// # Invariants /// /// - `fields.len() == values.len()` /// - Field names should be unique (not enforced at construction) +/// +/// # Examples +/// +/// ``` +/// # #![feature(allocator_api)] +/// # extern crate alloc; +/// use alloc::alloc::Global; +/// +/// use hashql_mir::interpret::value::{Struct, StructBuilder, Value}; +/// # use hashql_core::heap::Heap; +/// # use hashql_mir::intern::Interner; +/// +/// let heap = Heap::new(); +/// let interner = Interner::new(&heap); +/// +/// let mut builder = StructBuilder::<'_, Global, 2>::new(); +/// builder.push(heap.intern_symbol("x"), Value::Integer(1.into())); +/// builder.push(heap.intern_symbol("y"), Value::Integer(2.into())); +/// let s = builder.finish(&interner.symbols, Global); +/// +/// assert_eq!(s.len(), 2); +/// assert_eq!( +/// s.get_by_name(heap.intern_symbol("x")), +/// Some(&Value::Integer(1.into())), +/// ); +/// ``` #[derive(Debug, Clone)] pub struct Struct<'heap, A: Allocator> { fields: Interned<'heap, [Symbol<'heap>]>, @@ -32,8 +66,17 @@ pub struct Struct<'heap, A: Allocator> { impl<'heap, A: Allocator> Struct<'heap, A> { /// Creates a new struct without checking invariants. /// - /// The caller must ensure that `fields` and `values` have the same length. - pub fn new_unchecked( + /// # Safety + /// + /// The caller must ensure that: + /// - `fields` and `values` have the same length + /// - `fields` is sorted by [`Symbol`] ordering + #[expect( + unsafe_code, + reason = "callers must uphold the sorted-fields invariant" + )] + #[inline] + pub unsafe fn new_unchecked( fields: Interned<'heap, [Symbol<'heap>]>, values: Rc<[Value<'heap, A>], A>, ) -> Self { @@ -45,7 +88,11 @@ impl<'heap, A: Allocator> Struct<'heap, A> { /// Creates a new struct from field names and values. /// - /// Returns [`None`] if `fields` and `values` have different lengths. + /// Returns [`None`] if `fields` and `values` have different lengths, + /// or if `fields` is not sorted. + /// + /// Prefer [`StructBuilder`] for constructing structs, as it handles + /// sorting fields correctly. #[must_use] pub fn new( fields: Interned<'heap, [Symbol<'heap>]>, @@ -53,34 +100,145 @@ impl<'heap, A: Allocator> Struct<'heap, A> { ) -> Option { let values = values.into(); - (fields.len() == values.len()).then(|| Self::new_unchecked(fields, values)) + if fields.len() != values.len() || !fields.is_sorted() { + return None; + } + + // SAFETY: we just verified length equality and sort order. + #[expect(unsafe_code, reason = "invariants checked above")] + Some(unsafe { Self::new_unchecked(fields, values) }) } /// Returns the field names. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// # use hashql_core::heap::Heap; + /// # use hashql_mir::intern::Interner; + /// use hashql_mir::interpret::value::{StructBuilder, Value}; + /// + /// # let heap = Heap::new(); + /// # let interner = Interner::new(&heap); + /// let mut builder = StructBuilder::<'_, Global, 2>::new(); + /// builder.push(heap.intern_symbol("a"), Value::Unit); + /// builder.push(heap.intern_symbol("b"), Value::Unit); + /// let s = builder.finish(&interner.symbols, Global); + /// + /// let names: Vec<_> = s.fields().iter().map(|sym| sym.as_str()).collect(); + /// assert_eq!(names, vec!["a", "b"]); + /// ``` + #[inline] #[must_use] pub const fn fields(&self) -> &Interned<'heap, [Symbol<'heap>]> { &self.fields } /// Returns the field values. + /// + /// Values are in the same order as [`fields`](Self::fields). + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// # use hashql_core::heap::Heap; + /// # use hashql_mir::intern::Interner; + /// use hashql_mir::interpret::value::{StructBuilder, Value}; + /// + /// # let heap = Heap::new(); + /// # let interner = Interner::new(&heap); + /// let mut builder = StructBuilder::<'_, Global, 1>::new(); + /// builder.push(heap.intern_symbol("x"), Value::Integer(5.into())); + /// let s = builder.finish(&interner.symbols, Global); + /// + /// assert_eq!(s.values(), &[Value::Integer(5.into())]); + /// ``` + #[inline] #[must_use] pub fn values(&self) -> &[Value<'heap, A>] { &self.values } /// Returns the number of fields. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// # use hashql_core::heap::Heap; + /// # use hashql_mir::intern::Interner; + /// use hashql_mir::interpret::value::{StructBuilder, Value}; + /// + /// # let heap = Heap::new(); + /// # let interner = Interner::new(&heap); + /// let mut builder = StructBuilder::<'_, Global, 2>::new(); + /// builder.push(heap.intern_symbol("a"), Value::Unit); + /// builder.push(heap.intern_symbol("b"), Value::Unit); + /// let s = builder.finish(&interner.symbols, Global); + /// + /// assert_eq!(s.len(), 2); + /// ``` + #[inline] #[must_use] pub fn len(&self) -> usize { self.fields.len() } /// Returns `true` if the struct has no fields. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// # use hashql_core::heap::Heap; + /// # use hashql_mir::intern::Interner; + /// use hashql_mir::interpret::value::{StructBuilder, Value}; + /// + /// # let heap = Heap::new(); + /// # let interner = Interner::new(&heap); + /// let builder = StructBuilder::<'_, Global, 0>::new(); + /// let s = builder.finish(&interner.symbols, Global); + /// assert!(s.is_empty()); + /// ``` + #[inline] #[must_use] pub fn is_empty(&self) -> bool { self.fields.is_empty() } /// Returns the value for the given `field` name. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// # use hashql_core::heap::Heap; + /// # use hashql_mir::intern::Interner; + /// use hashql_mir::interpret::value::{StructBuilder, Value}; + /// + /// # let heap = Heap::new(); + /// # let interner = Interner::new(&heap); + /// let name = heap.intern_symbol("age"); + /// let mut builder = StructBuilder::<'_, Global, 1>::new(); + /// builder.push(name, Value::Integer(30.into())); + /// let s = builder.finish(&interner.symbols, Global); + /// + /// assert_eq!(s.get_by_name(name), Some(&Value::Integer(30.into()))); + /// assert_eq!(s.get_by_name(heap.intern_symbol("missing")), None); + /// ``` + #[inline] #[must_use] pub fn get_by_name(&self, field: Symbol<'heap>) -> Option<&Value<'heap, A>> { self.fields @@ -90,6 +248,27 @@ impl<'heap, A: Allocator> Struct<'heap, A> { } /// Returns a mutable reference to the value for the given `field` name. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// # use hashql_core::heap::Heap; + /// # use hashql_mir::intern::Interner; + /// use hashql_mir::interpret::value::{StructBuilder, Value}; + /// + /// # let heap = Heap::new(); + /// # let interner = Interner::new(&heap); + /// let name = heap.intern_symbol("count"); + /// let mut builder = StructBuilder::<'_, Global, 1>::new(); + /// builder.push(name, Value::Integer(0.into())); + /// let mut s = builder.finish(&interner.symbols, Global); + /// + /// *s.get_by_name_mut(name).unwrap() = Value::Integer(10.into()); + /// assert_eq!(s.get_by_name(name), Some(&Value::Integer(10.into()))); + /// ``` #[must_use] pub fn get_by_name_mut(&mut self, field: Symbol<'heap>) -> Option<&mut Value<'heap, A>> where @@ -103,12 +282,73 @@ impl<'heap, A: Allocator> Struct<'heap, A> { } /// Returns a reference to the value at the given field `index`. + /// + /// Fields are sorted by symbol, so the positional index depends on + /// the sort order of the field names. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// # use hashql_core::heap::Heap; + /// # use hashql_mir::intern::Interner; + /// use hashql_mir::{ + /// body::place::FieldIndex, + /// interpret::value::{StructBuilder, Value}, + /// }; + /// + /// # let heap = Heap::new(); + /// # let interner = Interner::new(&heap); + /// let mut builder = StructBuilder::<'_, Global, 2>::new(); + /// builder.push(heap.intern_symbol("b"), Value::Integer(2.into())); + /// builder.push(heap.intern_symbol("a"), Value::Integer(1.into())); + /// let s = builder.finish(&interner.symbols, Global); + /// + /// // After sorting: "a" is at index 0, "b" is at index 1 + /// assert_eq!( + /// s.get_by_index(FieldIndex::new(0)), + /// Some(&Value::Integer(1.into())) + /// ); + /// assert_eq!( + /// s.get_by_index(FieldIndex::new(1)), + /// Some(&Value::Integer(2.into())) + /// ); + /// ``` + #[inline] #[must_use] pub fn get_by_index(&self, index: FieldIndex) -> Option<&Value<'heap, A>> { self.values.get(index.as_usize()) } /// Returns a mutable reference to the value at the given field `index`. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// # use hashql_core::heap::Heap; + /// # use hashql_mir::intern::Interner; + /// use hashql_mir::{ + /// body::place::FieldIndex, + /// interpret::value::{StructBuilder, Value}, + /// }; + /// + /// # let heap = Heap::new(); + /// # let interner = Interner::new(&heap); + /// let mut builder = StructBuilder::<'_, Global, 1>::new(); + /// builder.push(heap.intern_symbol("x"), Value::Integer(0.into())); + /// let mut s = builder.finish(&interner.symbols, Global); + /// + /// *s.get_by_index_mut(FieldIndex::new(0)).unwrap() = Value::Integer(99.into()); + /// assert_eq!( + /// s.get_by_index(FieldIndex::new(0)), + /// Some(&Value::Integer(99.into())) + /// ); + /// ``` pub fn get_by_index_mut(&mut self, index: FieldIndex) -> Option<&mut Value<'heap, A>> where A: Clone, @@ -118,6 +358,27 @@ impl<'heap, A: Allocator> Struct<'heap, A> { } /// Returns an iterator over (field name, value) pairs. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// # use hashql_core::heap::Heap; + /// # use hashql_mir::intern::Interner; + /// use hashql_mir::interpret::value::{StructBuilder, Value}; + /// + /// # let heap = Heap::new(); + /// # let interner = Interner::new(&heap); + /// let mut builder = StructBuilder::<'_, Global, 2>::new(); + /// builder.push(heap.intern_symbol("x"), Value::Integer(1.into())); + /// builder.push(heap.intern_symbol("y"), Value::Integer(2.into())); + /// let s = builder.finish(&interner.symbols, Global); + /// + /// let names: Vec<_> = s.iter().map(|(name, _)| name.as_str().to_owned()).collect(); + /// assert_eq!(names, vec!["x", "y"]); + /// ``` pub fn iter(&self) -> StructIter<'_, 'heap, A> { StructIter { fields: self.fields.iter().copied(), @@ -222,6 +483,20 @@ pub struct StructBuilder<'heap, A: Allocator, const N: usize> { #[expect(unsafe_code)] impl<'heap, A: Allocator, const N: usize> StructBuilder<'heap, A, N> { /// Creates an empty builder with capacity for `N` fields. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::StructBuilder; + /// + /// let builder = StructBuilder::<'_, Global, 3>::new(); + /// assert!(builder.is_empty()); + /// assert_eq!(builder.len(), 0); + /// ``` + #[inline] #[must_use] pub const fn new() -> Self { Self { @@ -246,12 +521,14 @@ impl<'heap, A: Allocator, const N: usize> StructBuilder<'heap, A, N> { } /// Returns the number of fields pushed so far. + #[inline] #[must_use] pub const fn len(&self) -> usize { self.initialized } /// Returns `true` if no fields have been pushed. + #[inline] #[must_use] pub const fn is_empty(&self) -> bool { self.initialized == 0 @@ -278,6 +555,23 @@ impl<'heap, A: Allocator, const N: usize> StructBuilder<'heap, A, N> { /// /// - If the builder is full (`initialized == N`) /// - If `field` has already been pushed + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// # use hashql_core::heap::Heap; + /// use hashql_mir::interpret::value::{StructBuilder, Value}; + /// + /// # let heap = Heap::new(); + /// let mut builder = StructBuilder::<'_, Global, 2>::new(); + /// builder.push(heap.intern_symbol("x"), Value::Integer(1.into())); + /// builder.push(heap.intern_symbol("y"), Value::Integer(2.into())); + /// + /// assert_eq!(builder.len(), 2); + /// ``` pub fn push(&mut self, field: Symbol<'heap>, value: Value<'heap, A>) { assert_ne!(self.initialized, N, "struct is full"); assert!(!self.fields().contains(&field), "field already exists"); @@ -289,7 +583,42 @@ impl<'heap, A: Allocator, const N: usize> StructBuilder<'heap, A, N> { } /// Consumes the builder and produces a [`Struct`]. - pub fn finish(mut self, interner: &Interner<'heap>, alloc: A) -> Struct<'heap, A> { + /// + /// Fields are sorted by symbol before interning, so the resulting + /// struct's field order may differ from the push order. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// # use hashql_core::heap::Heap; + /// # use hashql_mir::intern::Interner; + /// use hashql_mir::interpret::value::{StructBuilder, Value}; + /// + /// let heap = Heap::new(); + /// let interner = Interner::new(&heap); + /// + /// let mut builder = StructBuilder::<'_, Global, 2>::new(); + /// builder.push(heap.intern_symbol("b"), Value::Integer(2.into())); + /// builder.push(heap.intern_symbol("a"), Value::Integer(1.into())); + /// let s = builder.finish(&interner.symbols, Global); + /// + /// // Fields are sorted: "a" comes first + /// let names: Vec<_> = s.fields().iter().map(|sym| sym.as_str()).collect(); + /// assert_eq!(names, vec!["a", "b"]); + /// // Values follow their fields + /// assert_eq!( + /// s.values(), + /// &[Value::Integer(1.into()), Value::Integer(2.into())] + /// ); + /// ``` + pub fn finish( + mut self, + symbols: &InternSet<'heap, [Symbol<'heap>]>, + alloc: A, + ) -> Struct<'heap, A> { // SAFETY: `fields[..initialized]` is fully initialized by invariant. let fields_mut = unsafe { self.fields[..self.initialized].assume_init_mut() }; // SAFETY: `values[..initialized]` is fully initialized by invariant. @@ -300,7 +629,7 @@ impl<'heap, A: Allocator, const N: usize> StructBuilder<'heap, A, N> { // initialization invariant is preserved even if it were to unwind. co_sort(fields_mut, values_mut); - let fields = interner.symbols.intern_slice(self.fields()); + let fields = symbols.intern_slice(self.fields()); // Allocate an uninitialized Rc slice for the values. // @@ -362,7 +691,10 @@ mod tests { use hashql_core::heap::Heap; use super::*; - use crate::interpret::value::{Int, Str, Value}; + use crate::{ + intern::Interner, + interpret::value::{Int, Str, Value}, + }; fn int(value: i128) -> Value<'static> { Value::Integer(Int::from(value)) @@ -385,7 +717,7 @@ mod tests { builder.push(sym_b, int(2)); builder.push(sym_a, int(1)); - let result = builder.finish(&interner, Global); + let result = builder.finish(&interner.symbols, Global); // Fields should be sorted: a before b. assert_eq!(result.fields().len(), 2); @@ -399,7 +731,7 @@ mod tests { let interner = Interner::new(&heap); let builder = StructBuilder::<'_, Global, 0>::new(); - let result = builder.finish(&interner, Global); + let result = builder.finish(&interner.symbols, Global); assert!(result.is_empty()); assert_eq!(result.len(), 0); @@ -415,7 +747,7 @@ mod tests { let mut builder = StructBuilder::<'_, Global, 1>::new(); builder.push(sym, int(42)); - let result = builder.finish(&interner, Global); + let result = builder.finish(&interner.symbols, Global); assert_eq!(result.len(), 1); assert_eq!(result.get_by_name(sym), Some(&int(42))); @@ -470,7 +802,7 @@ mod tests { builder.push(sym_b, string("beta")); // finish moves values into Rc; builder Drop must not re-drop them. - let result = builder.finish(&interner, Global); + let result = builder.finish(&interner.symbols, Global); assert_eq!(result.len(), 2); // Verify values survived the move. @@ -497,7 +829,7 @@ mod tests { builder.push(sym_a, string("alpha")); builder.push(sym_b, string("bravo")); - let result = builder.finish(&interner, Global); + let result = builder.finish(&interner.symbols, Global); // After sorting: a, b, c. let pairs: Vec<_> = result.iter().collect(); diff --git a/libs/@local/hashql/mir/src/interpret/value/tuple.rs b/libs/@local/hashql/mir/src/interpret/value/tuple.rs index 5605f1ac604..fc53a59cf0b 100644 --- a/libs/@local/hashql/mir/src/interpret/value/tuple.rs +++ b/libs/@local/hashql/mir/src/interpret/value/tuple.rs @@ -15,14 +15,28 @@ use crate::body::place::FieldIndex; /// A positional tuple value. /// -/// Contains an ordered sequence of values accessed by index. Unlike unit -/// (represented by [`Value::Unit`]), a tuple always contains at least one -/// element. +/// Contains an ordered sequence of values accessed by [`FieldIndex`]. Unlike +/// unit (represented by [`Value::Unit`]), a tuple always contains at least +/// one element. /// /// # Invariants /// /// - Must be non-empty (empty tuples should use [`Value::Unit`]) /// +/// # Examples +/// +/// ``` +/// use hashql_mir::interpret::value::{Tuple, Value}; +/// # extern crate alloc; +/// # use alloc::rc::Rc; +/// +/// let values: Rc<[Value]> = Rc::from(vec![Value::Integer(1.into()), Value::Integer(2.into())]); +/// let tuple = Tuple::new(values).expect("non-empty"); +/// +/// assert_eq!(tuple.len().get(), 2); +/// assert_eq!(tuple.values()[0], Value::Integer(1.into())); +/// ``` +/// /// [`Value::Unit`]: super::Value::Unit #[derive(Debug, Clone)] pub struct Tuple<'heap, A: Allocator> { @@ -33,6 +47,7 @@ impl<'heap, A: Allocator> Tuple<'heap, A> { /// Creates a new tuple without checking invariants. /// /// The caller must ensure that `values` is non-empty. + #[inline] pub fn new_unchecked(values: Rc<[Value<'heap, A>], A>) -> Self { debug_assert!(!values.is_empty(), "tuple is non-empty by construction"); @@ -42,20 +57,65 @@ impl<'heap, A: Allocator> Tuple<'heap, A> { /// Creates a new tuple from a slice of values. /// /// Returns [`None`] if `values` is empty. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::{Tuple, Value}; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let values: Rc<[Value]> = Rc::from(vec![Value::Unit, Value::Integer(1.into())]); + /// assert!(Tuple::new(values).is_some()); + /// + /// let empty: Rc<[Value]> = Rc::from(vec![]); + /// assert!(Tuple::new(empty).is_none()); + /// ``` #[must_use] pub fn new(values: impl Into], A>>) -> Option { let values = values.into(); - (!values.is_empty()).then_some(Self::new_unchecked(values)) + (!values.is_empty()).then(|| Self::new_unchecked(values)) } - /// Returns the tuple's values. + /// Returns the tuple's values as a slice. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::{Tuple, Value}; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let values: Rc<[Value]> = Rc::from(vec![Value::Integer(1.into()), Value::Unit]); + /// let tuple = Tuple::new(values).unwrap(); + /// + /// assert_eq!(tuple.values(), &[Value::Integer(1.into()), Value::Unit]); + /// ``` + #[inline] #[must_use] pub fn values(&self) -> &[Value<'heap, A>] { &self.values } /// Returns the number of elements. + /// + /// Always at least 1 (empty tuples are represented by [`Value::Unit`]). + /// + /// [`Value::Unit`]: super::Value::Unit + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::{Tuple, Value}; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let values: Rc<[Value]> = Rc::from(vec![Value::Unit, Value::Unit, Value::Unit]); + /// let tuple = Tuple::new(values).unwrap(); + /// assert_eq!(tuple.len().get(), 3); + /// ``` + #[inline] #[must_use] pub fn len(&self) -> NonZero { NonZero::new(self.values.len()) @@ -63,12 +123,57 @@ impl<'heap, A: Allocator> Tuple<'heap, A> { } /// Returns a reference to the element at the given `index`. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::{ + /// body::place::FieldIndex, + /// interpret::value::{Tuple, Value}, + /// }; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let values: Rc<[Value]> = Rc::from(vec![Value::Integer(10.into()), Value::Integer(20.into())]); + /// let tuple = Tuple::new(values).unwrap(); + /// + /// assert_eq!( + /// tuple.get(FieldIndex::new(0)), + /// Some(&Value::Integer(10.into())) + /// ); + /// assert_eq!( + /// tuple.get(FieldIndex::new(1)), + /// Some(&Value::Integer(20.into())) + /// ); + /// assert_eq!(tuple.get(FieldIndex::new(2)), None); + /// ``` + #[inline] #[must_use] pub fn get(&self, index: FieldIndex) -> Option<&Value<'heap, A>> { self.values.get(index.as_usize()) } /// Returns a mutable reference to the element at the given `index`. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::{ + /// body::place::FieldIndex, + /// interpret::value::{Tuple, Value}, + /// }; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let values: Rc<[Value]> = Rc::from(vec![Value::Integer(1.into()), Value::Integer(2.into())]); + /// let mut tuple = Tuple::new(values).unwrap(); + /// + /// *tuple.get_mut(FieldIndex::new(0)).unwrap() = Value::Integer(99.into()); + /// assert_eq!( + /// tuple.get(FieldIndex::new(0)), + /// Some(&Value::Integer(99.into())) + /// ); + /// ``` #[must_use] pub fn get_mut(&mut self, index: FieldIndex) -> Option<&mut Value<'heap, A>> where @@ -79,6 +184,26 @@ impl<'heap, A: Allocator> Tuple<'heap, A> { } /// Returns an iterator over the tuple's elements. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::{Tuple, Value}; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let values: Rc<[Value]> = Rc::from(vec![Value::Integer(1.into()), Value::Integer(2.into())]); + /// let tuple = Tuple::new(values).unwrap(); + /// + /// let sum: i128 = tuple + /// .iter() + /// .filter_map(|v| match v { + /// Value::Integer(i) => Some(i.as_int()), + /// _ => None, + /// }) + /// .sum(); + /// assert_eq!(sum, 3); + /// ``` pub fn iter(&self) -> core::slice::Iter<'_, Value<'heap, A>> { self.values.iter() } diff --git a/libs/@local/hashql/mir/src/pass/analysis/data_dependency/mod.rs b/libs/@local/hashql/mir/src/pass/analysis/data_dependency/mod.rs index 99c9d311537..eb98209a6d4 100644 --- a/libs/@local/hashql/mir/src/pass/analysis/data_dependency/mod.rs +++ b/libs/@local/hashql/mir/src/pass/analysis/data_dependency/mod.rs @@ -104,6 +104,7 @@ impl DataDependencyAnalysis<'_> { } impl Default for DataDependencyAnalysis<'_> { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/pass/execution/cost/mod.rs b/libs/@local/hashql/mir/src/pass/execution/cost/mod.rs index 470f53f4259..100bf4f2bc2 100644 --- a/libs/@local/hashql/mir/src/pass/execution/cost/mod.rs +++ b/libs/@local/hashql/mir/src/pass/execution/cost/mod.rs @@ -252,6 +252,7 @@ impl From for ApproxCost { } impl From for ApproxCost { + #[inline] fn from(value: InformationUnit) -> Self { #[expect(clippy::cast_precision_loss)] Self(value.as_u32() as f32) diff --git a/libs/@local/hashql/mir/src/pass/execution/island/graph/mod.rs b/libs/@local/hashql/mir/src/pass/execution/island/graph/mod.rs index c3c4f6a54dc..97170c90c46 100644 --- a/libs/@local/hashql/mir/src/pass/execution/island/graph/mod.rs +++ b/libs/@local/hashql/mir/src/pass/execution/island/graph/mod.rs @@ -23,6 +23,7 @@ pub(crate) mod tests; use alloc::alloc::Global; use core::{ alloc::Allocator, + fmt, ops::{Index, IndexMut}, }; @@ -287,6 +288,16 @@ impl IslandGraph { } } +impl fmt::Debug for IslandGraph { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("IslandGraph") + .field("vertex", &self.vertex) + .field("inner", &self.inner) + .field("lookup", &self.lookup) + .finish() + } +} + impl DirectedGraph for IslandGraph { type Edge<'this> = &'this Edge diff --git a/libs/@local/hashql/mir/src/pass/execution/island/mod.rs b/libs/@local/hashql/mir/src/pass/execution/island/mod.rs index fb26e47b8c2..9d9f0446afc 100644 --- a/libs/@local/hashql/mir/src/pass/execution/island/mod.rs +++ b/libs/@local/hashql/mir/src/pass/execution/island/mod.rs @@ -84,6 +84,7 @@ impl Island { self.members.is_empty() } + #[inline] #[must_use] pub const fn traversals(&self) -> TraversalPathBitSet { self.traversals @@ -114,6 +115,7 @@ impl IslandPlacement { } impl Default for IslandPlacement { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/pass/execution/mod.rs b/libs/@local/hashql/mir/src/pass/execution/mod.rs index ce0bddb23ef..81408178561 100644 --- a/libs/@local/hashql/mir/src/pass/execution/mod.rs +++ b/libs/@local/hashql/mir/src/pass/execution/mod.rs @@ -19,7 +19,7 @@ mod terminator_placement; pub mod traversal; mod vertex; -use core::{alloc::Allocator, assert_matches}; +use core::{alloc::Allocator, assert_matches, fmt}; use hashql_core::heap::{BumpAllocator, Heap}; @@ -57,6 +57,15 @@ pub struct ExecutionAnalysisResidual { pub islands: IslandGraph, } +impl core::fmt::Debug for ExecutionAnalysisResidual { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("ExecutionAnalysisResidual") + .field("assignment", &self.assignment) + .field("islands", &self.islands) + .finish() + } +} + pub struct ExecutionAnalysis<'ctx, 'heap, S: Allocator> { pub footprints: &'ctx DefIdSlice>, pub scratch: S, diff --git a/libs/@local/hashql/mir/src/pass/execution/splitting/mod.rs b/libs/@local/hashql/mir/src/pass/execution/splitting/mod.rs index c3fa52702a9..0cc69483b44 100644 --- a/libs/@local/hashql/mir/src/pass/execution/splitting/mod.rs +++ b/libs/@local/hashql/mir/src/pass/execution/splitting/mod.rs @@ -401,6 +401,7 @@ impl BasicBlockSplitting { } impl Default for BasicBlockSplitting { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/pass/execution/terminator_placement/mod.rs b/libs/@local/hashql/mir/src/pass/execution/terminator_placement/mod.rs index 36b83bd728c..bb2c3e6ce98 100644 --- a/libs/@local/hashql/mir/src/pass/execution/terminator_placement/mod.rs +++ b/libs/@local/hashql/mir/src/pass/execution/terminator_placement/mod.rs @@ -201,6 +201,7 @@ impl TransMatrix { } impl Default for TransMatrix { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/pass/execution/traversal/mod.rs b/libs/@local/hashql/mir/src/pass/execution/traversal/mod.rs index 37b5f21404f..76cd0be7d1d 100644 --- a/libs/@local/hashql/mir/src/pass/execution/traversal/mod.rs +++ b/libs/@local/hashql/mir/src/pass/execution/traversal/mod.rs @@ -309,6 +309,7 @@ impl TraversalPathBitMap { } impl From for TraversalPathBitMap { + #[inline] fn from(value: TraversalPathBitSet) -> Self { let mut this = TraversalMapLattice.bottom(); this[value.vertex()] = value; diff --git a/libs/@local/hashql/mir/src/pass/mod.rs b/libs/@local/hashql/mir/src/pass/mod.rs index a71f27bd006..2fd6b3292dc 100644 --- a/libs/@local/hashql/mir/src/pass/mod.rs +++ b/libs/@local/hashql/mir/src/pass/mod.rs @@ -19,15 +19,26 @@ use core::{ alloc::Allocator, + mem, ops::{BitOr, BitOrAssign}, }; -use hashql_core::heap::BumpAllocator; +use hashql_core::{ + heap::{BumpAllocator, Heap, ResetAllocator as _, Scratch}, + span::SpanId, +}; +use hashql_diagnostics::Status; +use self::{ + analysis::SizeEstimationAnalysis, + execution::{ExecutionAnalysis, ExecutionAnalysisResidual}, + transform::{Inline, InlineConfig, PostInline, PreInline}, +}; use crate::{ body::Body, context::MirContext, def::{DefId, DefIdSlice, DefIdVec}, + error::MirDiagnosticCategory, }; pub mod analysis; @@ -159,6 +170,7 @@ impl BitOrAssign for Changed { } impl From for Changed { + #[inline] fn from(value: bool) -> Self { if value { Self::Yes } else { Self::No } } @@ -467,6 +479,104 @@ pub trait GlobalAnalysisPass<'env, 'heap> { } } +/// Configuration for the MIR lowering pipeline. +#[derive(Debug, Clone, Default)] +pub struct LowerConfig { + pub inline: InlineConfig, +} + +/// Runs the MIR lowering pipeline over all bodies. +/// +/// Produces optimized, fully inlined MIR ready for execution placement. The +/// pipeline runs three phases: +/// +/// 1. **Pre-inline canonicalization**: simplifies each body in isolation (constant folding, dead +/// code elimination, CFG cleanup) so that the inliner sees clean callees and makes better +/// decisions. +/// 2. **Inlining**: inter-procedural pass that substitutes callees into call sites based on cost +/// heuristics, with aggressive inlining for filter bodies. +/// 3. **Post-inline canonicalization**: cleans up redundancy introduced by inlining (duplicate +/// code, dead stores, unreachable blocks). +/// +/// # Errors +/// +/// Returns `Err` if any pass emits a critical diagnostic (`Error` or `Bug` +/// severity). This indicates a compiler invariant violation, since transform +/// passes operate on well-typed, well-formed MIR. +pub fn lower<'heap>( + context: &mut MirContext<'_, 'heap>, + scratch: &mut Scratch, + bodies: &mut DefIdSlice>, + config: &LowerConfig, +) -> Status<(), MirDiagnosticCategory, SpanId> { + scratch.reset(); + + let mut state = GlobalTransformState::new_in(&*bodies, context.heap); + + let mut pass = PreInline::new_in(&mut *scratch); + let _: Changed = pass.run(context, &mut state, bodies); + scratch.reset(); + + let mut pass = Inline::new_in(config.inline, &mut *scratch); + let _: Changed = pass.run(context, &mut state, bodies); + scratch.reset(); + + let mut pass = PostInline::new_in(&mut *scratch); + let _: Changed = pass.run(context, &mut state, bodies); + scratch.reset(); + + let issues = mem::take(&mut context.diagnostics); + issues.into_status(()) +} + +/// Determines which execution backend each basic block runs on. +/// +/// Only [`GraphReadFilter`] bodies are analyzed; all other bodies receive +/// `None` in the result. The pipeline runs two phases: +/// +/// 1. **Size estimation**: computes per-body footprints used to estimate data transfer costs at +/// island boundaries. +/// 2. **Execution analysis**: for each filter body, computes per-target statement and terminator +/// costs, solves the placement problem, fuses adjacent same-backend blocks, and builds the +/// island graph. +/// +/// # Errors +/// +/// Returns `Err` if the placement solver emits a critical diagnostic. The +/// interpreter is a universal execution target, so a valid assignment +/// should always exist; a solver failure indicates a bug in domain pruning +/// or target construction. +/// +/// [`GraphReadFilter`]: crate::body::Source::GraphReadFilter +pub fn place<'heap>( + context: &mut MirContext<'_, 'heap>, + scratch: &mut Scratch, + bodies: &mut DefIdSlice>, +) -> Status< + DefIdVec>, &'heap Heap>, + MirDiagnosticCategory, + SpanId, +> { + scratch.reset(); + + let heap = context.heap; + + let mut pass = SizeEstimationAnalysis::new_in(&*scratch); + pass.run(context, bodies); + let footprints = pass.finish(); + scratch.reset(); + + let pass = ExecutionAnalysis { + footprints: &footprints, + scratch: &mut *scratch, + }; + let residual = pass.run_all_in(context, bodies, heap); + scratch.reset(); + + let issues = mem::take(&mut context.diagnostics); + issues.into_status(residual) +} + #[cfg(test)] mod tests { use super::Changed; diff --git a/libs/@local/hashql/mir/src/pass/transform/canonicalization.rs b/libs/@local/hashql/mir/src/pass/transform/canonicalization.rs index e218ee2544a..221bb4ab101 100644 --- a/libs/@local/hashql/mir/src/pass/transform/canonicalization.rs +++ b/libs/@local/hashql/mir/src/pass/transform/canonicalization.rs @@ -26,6 +26,7 @@ pub struct CanonicalizationConfig { } impl Default for CanonicalizationConfig { + #[inline] fn default() -> Self { Self { max_iterations: 16 } } diff --git a/libs/@local/hashql/mir/src/pass/transform/cfg_simplify/mod.rs b/libs/@local/hashql/mir/src/pass/transform/cfg_simplify/mod.rs index a8d33328cbb..aff1a68addb 100644 --- a/libs/@local/hashql/mir/src/pass/transform/cfg_simplify/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/cfg_simplify/mod.rs @@ -483,6 +483,7 @@ impl CfgSimplify { } impl Default for CfgSimplify { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/pass/transform/copy_propagation/mod.rs b/libs/@local/hashql/mir/src/pass/transform/copy_propagation/mod.rs index 55e866ff43f..72c793a2adf 100644 --- a/libs/@local/hashql/mir/src/pass/transform/copy_propagation/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/copy_propagation/mod.rs @@ -213,6 +213,7 @@ impl CopyPropagation { } impl Default for CopyPropagation { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/pass/transform/dse/mod.rs b/libs/@local/hashql/mir/src/pass/transform/dse/mod.rs index 8fdbeca6ee7..787d56e6dae 100644 --- a/libs/@local/hashql/mir/src/pass/transform/dse/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/dse/mod.rs @@ -132,6 +132,7 @@ impl DeadStoreElimination { } impl Default for DeadStoreElimination { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/pass/transform/forward_substitution.rs b/libs/@local/hashql/mir/src/pass/transform/forward_substitution.rs index 145b0c6c70e..ef4a2c3c68b 100644 --- a/libs/@local/hashql/mir/src/pass/transform/forward_substitution.rs +++ b/libs/@local/hashql/mir/src/pass/transform/forward_substitution.rs @@ -157,6 +157,7 @@ impl<'env, 'heap, A: Allocator> TransformPass<'env, 'heap> for ForwardSubstituti } impl Default for ForwardSubstitution { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs index 34ee56a1333..18e1f148ed6 100644 --- a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs @@ -158,6 +158,7 @@ impl InstSimplify { } impl Default for InstSimplify { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/pass/transform/ssa_repair/mod.rs b/libs/@local/hashql/mir/src/pass/transform/ssa_repair/mod.rs index 46eabd3cbee..49ed23dd800 100644 --- a/libs/@local/hashql/mir/src/pass/transform/ssa_repair/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/ssa_repair/mod.rs @@ -156,6 +156,7 @@ impl SsaRepair { } impl Default for SsaRepair { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/reify/atom.rs b/libs/@local/hashql/mir/src/reify/atom.rs index 394b9b793b0..ca26410b975 100644 --- a/libs/@local/hashql/mir/src/reify/atom.rs +++ b/libs/@local/hashql/mir/src/reify/atom.rs @@ -1,3 +1,5 @@ +use core::alloc::Allocator; + use hashql_core::{id::Id as _, r#type::kind::TypeKind}; use hashql_hir::node::{ Node, @@ -25,7 +27,7 @@ use crate::{ }, }; -impl<'heap> Reifier<'_, '_, '_, '_, 'heap> { +impl<'heap, A: Allocator, S: Allocator> Reifier<'_, '_, '_, '_, 'heap, A, S> { fn local(&mut self, node: Node<'heap>) -> Local { let NodeKind::Variable(Variable::Local(local)) = node.kind else { self.state diff --git a/libs/@local/hashql/mir/src/reify/current.rs b/libs/@local/hashql/mir/src/reify/current.rs index f033cc15721..4ea1fdd1870 100644 --- a/libs/@local/hashql/mir/src/reify/current.rs +++ b/libs/@local/hashql/mir/src/reify/current.rs @@ -48,6 +48,7 @@ impl ForwardRef { #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub(crate) struct EntryBlock(pub BasicBlockId); impl From for BasicBlockId { + #[inline] fn from(entry: EntryBlock) -> Self { entry.0 } @@ -56,6 +57,7 @@ impl From for BasicBlockId { #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub(crate) struct ExitBlock(pub BasicBlockId); impl From for BasicBlockId { + #[inline] fn from(exit: ExitBlock) -> Self { exit.0 } diff --git a/libs/@local/hashql/mir/src/reify/error.rs b/libs/@local/hashql/mir/src/reify/error.rs index e4ad649cccc..ffdbf04023e 100644 --- a/libs/@local/hashql/mir/src/reify/error.rs +++ b/libs/@local/hashql/mir/src/reify/error.rs @@ -8,9 +8,8 @@ use hashql_diagnostics::{ severity::{Critical, Severity}, }; -pub(crate) type ReifyDiagnostic = Diagnostic; -pub(crate) type ReifyDiagnosticIssues = - DiagnosticIssues; +pub type ReifyDiagnostic = Diagnostic; +pub type ReifyDiagnosticIssues = DiagnosticIssues; // Terminal categories for user-facing errors const UNSUPPORTED_FEATURE: TerminalDiagnosticCategory = TerminalDiagnosticCategory { diff --git a/libs/@local/hashql/mir/src/reify/mod.rs b/libs/@local/hashql/mir/src/reify/mod.rs index e72e49d3116..19180f265fb 100644 --- a/libs/@local/hashql/mir/src/reify/mod.rs +++ b/libs/@local/hashql/mir/src/reify/mod.rs @@ -6,7 +6,8 @@ mod terminator; mod transform; mod types; -use core::debug_assert_matches; +use alloc::alloc::Global; +use core::{alloc::Allocator, debug_assert_matches}; use hashql_core::{ collections::{ @@ -36,11 +37,11 @@ use hashql_hir::{ }, }; +pub use self::error::{ReifyDiagnostic, ReifyDiagnosticCategory, ReifyDiagnosticIssues}; use self::{ current::CurrentBlock, error::{ - ReifyDiagnosticCategory, ReifyDiagnosticIssues, expected_anf_thunk, expected_anf_variable, - external_modules_unsupported, local_not_thunk, + expected_anf_thunk, expected_anf_variable, external_modules_unsupported, local_not_thunk, }, types::unwrap_closure_type, }; @@ -65,13 +66,15 @@ use crate::{ /// /// This structure contains the essential components needed to transform HIR(ANF) into MIR, /// including symbol tables, type information, and memory management. -pub struct ReifyContext<'mir, 'hir, 'env, 'heap> { +pub struct ReifyContext<'mir, 'hir, 'env, 'heap, A: Allocator = Global, S: Allocator = Global> { /// Mutable reference to the collection of MIR bodies being generated. - pub bodies: &'mir mut DefIdVec>, + pub bodies: &'mir mut DefIdVec, A>, /// MIR context. pub mir: &'mir mut MirContext<'env, 'heap>, /// HIR context containing the source nodes and variable mappings. pub hir: &'hir HirContext<'hir, 'heap>, + /// Scratch allocator for temporary memory usage during reification. + pub scratch: S, } /// Tracks the mapping between variable IDs and their corresponding thunk definition IDs. @@ -83,12 +86,12 @@ pub struct ReifyContext<'mir, 'hir, 'env, 'heap> { /// /// Thunks are sparse and limited to the first few IDs since nested thunks are not allowed. /// Using a vector here is memory-efficient given this constraint. -pub struct Thunks { - defs: VarIdVec>, +pub struct Thunks { + defs: VarIdVec, S>, set: MixedBitSet, } -impl Thunks { +impl Thunks { fn insert(&mut self, var: VarId, def: DefId) { self.defs.insert(var, def); self.set.insert(var); @@ -99,9 +102,9 @@ impl Thunks { /// /// This structure maintains global state needed throughout reification, including /// thunk mappings, constructor definitions, and memory pools for efficient allocation. -struct CrossCompileState<'heap> { +struct CrossCompileState<'heap, S: Allocator> { /// Mapping of variable IDs to their thunk definitions. - thunks: Thunks, + thunks: Thunks, /// Collection of diagnostics encountered during reification. diagnostics: ReifyDiagnosticIssues, @@ -117,12 +120,12 @@ struct CrossCompileState<'heap> { /// Each `Reifier` instance is responsible for converting a single function/thunk/closure /// from HIR to MIR. It maintains its own local state for basic blocks, variable mappings, /// and local allocation while sharing global state through the context and cross-compile state. -struct Reifier<'ctx, 'mir, 'hir, 'env, 'heap> { +struct Reifier<'ctx, 'mir, 'hir, 'env, 'heap, A: Allocator, S: Allocator> { /// Reference to the global reification context. - context: &'ctx mut ReifyContext<'mir, 'hir, 'env, 'heap>, + context: &'ctx mut ReifyContext<'mir, 'hir, 'env, 'heap, A, S>, /// Reference to the shared cross-compilation state. - state: &'ctx mut CrossCompileState<'heap>, + state: &'ctx mut CrossCompileState<'heap, S>, /// Basic blocks being constructed for the current function body. blocks: BasicBlockVec, &'heap Heap>, @@ -133,10 +136,12 @@ struct Reifier<'ctx, 'mir, 'hir, 'env, 'heap> { local_decls: LocalVec, &'heap Heap>, } -impl<'ctx, 'mir, 'hir, 'env, 'heap> Reifier<'ctx, 'mir, 'hir, 'env, 'heap> { +impl<'ctx, 'mir, 'hir, 'env, 'heap, A: Allocator, S: Allocator> + Reifier<'ctx, 'mir, 'hir, 'env, 'heap, A, S> +{ const fn new( - context: &'ctx mut ReifyContext<'mir, 'hir, 'env, 'heap>, - state: &'ctx mut CrossCompileState<'heap>, + context: &'ctx mut ReifyContext<'mir, 'hir, 'env, 'heap, A, S>, + state: &'ctx mut CrossCompileState<'heap, S>, ) -> Self { let blocks = BasicBlockVec::new_in(context.mir.heap); let local_decls = LocalVec::new_in(context.mir.heap); @@ -473,9 +478,9 @@ impl<'ctx, 'mir, 'hir, 'env, 'heap> Reifier<'ctx, 'mir, 'hir, 'env, 'heap> { /// /// See [BE-67](https://linear.app/hash/issue/BE-67/hashql-implement-modules) for /// planned multi-module architecture. -pub fn from_hir<'heap>( +pub fn from_hir<'heap, A: Allocator, S: Allocator + Clone>( node: Node<'heap>, - context: &mut ReifyContext<'_, '_, '_, 'heap>, + context: &mut ReifyContext<'_, '_, '_, 'heap, A, S>, ) -> Status { // The node is already in HIR(ANF) - each node will be a thunk. let NodeKind::Let(Let { bindings, body }) = node.kind else { @@ -496,7 +501,7 @@ pub fn from_hir<'heap>( }; let thunks = Thunks { - defs: VarIdVec::new(), + defs: VarIdVec::new_in(context.scratch.clone()), set: MixedBitSet::new_empty(context.hir.counter.var.size()), }; let mut state = CrossCompileState { diff --git a/libs/@local/hashql/mir/src/reify/rvalue.rs b/libs/@local/hashql/mir/src/reify/rvalue.rs index b29a0442b80..4cb798feab5 100644 --- a/libs/@local/hashql/mir/src/reify/rvalue.rs +++ b/libs/@local/hashql/mir/src/reify/rvalue.rs @@ -1,3 +1,5 @@ +use core::alloc::Allocator; + use hashql_core::{ id::{Id as _, IdVec}, symbol::sym, @@ -32,7 +34,7 @@ use crate::{ interpret::value::{Int, TryFromPrimitiveError}, }; -impl<'mir, 'heap> Reifier<'_, 'mir, '_, '_, 'heap> { +impl<'mir, 'heap, A: Allocator, S: Allocator> Reifier<'_, 'mir, '_, '_, 'heap, A, S> { fn rvalue_data(&mut self, data: Data<'heap>) -> RValue<'heap> { match data { Data::Primitive(primitive) => { @@ -53,6 +55,8 @@ impl<'mir, 'heap> Reifier<'_, 'mir, '_, '_, 'heap> { operands.push(self.operand(field.value)); } + debug_assert!(field_names.is_sorted()); + RValue::Aggregate(Aggregate { kind: AggregateKind::Struct { fields: self.context.mir.interner.symbols.intern_slice(&field_names), @@ -110,15 +114,16 @@ impl<'mir, 'heap> Reifier<'_, 'mir, '_, '_, 'heap> { RValue::Load(Operand::Constant(Constant::Unit)) } TypeOperation::Constructor(ctor @ TypeConstructor { name }) => { - if let Some(&ptr) = self.state.ctor.get(&name) { - return RValue::Load(Operand::Constant(Constant::FnPtr(ptr))); - } - - let compiler = Reifier::new(self.context, self.state); - let ptr = compiler.lower_ctor(hir, ctor); - self.state.ctor.insert(name, ptr); + let def = if let Some(&ptr) = self.state.ctor.get(&name) { + ptr + } else { + let compiler = Reifier::new(self.context, self.state); + let ptr = compiler.lower_ctor(hir, ctor); + self.state.ctor.insert(name, ptr); + ptr + }; - let ptr = Operand::Constant(Constant::FnPtr(ptr)); + let ptr = Operand::Constant(Constant::FnPtr(def)); let env = Operand::Constant(Constant::Unit); let mut operands = IdVec::with_capacity_in(2, self.context.mir.heap); operands.push(ptr); diff --git a/libs/@local/hashql/mir/src/reify/terminator.rs b/libs/@local/hashql/mir/src/reify/terminator.rs index cfba67fb9d3..437bdbe68ed 100644 --- a/libs/@local/hashql/mir/src/reify/terminator.rs +++ b/libs/@local/hashql/mir/src/reify/terminator.rs @@ -1,3 +1,5 @@ +use core::alloc::Allocator; + use hashql_core::{heap, id::Id as _, span::SpanId}; use hashql_hir::node::{ branch, @@ -23,7 +25,7 @@ use crate::{ def::DefId, }; -impl<'mir, 'heap> Reifier<'_, 'mir, '_, '_, 'heap> { +impl<'mir, 'heap, A: Allocator, S: Allocator> Reifier<'_, 'mir, '_, '_, 'heap, A, S> { fn terminator_graph_read_head( &mut self, head: graph::GraphReadHead<'heap>, diff --git a/libs/@local/hashql/mir/src/reify/transform.rs b/libs/@local/hashql/mir/src/reify/transform.rs index a3249dca65a..5bbc4bfca06 100644 --- a/libs/@local/hashql/mir/src/reify/transform.rs +++ b/libs/@local/hashql/mir/src/reify/transform.rs @@ -1,3 +1,5 @@ +use core::alloc::Allocator; + use hashql_core::{ collections::TinyVec, id::{IdVec, bit_vec::BitRelations as _}, @@ -28,7 +30,7 @@ use crate::{ def::DefId, }; -impl<'mir, 'heap> Reifier<'_, 'mir, '_, '_, 'heap> { +impl<'mir, 'heap, A: Allocator, S: Allocator> Reifier<'_, 'mir, '_, '_, 'heap, A, S> { pub(super) fn transform_closure( &mut self, block: &mut CurrentBlock<'mir, 'heap>, diff --git a/libs/@local/hashql/mir/tests/ui/interpret/.spec.toml b/libs/@local/hashql/mir/tests/ui/interpret/.spec.toml new file mode 100644 index 00000000000..acab5275af2 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/interpret/.spec.toml @@ -0,0 +1 @@ +suite = "mir/interpret" diff --git a/libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.aux.mir b/libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.aux.mir new file mode 100644 index 00000000000..3b899d54d7f --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.aux.mir @@ -0,0 +1,409 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +fn {ctor#::main::A:0}(%0: ()) -> ::main::A:0 { + let %1: ::main::A:0 + + bb0(): { + %1 = opaque(::main::A:0, ()) + + return %1 + } +} + +thunk {thunk#2}() -> () -> ::main::A:0 { + let %0: () -> ::main::A:0 + + bb0(): { + %0 = closure(({ctor#::main::A:0} as FnPtr), ()) + + return %0 + } +} + +thunk {thunk#3}() -> ::main::A:0 { + let %0: () -> ::main::A:0 + let %1: ::main::A:0 + + bb0(): { + %0 = apply ({thunk#2} as FnPtr) + %1 = apply %0.0 %0.1 + + return %1 + } +} + +thunk {thunk#4}() -> () -> ::main::A:0 { + let %0: () -> ::main::A:0 + + bb0(): { + %0 = closure(({ctor#::main::A:0} as FnPtr), ()) + + return %0 + } +} + +thunk {thunk#5}() -> ::main::A:0 { + let %0: () -> ::main::A:0 + let %1: ::main::A:0 + + bb0(): { + %0 = apply ({thunk#4} as FnPtr) + %1 = apply %0.0 %0.1 + + return %1 + } +} + +thunk {thunk#6}() -> (x: ::main::A:0, y: ::main::A:0) { + let %0: ::main::A:0 + let %1: ::main::A:0 + let %2: (x: ::main::A:0, y: ::main::A:0) + + bb0(): { + %0 = apply ({thunk#3} as FnPtr) + %1 = apply ({thunk#5} as FnPtr) + %2 = (x: %0, y: %1) + + return %2 + } +} + +fn {ctor#::main::Outer:0}(%0: (), %1: (x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 { + let %2: ::main::Outer:0 + + bb0(): { + %2 = opaque(::main::Outer:0, %1) + + return %2 + } +} + +thunk {thunk#7}() -> ((x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 { + let %0: ((x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 + + bb0(): { + %0 = closure(({ctor#::main::Outer:0} as FnPtr), ()) + + return %0 + } +} + +*thunk {thunk#8}() -> ::main::Outer:0 { + let %0: (x: ::main::A:0, y: ::main::A:0) + let %1: ((x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 + let %2: ::main::Outer:0 + + bb0(): { + %0 = apply ({thunk#6} as FnPtr) + %1 = apply ({thunk#7} as FnPtr) + %2 = apply %1.0 %1.1 %0 + + return %2 + } +} + +════ Pre-inlining MIR ══════════════════════════════════════════════════════════ + +fn {ctor#::main::A:0}(%0: ()) -> ::main::A:0 { + let %1: ::main::A:0 + + bb0(): { + %1 = opaque(::main::A:0, ()) + + return %1 + } +} + +thunk {thunk#2}() -> () -> ::main::A:0 { + let %0: () -> ::main::A:0 + + bb0(): { + %0 = closure(({ctor#::main::A:0} as FnPtr), ()) + + return %0 + } +} + +thunk {thunk#3}() -> ::main::A:0 { + let %0: ::main::A:0 + + bb0(): { + %0 = opaque(::main::A:0, ()) + + return %0 + } +} + +thunk {thunk#4}() -> () -> ::main::A:0 { + let %0: () -> ::main::A:0 + + bb0(): { + %0 = closure(({ctor#::main::A:0} as FnPtr), ()) + + return %0 + } +} + +thunk {thunk#5}() -> ::main::A:0 { + let %0: ::main::A:0 + + bb0(): { + %0 = opaque(::main::A:0, ()) + + return %0 + } +} + +thunk {thunk#6}() -> (x: ::main::A:0, y: ::main::A:0) { + let %0: (x: ::main::A:0, y: ::main::A:0) + let %1: ::main::A:0 + let %2: ::main::A:0 + + bb0(): { + %1 = opaque(::main::A:0, ()) + %2 = opaque(::main::A:0, ()) + %0 = (x: %1, y: %2) + + return %0 + } +} + +fn {ctor#::main::Outer:0}(%0: (), %1: (x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 { + let %2: ::main::Outer:0 + + bb0(): { + %2 = opaque(::main::Outer:0, %1) + + return %2 + } +} + +thunk {thunk#7}() -> ((x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 { + let %0: ((x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 + + bb0(): { + %0 = closure(({ctor#::main::Outer:0} as FnPtr), ()) + + return %0 + } +} + +*thunk {thunk#8}() -> ::main::Outer:0 { + let %0: (x: ::main::A:0, y: ::main::A:0) + let %1: ::main::A:0 + let %2: ::main::A:0 + let %3: ::main::Outer:0 + + bb0(): { + %1 = opaque(::main::A:0, ()) + %2 = opaque(::main::A:0, ()) + %0 = (x: %1, y: %2) + %3 = opaque(::main::Outer:0, %0) + + return %3 + } +} + +════ Inlined MIR ═══════════════════════════════════════════════════════════════ + +fn {ctor#::main::A:0}(%0: ()) -> ::main::A:0 { + let %1: ::main::A:0 + + bb0(): { + %1 = opaque(::main::A:0, ()) + + return %1 + } +} + +thunk {thunk#2}() -> () -> ::main::A:0 { + let %0: () -> ::main::A:0 + + bb0(): { + %0 = closure(({ctor#::main::A:0} as FnPtr), ()) + + return %0 + } +} + +thunk {thunk#3}() -> ::main::A:0 { + let %0: ::main::A:0 + + bb0(): { + %0 = opaque(::main::A:0, ()) + + return %0 + } +} + +thunk {thunk#4}() -> () -> ::main::A:0 { + let %0: () -> ::main::A:0 + + bb0(): { + %0 = closure(({ctor#::main::A:0} as FnPtr), ()) + + return %0 + } +} + +thunk {thunk#5}() -> ::main::A:0 { + let %0: ::main::A:0 + + bb0(): { + %0 = opaque(::main::A:0, ()) + + return %0 + } +} + +thunk {thunk#6}() -> (x: ::main::A:0, y: ::main::A:0) { + let %0: (x: ::main::A:0, y: ::main::A:0) + let %1: ::main::A:0 + let %2: ::main::A:0 + + bb0(): { + %1 = opaque(::main::A:0, ()) + %2 = opaque(::main::A:0, ()) + %0 = (x: %1, y: %2) + + return %0 + } +} + +fn {ctor#::main::Outer:0}(%0: (), %1: (x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 { + let %2: ::main::Outer:0 + + bb0(): { + %2 = opaque(::main::Outer:0, %1) + + return %2 + } +} + +thunk {thunk#7}() -> ((x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 { + let %0: ((x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 + + bb0(): { + %0 = closure(({ctor#::main::Outer:0} as FnPtr), ()) + + return %0 + } +} + +*thunk {thunk#8}() -> ::main::Outer:0 { + let %0: (x: ::main::A:0, y: ::main::A:0) + let %1: ::main::A:0 + let %2: ::main::A:0 + let %3: ::main::Outer:0 + + bb0(): { + %1 = opaque(::main::A:0, ()) + %2 = opaque(::main::A:0, ()) + %0 = (x: %1, y: %2) + %3 = opaque(::main::Outer:0, %0) + + return %3 + } +} + +════ Post Inline MIR ═══════════════════════════════════════════════════════════ + +fn {ctor#::main::A:0}(%0: ()) -> ::main::A:0 { + let %1: ::main::A:0 + + bb0(): { + %1 = opaque(::main::A:0, ()) + + return %1 + } +} + +thunk {thunk#2}() -> () -> ::main::A:0 { + let %0: () -> ::main::A:0 + + bb0(): { + %0 = closure(({ctor#::main::A:0} as FnPtr), ()) + + return %0 + } +} + +thunk {thunk#3}() -> ::main::A:0 { + let %0: ::main::A:0 + + bb0(): { + %0 = opaque(::main::A:0, ()) + + return %0 + } +} + +thunk {thunk#4}() -> () -> ::main::A:0 { + let %0: () -> ::main::A:0 + + bb0(): { + %0 = closure(({ctor#::main::A:0} as FnPtr), ()) + + return %0 + } +} + +thunk {thunk#5}() -> ::main::A:0 { + let %0: ::main::A:0 + + bb0(): { + %0 = opaque(::main::A:0, ()) + + return %0 + } +} + +thunk {thunk#6}() -> (x: ::main::A:0, y: ::main::A:0) { + let %0: (x: ::main::A:0, y: ::main::A:0) + let %1: ::main::A:0 + let %2: ::main::A:0 + + bb0(): { + %1 = opaque(::main::A:0, ()) + %2 = opaque(::main::A:0, ()) + %0 = (x: %1, y: %2) + + return %0 + } +} + +fn {ctor#::main::Outer:0}(%0: (), %1: (x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 { + let %2: ::main::Outer:0 + + bb0(): { + %2 = opaque(::main::Outer:0, %1) + + return %2 + } +} + +thunk {thunk#7}() -> ((x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 { + let %0: ((x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 + + bb0(): { + %0 = closure(({ctor#::main::Outer:0} as FnPtr), ()) + + return %0 + } +} + +*thunk {thunk#8}() -> ::main::Outer:0 { + let %0: (x: ::main::A:0, y: ::main::A:0) + let %1: ::main::A:0 + let %2: ::main::A:0 + let %3: ::main::Outer:0 + + bb0(): { + %1 = opaque(::main::A:0, ()) + %2 = opaque(::main::A:0, ()) + %0 = (x: %1, y: %2) + %3 = opaque(::main::Outer:0, %0) + + return %3 + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.jsonc b/libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.jsonc new file mode 100644 index 00000000000..730d16d09c2 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.jsonc @@ -0,0 +1,13 @@ +//@ run: pass +//@ description: The interpreter should be able to simply delegate to the underlying struct. +[ + "newtype", + "A", + "Null", + [ + "newtype", + "Outer", + { "#struct": { "x": "A", "y": "A" } }, + ["Outer", { "#struct": { "x": ["A"], "y": ["A"] } }] + ] +] diff --git a/libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.stdout b/libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.stdout new file mode 100644 index 00000000000..fa19ab2c819 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.stdout @@ -0,0 +1,37 @@ +Opaque( + Opaque { + name: Symbol( + "::main::Outer:0", + ), + value: Struct( + Struct { + fields: [ + Symbol( + "x", + ), + Symbol( + "y", + ), + ], + values: [ + Opaque( + Opaque { + name: Symbol( + "::main::A:0", + ), + value: Unit, + }, + ), + Opaque( + Opaque { + name: Symbol( + "::main::A:0", + ), + value: Unit, + }, + ), + ], + }, + ), + }, +) \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/reify/ctor-cached-closure.jsonc b/libs/@local/hashql/mir/tests/ui/reify/ctor-cached-closure.jsonc new file mode 100644 index 00000000000..68a06ccee6d --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/reify/ctor-cached-closure.jsonc @@ -0,0 +1,15 @@ +//@ run: pass +//@ description: Cached constructor references must produce closure aggregates, not bare FnPtrs. +//@ description: Regression test: the second use of a constructor was previously emitted as a bare +//@ description: FnPtr (cache hit path), while the calling convention expects a fat pointer (closure). +[ + "newtype", + "A", + "Null", + [ + "newtype", + "Outer", + { "#struct": { "x": "A", "y": "A" } }, + ["Outer", { "#struct": { "x": ["A"], "y": ["A"] } }] + ] +] diff --git a/libs/@local/hashql/mir/tests/ui/reify/ctor-cached-closure.stdout b/libs/@local/hashql/mir/tests/ui/reify/ctor-cached-closure.stdout new file mode 100644 index 00000000000..17f3aed3dc1 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/reify/ctor-cached-closure.stdout @@ -0,0 +1,101 @@ +fn {ctor#::main::A:0}(%0: ()) -> ::main::A:0 { + let %1: ::main::A:0 + + bb0(): { + %1 = opaque(::main::A:0, ()) + + return %1 + } +} + +thunk {thunk#2}() -> () -> ::main::A:0 { + let %0: () -> ::main::A:0 + + bb0(): { + %0 = closure(({ctor#::main::A:0} as FnPtr), ()) + + return %0 + } +} + +thunk {thunk#3}() -> ::main::A:0 { + let %0: () -> ::main::A:0 + let %1: ::main::A:0 + + bb0(): { + %0 = apply ({thunk#2} as FnPtr) + %1 = apply %0.0 %0.1 + + return %1 + } +} + +thunk {thunk#4}() -> () -> ::main::A:0 { + let %0: () -> ::main::A:0 + + bb0(): { + %0 = closure(({ctor#::main::A:0} as FnPtr), ()) + + return %0 + } +} + +thunk {thunk#5}() -> ::main::A:0 { + let %0: () -> ::main::A:0 + let %1: ::main::A:0 + + bb0(): { + %0 = apply ({thunk#4} as FnPtr) + %1 = apply %0.0 %0.1 + + return %1 + } +} + +thunk {thunk#6}() -> (x: ::main::A:0, y: ::main::A:0) { + let %0: ::main::A:0 + let %1: ::main::A:0 + let %2: (x: ::main::A:0, y: ::main::A:0) + + bb0(): { + %0 = apply ({thunk#3} as FnPtr) + %1 = apply ({thunk#5} as FnPtr) + %2 = (x: %0, y: %1) + + return %2 + } +} + +fn {ctor#::main::Outer:0}(%0: (), %1: (x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 { + let %2: ::main::Outer:0 + + bb0(): { + %2 = opaque(::main::Outer:0, %1) + + return %2 + } +} + +thunk {thunk#7}() -> ((x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 { + let %0: ((x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 + + bb0(): { + %0 = closure(({ctor#::main::Outer:0} as FnPtr), ()) + + return %0 + } +} + +*thunk {thunk#8}() -> ::main::Outer:0 { + let %0: (x: ::main::A:0, y: ::main::A:0) + let %1: ((x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 + let %2: ::main::Outer:0 + + bb0(): { + %0 = apply ({thunk#6} as FnPtr) + %1 = apply ({thunk#7} as FnPtr) + %2 = apply %1.0 %1.1 %0 + + return %2 + } +} \ No newline at end of file