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/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/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/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/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..53de57a402f 100644 --- a/libs/@local/hashql/mir/src/reify/mod.rs +++ b/libs/@local/hashql/mir/src/reify/mod.rs @@ -36,11 +36,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, }; diff --git a/libs/@local/hashql/mir/src/reify/rvalue.rs b/libs/@local/hashql/mir/src/reify/rvalue.rs index b29a0442b80..d604a0ba16c 100644 --- a/libs/@local/hashql/mir/src/reify/rvalue.rs +++ b/libs/@local/hashql/mir/src/reify/rvalue.rs @@ -53,6 +53,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), 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..0898aabc27b --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.aux.mir @@ -0,0 +1,419 @@ +════ 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 = ({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 { + bb0(): { + return ({ctor#::main::A:0} as FnPtr) + } +} + +thunk {thunk#5}() -> ::main::A:0 { + let %0: () -> ::main::A:0 + let %1: ::main::A:0 + + bb0(): { + %0 = ({ctor#::main::A:0} as FnPtr) + %1 = apply %0.0 %0.1 + + return %1 + } +} + +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 + let %3: ::main::A:0 + + bb0(): { + %1 = opaque(::main::A:0, ()) + %2 = ({ctor#::main::A:0} as FnPtr) + %3 = apply %2.0 %2.1 + %0 = (x: %1, y: %3) + + 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::Outer:0 + + bb0(): { + %0 = apply ({thunk#6} as FnPtr) + %1 = opaque(::main::Outer:0, %0) + + return %1 + } +} + +════ 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 { + bb0(): { + return ({ctor#::main::A:0} as FnPtr) + } +} + +thunk {thunk#5}() -> ::main::A:0 { + let %0: () -> ::main::A:0 + let %1: ::main::A:0 + + bb0(): { + %0 = ({ctor#::main::A:0} as FnPtr) + %1 = apply %0.0 %0.1 + + return %1 + } +} + +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 + let %3: ::main::A:0 + + bb0(): { + %1 = opaque(::main::A:0, ()) + %2 = ({ctor#::main::A:0} as FnPtr) + %3 = apply %2.0 %2.1 + %0 = (x: %1, y: %3) + + 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::Outer:0 + let %2: (x: ::main::A:0, y: ::main::A:0) + let %3: ::main::A:0 + let %4: () -> ::main::A:0 + let %5: ::main::A:0 + + bb0(): { + goto -> bb2() + } + + bb1(%0): { + %1 = opaque(::main::Outer:0, %0) + + return %1 + } + + bb2(): { + %3 = opaque(::main::A:0, ()) + %4 = ({ctor#::main::A:0} as FnPtr) + %5 = apply %4.0 %4.1 + %2 = (x: %3, y: %5) + + goto -> bb1(%2) + } +} + +════ 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 { + bb0(): { + return ({ctor#::main::A:0} as FnPtr) + } +} + +thunk {thunk#5}() -> ::main::A:0 { + let %0: () -> ::main::A:0 + let %1: ::main::A:0 + + bb0(): { + %0 = ({ctor#::main::A:0} as FnPtr) + %1 = apply %0.0 %0.1 + + return %1 + } +} + +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 + let %3: ::main::A:0 + + bb0(): { + %1 = opaque(::main::A:0, ()) + %2 = ({ctor#::main::A:0} as FnPtr) + %3 = apply %2.0 %2.1 + %0 = (x: %1, y: %3) + + 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: ::main::Outer:0 + let %1: (x: ::main::A:0, y: ::main::A:0) + let %2: ::main::A:0 + let %3: () -> ::main::A:0 + let %4: ::main::A:0 + + bb0(): { + %2 = opaque(::main::A:0, ()) + %3 = ({ctor#::main::A:0} as FnPtr) + %4 = apply %3.0 %3.1 + %1 = (x: %2, y: %4) + %0 = opaque(::main::Outer:0, %1) + + return %0 + } +} \ 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..9f1993f89bf --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.jsonc @@ -0,0 +1,14 @@ +//@ run: fail +//@ 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"] } }] + //~^ INTERNAL COMPILER ERROR cannot project field + ] +] diff --git a/libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.stderr b/libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.stderr new file mode 100644 index 00000000000..5c5735d78d5 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.stderr @@ -0,0 +1,13 @@ +error[interpret::type-invariant]: Type Invariant + ╭▸ +11 │ ["Outer", { "#struct": { "x": ["A"], "y": ["A"] } }] + │ ━━━━━ cannot project field from `*0` + │ + ├ help: type checking should have ensured only projectable types are projected + ├ help: This is a bug in the compiler, not an issue with your code. + ├ help: Please report this issue along with a minimal code example that reproduces the error. + ╰ note: Internal compiler errors indicate a bug in the compiler itself that needs to be fixed. + + We would appreciate if you could file a GitHub or Linear issue and reference this error. + + When reporting this issue, please include your query, any relevant type definitions, and the complete error message shown above. \ No newline at end of file