diff --git a/libs/@local/hashql/diagnostics/src/diagnostic/label.rs b/libs/@local/hashql/diagnostics/src/diagnostic/label.rs index 5785c5d6bfd..a447d815168 100644 --- a/libs/@local/hashql/diagnostics/src/diagnostic/label.rs +++ b/libs/@local/hashql/diagnostics/src/diagnostic/label.rs @@ -255,6 +255,39 @@ impl Labels { } } + /// Returns the number of labels in the collection. + /// + /// # Examples + /// + /// ``` + /// use hashql_diagnostics::{Label, diagnostic::Labels}; + /// + /// let mut labels = Labels::new(Label::new(0..5, "primary")); + /// labels.push(Label::new(10..15, "secondary")); + /// + /// assert_eq!(labels.len(), 2); + /// ``` + #[must_use] + pub const fn len(&self) -> usize { + self.labels.len() + } + + /// Returns `true` if the collection contains no labels. + /// + /// # Examples + /// + /// ``` + /// use hashql_diagnostics::{Label, diagnostic::Labels}; + /// + /// let labels = Labels::new(Label::new(0..5, "primary")); + /// + /// assert!(!labels.is_empty()); + /// ``` + #[must_use] + pub const fn is_empty(&self) -> bool { + self.labels.is_empty() + } + /// Adds a secondary label to the collection. /// /// All labels added via this method become secondary labels, which have the purpose of diff --git a/libs/@local/hashql/eval/src/postgres/filter/mod.rs b/libs/@local/hashql/eval/src/postgres/filter/mod.rs index fb854e37498..7e6eb4ac88d 100644 --- a/libs/@local/hashql/eval/src/postgres/filter/mod.rs +++ b/libs/@local/hashql/eval/src/postgres/filter/mod.rs @@ -360,7 +360,7 @@ impl<'ctx, 'heap, A: Allocator, S: Allocator> GraphReadFilterCompiler<'ctx, 'hea constant: &Constant<'heap>, ) -> Expression { match constant { - Constant::Int(int) if let Some(uint) = int.as_u32() => { + Constant::Int(int) if let Ok(uint) = u32::try_from(int.as_uint()) => { Expression::Constant(query::Constant::U32(uint)) } &Constant::Int(int) => db.parameters.int(int).into(), diff --git a/libs/@local/hashql/eval/tests/ui/postgres/constant-true-filter.aux.mir b/libs/@local/hashql/eval/tests/ui/postgres/constant-true-filter.aux.mir index 441df13a42b..7c924219d5d 100644 --- a/libs/@local/hashql/eval/tests/ui/postgres/constant-true-filter.aux.mir +++ b/libs/@local/hashql/eval/tests/ui/postgres/constant-true-filter.aux.mir @@ -10,7 +10,7 @@ thunk {thunk#1}() -> ::graph::temporal::PinnedTransactionTimeTemporalAxes | ::gr fn {graph::read::filter@7}(%0: (), %1: ::graph::types::knowledge::entity::Entity) -> Boolean { bb0(): { // interpreter - return 1 + return true } } diff --git a/libs/@local/hashql/eval/tests/ui/postgres/env-captured-variable.aux.mir b/libs/@local/hashql/eval/tests/ui/postgres/env-captured-variable.aux.mir index 91b42901077..349ef783c3c 100644 --- a/libs/@local/hashql/eval/tests/ui/postgres/env-captured-variable.aux.mir +++ b/libs/@local/hashql/eval/tests/ui/postgres/env-captured-variable.aux.mir @@ -10,7 +10,7 @@ fn {graph::read::filter@11}(%0: (::graph::types::knowledge::entity::EntityUuid,) fn {graph::read::filter@27}(%0: (), %1: ::graph::types::knowledge::entity::Entity) -> Boolean { bb0(): { // interpreter - return 1 + return true } } diff --git a/libs/@local/hashql/eval/tests/ui/postgres/filter/diamond_cfg_merge.snap b/libs/@local/hashql/eval/tests/ui/postgres/filter/diamond_cfg_merge.snap index 55cb5640df4..d0f27457b6e 100644 --- a/libs/@local/hashql/eval/tests/ui/postgres/filter/diamond_cfg_merge.snap +++ b/libs/@local/hashql/eval/tests/ui/postgres/filter/diamond_cfg_merge.snap @@ -15,11 +15,11 @@ fn {graph::read::filter@4294967040}(%0: (), %1: Entity) -> Boolean { } bb1(): { - goto -> bb3(1) + goto -> bb3(true) } bb2(): { - goto -> bb3(0) + goto -> bb3(false) } bb3(%3): { diff --git a/libs/@local/hashql/eval/tests/ui/postgres/filter/island_exit_switch_int.snap b/libs/@local/hashql/eval/tests/ui/postgres/filter/island_exit_switch_int.snap index a48748ff949..264678eb028 100644 --- a/libs/@local/hashql/eval/tests/ui/postgres/filter/island_exit_switch_int.snap +++ b/libs/@local/hashql/eval/tests/ui/postgres/filter/island_exit_switch_int.snap @@ -22,7 +22,7 @@ fn {graph::read::filter@4294967040}(%0: (), %1: Entity) -> ? { } bb1(): { - %7 = 1 + %7 = true return %7 } diff --git a/libs/@local/hashql/eval/tests/ui/postgres/filter/switch_int_many_branches.snap b/libs/@local/hashql/eval/tests/ui/postgres/filter/switch_int_many_branches.snap index 490a97f7c89..f4f8f4d594f 100644 --- a/libs/@local/hashql/eval/tests/ui/postgres/filter/switch_int_many_branches.snap +++ b/libs/@local/hashql/eval/tests/ui/postgres/filter/switch_int_many_branches.snap @@ -15,23 +15,23 @@ fn {graph::read::filter@4294967040}(%0: (), %1: Entity) -> Boolean { } bb1(): { - return 1 + return true } bb2(): { - return 0 + return false } bb3(): { - return 1 + return true } bb4(): { - return 0 + return false } bb5(): { - return 1 + return true } } ==================== Island (entry: bb0, target: postgres) ===================== diff --git a/libs/@local/hashql/eval/tests/ui/postgres/input-parameter-exists.aux.mir b/libs/@local/hashql/eval/tests/ui/postgres/input-parameter-exists.aux.mir index 0bcb261a65d..3bc0c683046 100644 --- a/libs/@local/hashql/eval/tests/ui/postgres/input-parameter-exists.aux.mir +++ b/libs/@local/hashql/eval/tests/ui/postgres/input-parameter-exists.aux.mir @@ -35,7 +35,7 @@ thunk {thunk#4}() -> Boolean { } bb2(): { - return 1 + return true } } @@ -56,7 +56,7 @@ fn {graph::read::filter@7}(%0: (), %1: ::graph::types::knowledge::entity::Entity } bb2(): { // postgres - return 1 + return true } } diff --git a/libs/@local/hashql/eval/tests/ui/postgres/logical-and-inputs.aux.mir b/libs/@local/hashql/eval/tests/ui/postgres/logical-and-inputs.aux.mir index c8527abb702..eff60fbb2ea 100644 --- a/libs/@local/hashql/eval/tests/ui/postgres/logical-and-inputs.aux.mir +++ b/libs/@local/hashql/eval/tests/ui/postgres/logical-and-inputs.aux.mir @@ -35,7 +35,7 @@ thunk {thunk#4}() -> Boolean { } bb2(): { - return 0 + return false } } @@ -56,7 +56,7 @@ fn {graph::read::filter@7}(%0: (), %1: ::graph::types::knowledge::entity::Entity } bb2(): { // postgres - return 0 + return false } } diff --git a/libs/@local/hashql/eval/tests/ui/postgres/mixed-sources-filter.aux.mir b/libs/@local/hashql/eval/tests/ui/postgres/mixed-sources-filter.aux.mir index 6a68ca98848..430a0ff87b5 100644 --- a/libs/@local/hashql/eval/tests/ui/postgres/mixed-sources-filter.aux.mir +++ b/libs/@local/hashql/eval/tests/ui/postgres/mixed-sources-filter.aux.mir @@ -20,7 +20,7 @@ fn {graph::read::filter@20}(%0: (::graph::types::knowledge::entity::EntityUuid,) fn {graph::read::filter@36}(%0: (), %1: ::graph::types::knowledge::entity::Entity) -> Boolean { bb0(): { // interpreter - return 1 + return true } } diff --git a/libs/@local/hashql/mir/benches/interpret.rs b/libs/@local/hashql/mir/benches/interpret.rs index cc65af23087..c1d86696304 100644 --- a/libs/@local/hashql/mir/benches/interpret.rs +++ b/libs/@local/hashql/mir/benches/interpret.rs @@ -10,7 +10,6 @@ use alloc::alloc::Global; use codspeed_criterion_compat::{BenchmarkId, Criterion, criterion_group, criterion_main}; use hashql_core::{ - collections::FastHashMap, heap::{ResetAllocator as _, Scratch}, r#type::environment::Environment, }; @@ -22,7 +21,7 @@ use hashql_mir::{ def::{DefId, DefIdSlice}, intern::Interner, interpret::{ - CallStack, Runtime, RuntimeConfig, + CallStack, Inputs, Runtime, RuntimeConfig, value::{Int, Value}, }, pass::{ @@ -93,17 +92,14 @@ fn fibonacci_recursive(criterion: &mut Criterion) { run_bencher(bencher, create_fibonacci_body, |_, bodies, scratch| { let scratch = &*scratch; let bodies = DefIdSlice::from_raw(bodies); + let inputs = Inputs::new_in(scratch); - let mut runtime = Runtime::new_in( - RuntimeConfig::default(), - bodies, - FastHashMap::default(), - scratch, - ); + let mut runtime = + Runtime::new_in(RuntimeConfig::default(), bodies, &inputs, scratch); let callstack = CallStack::new(&runtime, DefId::new(0), [Value::Integer(Int::from(*n))]); - let Ok(Value::Integer(int)) = runtime.run(callstack) else { + let Ok(Value::Integer(int)) = runtime.run(callstack, |_| unreachable!()) else { unreachable!() }; diff --git a/libs/@local/hashql/mir/package.json b/libs/@local/hashql/mir/package.json index c4513eecab0..d701248bc8d 100644 --- a/libs/@local/hashql/mir/package.json +++ b/libs/@local/hashql/mir/package.json @@ -9,7 +9,7 @@ "fix:clippy": "just clippy --fix", "lint:clippy": "just clippy", "test:codspeed": "cargo codspeed run -p hashql-mir", - "test:miri": "cargo miri nextest run -- changed_bitor interpret::locals::tests pass::execution::block_partitioned_vec::tests pass::execution::cost::tests", + "test:miri": "cargo miri nextest run -- changed_bitor interpret::locals::tests interpret::value::r#struct::tests pass::execution::block_partitioned_vec::tests pass::execution::cost::tests", "test:unit": "mise run test:unit @rust/hashql-mir" }, "dependencies": { diff --git a/libs/@local/hashql/mir/src/interpret/error.rs b/libs/@local/hashql/mir/src/interpret/error.rs index dd88d3f34ab..0bbd9c766db 100644 --- a/libs/@local/hashql/mir/src/interpret/error.rs +++ b/libs/@local/hashql/mir/src/interpret/error.rs @@ -28,8 +28,25 @@ use crate::body::{ /// Type alias for interpreter diagnostics. /// /// The default severity kind is [`Severity`], which allows any severity level. -pub(crate) type InterpretDiagnostic = - Diagnostic; +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) + } +} // Terminal categories for ICEs const LOCAL_ACCESS: TerminalDiagnosticCategory = TerminalDiagnosticCategory { @@ -89,6 +106,8 @@ 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 { @@ -109,6 +128,7 @@ impl DiagnosticCategory for InterpretDiagnosticCategory { Self::BoundsCheck => Some(&BOUNDS_CHECK), Self::RuntimeLimit => Some(&RUNTIME_LIMIT), Self::InputResolution => Some(&INPUT_RESOLUTION), + Self::Suspension(category) => Some(category), } } } @@ -130,6 +150,7 @@ impl TypeName { /// Creates a type name from a static string. /// /// Used for simple type names like "Integer", "String", etc. + #[must_use] pub const fn terse(str: &'static str) -> Self { Self::Static(Cow::Borrowed(str)) } @@ -191,7 +212,7 @@ pub struct UnaryTypeMismatch<'heap, A: Allocator> { /// A few variants represent legitimate runtime errors that can occur in valid /// programs (marked in their documentation). #[derive(Debug, Clone)] -pub enum RuntimeError<'heap, A: Allocator> { +pub enum RuntimeError<'heap, E, A: Allocator> { /// Attempted to read an uninitialized local variable. /// /// This is an ICE: MIR construction should ensure locals are initialized @@ -204,30 +225,42 @@ pub enum RuntimeError<'heap, A: Allocator> { /// Index operation used an invalid type for the index. /// /// This is an ICE: type checking should ensure index types are valid. - InvalidIndexType { base: TypeName, index: TypeName }, + InvalidIndexType { + base: TypeName, + index: TypeName, + }, /// Subscript operation applied to a non-subscriptable type. /// /// This is an ICE: type checking should ensure subscript targets are /// lists or dicts. - InvalidSubscriptType { base: TypeName }, + InvalidSubscriptType { + base: TypeName, + }, /// Field projection applied to a non-projectable type. /// /// This is an ICE: type checking should ensure projection targets are /// structs or tuples. - InvalidProjectionType { base: TypeName }, + InvalidProjectionType { + base: TypeName, + }, /// Named field projection applied to a non-struct type. /// /// This is an ICE: type checking should ensure named field access is /// only used on structs. - InvalidProjectionByNameType { base: TypeName }, + InvalidProjectionByNameType { + base: TypeName, + }, /// Field index does not exist on the aggregate type. /// /// This is an ICE: type checking should validate field indices. - UnknownField { base: TypeName, field: FieldIndex }, + UnknownField { + base: TypeName, + field: FieldIndex, + }, /// Field name does not exist on the struct type. /// @@ -241,19 +274,26 @@ pub enum RuntimeError<'heap, A: Allocator> { /// /// This is an ICE: MIR construction should ensure aggregates have the /// correct number of values for their fields. - StructFieldLengthMismatch { values: usize, fields: usize }, + StructFieldLengthMismatch { + values: usize, + fields: usize, + }, /// Switch discriminant has a non-integer type. /// /// This is an ICE: type checking should ensure switch discriminants /// are integers. - InvalidDiscriminantType { r#type: TypeName }, + InvalidDiscriminantType { + r#type: TypeName, + }, /// Switch discriminant value has no matching branch. /// /// This is an ICE: MIR construction should ensure all possible /// discriminant values have corresponding branches. - InvalidDiscriminant { value: Int }, + InvalidDiscriminant { + value: Int, + }, /// Execution reached unreachable code. /// @@ -277,7 +317,9 @@ pub enum RuntimeError<'heap, A: Allocator> { /// /// This is an ICE: type checking should ensure only function pointers /// are called. - ApplyNonPointer { r#type: TypeName }, + ApplyNonPointer { + r#type: TypeName, + }, /// Attempted to step execution with an empty callstack. /// @@ -288,23 +330,49 @@ pub enum RuntimeError<'heap, A: Allocator> { /// /// This is currently a user-facing error but may become an ICE once /// bounds checking is implemented in program analysis. - OutOfRange { length: usize, index: Int }, + OutOfRange { + length: usize, + index: Int, + }, /// Required input was not provided to the runtime. /// /// This is currently a user-facing error but may become an ICE once /// input validation is implemented in program analysis. - InputNotFound { name: Symbol<'heap> }, + InputNotFound { + name: Symbol<'heap>, + }, /// Recursion depth exceeded the configured limit. /// /// This is a user-facing error that occurs when a program recurses /// too deeply, likely due to infinite recursion or deeply nested /// data structures. - RecursionLimitExceeded { limit: usize }, + RecursionLimitExceeded { + limit: usize, + }, + + /// Value has the wrong runtime type. + /// + /// This is an ICE: type checking should ensure values have the + /// correct types at all usage sites. + UnexpectedValueType { + expected: TypeName, + actual: TypeName, + }, + + /// Opaque constructor name does not match any expected constructor. + /// + /// This is an ICE: type checking should ensure opaque values carry + /// a constructor name from the expected set for the encoded sum type. + InvalidConstructor { + name: Symbol<'heap>, + }, + + Suspension(E), } -impl RuntimeError<'_, A> { +impl RuntimeError<'_, E, A> { /// Converts this runtime error into a diagnostic using the provided callstack. /// /// The callstack provides span information for error localization. The first @@ -313,11 +381,12 @@ impl RuntimeError<'_, A> { pub fn into_diagnostic( self, callstack: impl IntoIterator, + on_suspension: impl FnOnce(E) -> InterpretDiagnostic, ) -> InterpretDiagnostic { let mut spans = callstack.into_iter(); let primary_span = spans.next().unwrap_or(SpanId::SYNTHETIC); - let mut diagnostic = self.make_diagnostic(primary_span); + let mut diagnostic = self.make_diagnostic(primary_span, on_suspension); // Add callstack frames as secondary labels for span in spans { @@ -327,7 +396,11 @@ impl RuntimeError<'_, A> { diagnostic } - fn make_diagnostic(self, span: SpanId) -> InterpretDiagnostic { + fn make_diagnostic( + self, + span: SpanId, + on_suspension: impl FnOnce(E) -> InterpretDiagnostic, + ) -> InterpretDiagnostic { match self { Self::UninitializedLocal { local, decl } => uninitialized_local(span, local, decl), Self::InvalidIndexType { base, index } => invalid_index_type(span, &base, &index), @@ -351,6 +424,61 @@ impl RuntimeError<'_, A> { 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::UnexpectedValueType { expected, actual } => { + unexpected_value_type(span, &expected, &actual) + } + Self::InvalidConstructor { name } => invalid_constructor(span, name), + Self::Suspension(suspension) => on_suspension(suspension), + } + } +} + +impl<'heap, A: Allocator> RuntimeError<'heap, !, A> { + /// Widens the suspension type from `!` to any `S`. + /// + /// Useful when composing interpreter operations (which cannot suspend) with + /// bridge operations (which can). The `Suspension` variant is uninhabited, + /// so this is a no-op at runtime. + #[must_use] + #[inline] + pub fn widen(self) -> RuntimeError<'heap, S, A> { + match self { + Self::UninitializedLocal { local, decl } => { + RuntimeError::UninitializedLocal { local, decl } + } + Self::InvalidIndexType { base, index } => { + RuntimeError::InvalidIndexType { base, index } + } + Self::InvalidSubscriptType { base } => RuntimeError::InvalidSubscriptType { base }, + Self::InvalidProjectionType { base } => RuntimeError::InvalidProjectionType { base }, + Self::InvalidProjectionByNameType { base } => { + RuntimeError::InvalidProjectionByNameType { base } + } + Self::UnknownField { base, field } => RuntimeError::UnknownField { base, field }, + Self::UnknownFieldByName { base, field } => { + RuntimeError::UnknownFieldByName { base, field } + } + Self::StructFieldLengthMismatch { values, fields } => { + RuntimeError::StructFieldLengthMismatch { values, fields } + } + Self::InvalidDiscriminantType { r#type } => { + RuntimeError::InvalidDiscriminantType { r#type } + } + Self::InvalidDiscriminant { value } => RuntimeError::InvalidDiscriminant { value }, + Self::UnreachableReached => RuntimeError::UnreachableReached, + Self::BinaryTypeMismatch(mismatch) => RuntimeError::BinaryTypeMismatch(mismatch), + Self::UnaryTypeMismatch(mismatch) => RuntimeError::UnaryTypeMismatch(mismatch), + Self::ApplyNonPointer { r#type } => RuntimeError::ApplyNonPointer { r#type }, + Self::CallstackEmpty => RuntimeError::CallstackEmpty, + Self::OutOfRange { length, index } => RuntimeError::OutOfRange { length, index }, + Self::InputNotFound { name } => RuntimeError::InputNotFound { name }, + Self::RecursionLimitExceeded { limit } => { + RuntimeError::RecursionLimitExceeded { limit } + } + Self::UnexpectedValueType { expected, actual } => { + RuntimeError::UnexpectedValueType { expected, actual } + } + Self::InvalidConstructor { name } => RuntimeError::InvalidConstructor { name }, } } } @@ -541,6 +669,36 @@ fn apply_non_pointer(span: SpanId, r#type: &TypeName) -> InterpretDiagnostic { diagnostic } +fn unexpected_value_type( + span: SpanId, + expected: &TypeName, + actual: &TypeName, +) -> InterpretDiagnostic { + let mut diagnostic = + Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug).primary( + Label::new(span, format!("expected `{expected}`, found `{actual}`")), + ); + + diagnostic.add_message(Message::help( + "type checking should have ensured the value has the correct type", + )); + + diagnostic +} + +fn invalid_constructor(span: SpanId, name: Symbol) -> InterpretDiagnostic { + let mut diagnostic = + Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug).primary( + Label::new(span, format!("unrecognized opaque constructor `{name}`")), + ); + + diagnostic.add_message(Message::help( + "type checking should have ensured the opaque constructor is from the expected set", + )); + + diagnostic +} + // ============================================================================= // ICE: Structural Invariant // ============================================================================= diff --git a/libs/@local/hashql/mir/src/interpret/inputs.rs b/libs/@local/hashql/mir/src/interpret/inputs.rs new file mode 100644 index 00000000000..134567cee8c --- /dev/null +++ b/libs/@local/hashql/mir/src/interpret/inputs.rs @@ -0,0 +1,207 @@ +//! External input values for the interpreter. +//! +//! HashQL is a referentially transparent functional language — the same query with the same inputs +//! always produces the same result. [`Inputs`] is the mechanism for injecting values that vary +//! between executions, serving as the functional equivalent of environment variables. +//! +//! In J-Expr syntax, inputs are declared with `["input", "name", "Type"]` and optionally given +//! defaults via `["input", "name", "Type", {"#literal": value}]`. At runtime, the interpreter +//! resolves these declarations against the [`Inputs`] provided to the [`Runtime`]. +//! +//! [`Runtime`]: super::Runtime + +use alloc::alloc::Global; +use core::alloc::Allocator; + +use hashql_core::{ + collections::{ + fast_hash_map, fast_hash_map_in, fast_hash_map_with_capacity, + fast_hash_map_with_capacity_in, + }, + symbol::Symbol, +}; + +use super::value::Value; + +/// External input values available during interpretation. +/// +/// Maps input names (as interned [`Symbol`]s) to their runtime [`Value`]s. The interpreter +/// consults this map when evaluating [`InputOp::Load`] (retrieve a value) and +/// [`InputOp::Exists`] (test whether a value was provided). +/// +/// # Examples +/// +/// ``` +/// use hashql_core::symbol::sym; +/// use hashql_mir::interpret::{ +/// Inputs, +/// value::{Int, Value}, +/// }; +/// +/// let mut inputs = Inputs::new(); +/// inputs.insert(sym::foo, Value::Integer(Int::from(42_i64))); +/// +/// assert!(inputs.contains(sym::foo)); +/// assert_eq!( +/// inputs.get(sym::foo), +/// Some(&Value::Integer(Int::from(42_i64))) +/// ); +/// assert!(!inputs.contains(sym::bar)); +/// ``` +/// +/// [`InputOp::Load`]: hashql_hir::node::operation::InputOp::Load +/// [`InputOp::Exists`]: hashql_hir::node::operation::InputOp::Exists +pub struct Inputs<'heap, A: Allocator = Global> { + inner: hashql_core::collections::FastHashMap, Value<'heap, A>, A>, +} + +impl Inputs<'_> { + /// Creates an empty input set. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::Inputs; + /// + /// let inputs = Inputs::new(); + /// assert!(inputs.is_empty()); + /// ``` + #[inline] + #[must_use] + pub fn new() -> Self { + Self { + inner: fast_hash_map(), + } + } + + /// Creates an empty input set with the given capacity. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::Inputs; + /// + /// let inputs = Inputs::with_capacity(8); + /// assert!(inputs.is_empty()); + /// ``` + #[inline] + #[must_use] + pub fn with_capacity(capacity: usize) -> Self { + Self { + inner: fast_hash_map_with_capacity(capacity), + } + } +} + +impl Default for Inputs<'_> { + fn default() -> Self { + Self::new() + } +} + +impl<'heap, A: Allocator> Inputs<'heap, A> { + /// Creates an empty input set in the given allocator. + #[inline] + #[must_use] + pub fn new_in(alloc: A) -> Self + where + A: Clone, + { + Self { + inner: fast_hash_map_in(alloc), + } + } + + /// Creates an empty input set with the given capacity in the given allocator. + #[inline] + #[must_use] + pub fn with_capacity_in(capacity: usize, alloc: A) -> Self + where + A: Clone, + { + Self { + inner: fast_hash_map_with_capacity_in(capacity, alloc), + } + } + + /// Inserts an input value, returning the previous value if the name was already present. + /// + /// # Examples + /// + /// ``` + /// use hashql_core::symbol::sym; + /// use hashql_mir::interpret::{Inputs, value::Value}; + /// + /// let mut inputs = Inputs::new(); + /// assert!(inputs.insert(sym::foo, Value::Unit).is_none()); + /// assert!(inputs.insert(sym::foo, Value::Unit).is_some()); + /// ``` + #[inline] + pub fn insert( + &mut self, + name: Symbol<'heap>, + value: Value<'heap, A>, + ) -> Option> { + self.inner.insert(name, value) + } + + /// Returns the value for the given input name. + /// + /// # Examples + /// + /// ``` + /// use hashql_core::symbol::sym; + /// use hashql_mir::interpret::{ + /// Inputs, + /// value::{Int, Value}, + /// }; + /// + /// let mut inputs = Inputs::new(); + /// inputs.insert(sym::foo, Value::Integer(Int::from(10_i64))); + /// + /// assert_eq!( + /// inputs.get(sym::foo), + /// Some(&Value::Integer(Int::from(10_i64))) + /// ); + /// assert_eq!(inputs.get(sym::bar), None); + /// ``` + #[inline] + #[must_use] + pub fn get(&self, name: Symbol<'heap>) -> Option<&Value<'heap, A>> { + self.inner.get(&name) + } + + /// Returns whether an input with the given name has been provided. + /// + /// # Examples + /// + /// ``` + /// use hashql_core::symbol::sym; + /// use hashql_mir::interpret::{Inputs, value::Value}; + /// + /// let mut inputs = Inputs::new(); + /// inputs.insert(sym::foo, Value::Unit); + /// + /// assert!(inputs.contains(sym::foo)); + /// assert!(!inputs.contains(sym::bar)); + /// ``` + #[inline] + #[must_use] + pub fn contains(&self, name: Symbol<'heap>) -> bool { + self.inner.contains_key(&name) + } + + /// Returns the number of inputs. + #[inline] + #[must_use] + pub fn len(&self) -> usize { + self.inner.len() + } + + /// Returns whether the input set is empty. + #[inline] + #[must_use] + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } +} diff --git a/libs/@local/hashql/mir/src/interpret/locals.rs b/libs/@local/hashql/mir/src/interpret/locals.rs index 13fcf958c13..f35dc1dccf4 100644 --- a/libs/@local/hashql/mir/src/interpret/locals.rs +++ b/libs/@local/hashql/mir/src/interpret/locals.rs @@ -32,7 +32,7 @@ use crate::{ /// /// Stores the values of local variables during interpretation of a function. /// Locals are indexed by [`Local`] and may be uninitialized. -pub(crate) struct Locals<'ctx, 'heap, A: Allocator> { +pub struct Locals<'ctx, 'heap, A: Allocator> { /// Allocator for creating new values. alloc: A, /// Local variable declarations (for error reporting). @@ -89,7 +89,7 @@ impl<'ctx, 'heap, A: Allocator> Locals<'ctx, 'heap, A> { /// Returns [`RuntimeError::UninitializedLocal`] if the local has not been /// initialized. #[inline] - pub(crate) fn local(&self, local: Local) -> Result<&Value<'heap, A>, RuntimeError<'heap, A>> { + pub fn local(&self, local: Local) -> Result<&Value<'heap, A>, RuntimeError<'heap, E, A>> { self.inner.get(local).ok_or_else(|| { let decl = self.decl[local]; RuntimeError::UninitializedLocal { local, decl } @@ -98,7 +98,7 @@ impl<'ctx, 'heap, A: Allocator> Locals<'ctx, 'heap, A> { /// Gets a mutable reference to a local variable's value. #[inline] - pub(crate) fn local_mut(&mut self, local: Local) -> &mut Value<'heap, A> { + pub fn local_mut(&mut self, local: Local) -> &mut Value<'heap, A> { self.inner.fill_until(local, || Value::Unit) } @@ -107,10 +107,10 @@ impl<'ctx, 'heap, A: Allocator> Locals<'ctx, 'heap, A> { /// Follows the chain of projections (field access, indexing) to reach /// the final value. #[inline] - pub(crate) fn place( + pub(crate) fn place( &self, Place { local, projections }: &Place<'heap>, - ) -> Result<&Value<'heap, A>, RuntimeError<'heap, A>> { + ) -> Result<&Value<'heap, A>, RuntimeError<'heap, E, A>> { let mut value = self.local(*local)?; for projection in projections { @@ -137,11 +137,11 @@ impl<'ctx, 'heap, A: Allocator> Locals<'ctx, 'heap, A> { /// the final value. Index projections are evaluated before the mutable /// borrow to avoid borrowing conflicts. #[inline] - pub(crate) fn place_mut( + pub(crate) fn place_mut( &mut self, place: Place<'heap>, scratch: &mut Scratch<'heap, A>, - ) -> Result<&mut Value<'heap, A>, RuntimeError<'heap, A>> + ) -> Result<&mut Value<'heap, A>, RuntimeError<'heap, E, A>> where A: Clone, { @@ -184,10 +184,15 @@ impl<'ctx, 'heap, A: Allocator> Locals<'ctx, 'heap, A> { /// /// - For place operands: evaluates the place and borrows the value /// - For constant operands: converts the constant to a value - pub(crate) fn operand( + /// + /// # Errors + /// + /// - [`RuntimeError`] if the local is unassigned, or does is malformed, such that indexing + /// operations failed. + pub fn operand( &self, operand: &Operand<'heap>, - ) -> Result>, RuntimeError<'heap, A>> + ) -> Result>, RuntimeError<'heap, E, A>> where A: Clone, { @@ -203,11 +208,11 @@ impl<'ctx, 'heap, A: Allocator> Locals<'ctx, 'heap, A> { /// /// The caller must ensure that `operands` and `slice` have the same length. #[expect(unsafe_code, clippy::mem_forget)] - unsafe fn write_operands( + unsafe fn write_operands( &self, slice: &mut [MaybeUninit>], operands: &[Operand<'heap>], - ) -> Result<(), RuntimeError<'heap, A>> + ) -> Result<(), RuntimeError<'heap, E, A>> where A: Clone, { @@ -256,10 +261,10 @@ impl<'ctx, 'heap, A: Allocator> Locals<'ctx, 'heap, A> { /// /// Returns [`Value::Unit`] for empty tuples. #[expect(unsafe_code, clippy::panic_in_result_fn)] - fn aggregate_tuple( + fn aggregate_tuple( &self, operands: &IdSlice>, - ) -> Result, RuntimeError<'heap, A>> + ) -> Result, RuntimeError<'heap, E, A>> where A: Clone, { @@ -290,11 +295,11 @@ impl<'ctx, 'heap, A: Allocator> Locals<'ctx, 'heap, A> { /// Returns [`RuntimeError::StructFieldLengthMismatch`] if the number of /// fields does not match the number of operands. #[expect(unsafe_code, clippy::panic_in_result_fn)] - fn aggregate_struct( + fn aggregate_struct( &self, fields: Interned<'heap, [Symbol<'heap>]>, operands: &IdSlice>, - ) -> Result, RuntimeError<'heap, A>> + ) -> Result, RuntimeError<'heap, E, A>> where A: Clone, { @@ -326,10 +331,10 @@ impl<'ctx, 'heap, A: Allocator> Locals<'ctx, 'heap, A> { /// Dispatches to the appropriate construction method based on the aggregate /// kind and evaluates all operands to build the result. #[expect(clippy::integer_division_remainder_used)] - pub(crate) fn aggregate( + pub(crate) fn aggregate( &self, Aggregate { kind, operands }: &Aggregate<'heap>, - ) -> Result, RuntimeError<'heap, A>> + ) -> Result, RuntimeError<'heap, E, A>> where A: Clone, { @@ -476,7 +481,7 @@ mod tests { // SAFETY: The buffer has not been written to yet and operands == buf unsafe { locals - .write_operands(&mut buf, &operands) + .write_operands::(&mut buf, &operands) .expect("write_operands should not fail"); } @@ -519,7 +524,7 @@ mod tests { ]; // SAFETY: The buffer has not been written to yet and operands == buf - let result = unsafe { locals.write_operands(&mut buf, &operands) }; + let result = unsafe { locals.write_operands::(&mut buf, &operands) }; assert_matches!(result, Err(RuntimeError::UninitializedLocal{local, ..}) if local == Local::new(1)); // IMPORTANT: Do not read from `buf` here. On error, the internal Guard has @@ -545,12 +550,12 @@ mod tests { // SAFETY: The buffer is empty, so no writes are performed. unsafe { locals - .write_operands(&mut buf, &operands) + .write_operands::(&mut buf, &operands) .expect("should not fail"); } let value = locals - .aggregate_tuple(IdSlice::from_raw(&[])) + .aggregate_tuple::(IdSlice::from_raw(&[])) .expect("should not fail"); assert_eq!(value, Value::Unit); } @@ -580,7 +585,7 @@ mod tests { ]; let value = locals - .aggregate_tuple(IdSlice::from_raw(&operands)) + .aggregate_tuple::(IdSlice::from_raw(&operands)) .expect("aggregate_tuple should succeed"); let Value::Tuple(tuple) = value else { @@ -626,7 +631,7 @@ mod tests { ]; let value = locals - .aggregate_struct(fields, IdSlice::from_raw(&operands)) + .aggregate_struct::(fields, IdSlice::from_raw(&operands)) .expect("aggregate_struct should succeed"); let Value::Struct(r#struct) = value else { diff --git a/libs/@local/hashql/mir/src/interpret/mod.rs b/libs/@local/hashql/mir/src/interpret/mod.rs index 84abc3de58d..789d75e0535 100644 --- a/libs/@local/hashql/mir/src/interpret/mod.rs +++ b/libs/@local/hashql/mir/src/interpret/mod.rs @@ -5,21 +5,36 @@ //! The interpreter uses a stack-based execution model: //! //! - **[`Runtime`]**: The main interpreter engine that holds configuration, function bodies, and -//! input values. It provides the [`Runtime::run`] method for execution. +//! input values. //! - **[`CallStack`]**: Manages the call frames during execution, tracking local variables, current //! block position, and statement index for each function call. //! - **[`value`]**: Runtime value representation including primitives (integers, numbers, strings), //! aggregates (structs, tuples), and collections (lists, dicts). +//! - **[`suspension`]**: Types for the suspend/resume protocol when the interpreter needs external +//! data. +//! +//! # Execution +//! +//! For simple synchronous execution, [`Runtime::run`] drives interpretation to completion, +//! handling suspensions via a closure. For async or multi-backend orchestration, use +//! [`Runtime::start`] and [`Runtime::resume`] to manually drive the suspend/resume loop. //! //! [`Severity::Bug`]: hashql_diagnostics::severity::Severity::Bug //! [`Severity::Error`]: hashql_diagnostics::severity::Severity::Error -mod error; +pub mod error; +mod inputs; mod locals; mod runtime; mod scratch; +pub mod suspension; #[cfg(test)] mod tests; pub mod value; -pub use runtime::{CallStack, Runtime, RuntimeConfig}; +pub use self::{ + error::{RuntimeError, TypeName}, + inputs::Inputs, + locals::Locals, + runtime::{CallStack, Runtime, RuntimeConfig, Yield}, +}; diff --git a/libs/@local/hashql/mir/src/interpret/runtime.rs b/libs/@local/hashql/mir/src/interpret/runtime.rs index aad3cb82061..210a804d842 100644 --- a/libs/@local/hashql/mir/src/interpret/runtime.rs +++ b/libs/@local/hashql/mir/src/interpret/runtime.rs @@ -9,6 +9,7 @@ //! - [`Runtime`]: The main interpreter, holding configuration, function bodies, and inputs //! - [`RuntimeConfig`]: Configuration options like recursion limits //! - [`CallStack`]: Manages call frames during execution +//! - [`Yield`]: Returned by the interpreter, containing either a final value or a suspension //! //! # Execution Model //! @@ -19,17 +20,37 @@ //! 3. Following terminators to navigate between blocks //! 4. Pushing/popping call frames for function calls and returns //! 5. Returning the final value when the entry function returns +//! +//! # Suspension and Continuation +//! +//! When the interpreter encounters a [`GraphRead`] terminator, it cannot make +//! further progress without external data (e.g., a database query result). Rather +//! than making the interpreter async, it uses a **suspend/resume** protocol: +//! +//! 1. Call [`Runtime::start`] to begin interpretation +//! 2. If it returns [`Yield::Suspension`], inspect the [`Suspension`] to determine what data is +//! needed +//! 3. Fulfill the request and call [`Runtime::resume`] with the resulting [`Continuation`] +//! 4. Repeat until [`Yield::Return`] is received +//! +//! For callers that can handle suspensions synchronously, [`Runtime::run`] provides +//! a convenience wrapper that drives the loop with a closure. +//! +//! [`GraphRead`]: crate::body::terminator::GraphRead +//! [`Continuation`]: super::suspension::Continuation use alloc::{alloc::Global, borrow::Cow}; use core::{alloc::Allocator, debug_assert_matches, hint::cold_path, ops::ControlFlow}; -use hashql_core::{collections::FastHashMap, span::SpanId, symbol::Symbol}; +use hashql_core::span::SpanId; use hashql_hir::node::operation::{InputOp, UnOp}; use super::{ + Inputs, error::{BinaryTypeMismatch, InterpretDiagnostic, RuntimeError, TypeName, UnaryTypeMismatch}, locals::Locals, scratch::Scratch, + suspension::{Continuation, Suspension}, value::{Int, Value}, }; use crate::{ @@ -38,26 +59,56 @@ use crate::{ basic_block::{BasicBlock, BasicBlockId}, rvalue::{Apply, BinOp, Binary, Input, RValue, Unary}, statement::{Assign, StatementKind}, - terminator::{Goto, Return, SwitchInt, Target, TerminatorKind}, + terminator::{Goto, GraphReadHead, Return, SwitchInt, Target, TerminatorKind}, }, def::{DefId, DefIdSlice}, + interpret::suspension::{self, GraphReadSuspension}, }; +/// Creates a new call frame for the given body with the provided arguments. +fn make_frame_in<'ctx, 'heap, E, A: Allocator + Clone>( + body: &'ctx Body<'heap>, + args: impl ExactSizeIterator, E>>, + alloc: A, +) -> Result, E> { + let locals = Locals::new_in(body, args, alloc)?; + + Ok(Frame { + locals, + body, + current_block: CurrentBlock { + id: BasicBlockId::START, + block: &body.basic_blocks[BasicBlockId::START], + }, + current_statement: 0, + }) +} + +/// The current basic block being executed within a frame. +/// +/// Caches both the [`BasicBlockId`] and a direct reference to the [`BasicBlock`] +/// to avoid repeated indexing into the body's block storage during execution. +#[derive(Debug, Copy, Clone)] +pub(super) struct CurrentBlock<'ctx, 'heap> { + pub id: BasicBlockId, + pub block: &'ctx BasicBlock<'heap>, +} + /// A single call frame in the interpreter's call stack. /// /// Each frame represents an active function call and tracks: /// - Local variable storage /// - The function body being executed /// - Current position (block and statement index) -struct Frame<'ctx, 'heap, A: Allocator> { +pub(super) struct Frame<'ctx, 'heap, A: Allocator> { /// Local variable storage for this function call. - locals: Locals<'ctx, 'heap, A>, + pub locals: Locals<'ctx, 'heap, A>, /// The MIR body being executed. - body: &'ctx Body<'heap>, + pub body: &'ctx Body<'heap>, /// The current basic block. - current_block: &'ctx BasicBlock<'heap>, + pub current_block: CurrentBlock<'ctx, 'heap>, /// Index of the next statement to execute in the current block. - current_statement: usize, + pub current_statement: usize, } /// The call stack for the MIR interpreter. @@ -66,8 +117,12 @@ struct Frame<'ctx, 'heap, A: Allocator> { /// /// The call stack also provides [`unwind`](Self::unwind) for error reporting, /// which walks the stack to collect span information for diagnostics. +#[expect( + clippy::field_scoped_visibility_modifiers, + reason = "used when resolving the suspension" +)] pub struct CallStack<'ctx, 'heap, A: Allocator = Global> { - frames: Vec, A>, + pub(super) frames: Vec, A>, } impl<'ctx, 'heap, A: Allocator> CallStack<'ctx, 'heap, A> { @@ -91,6 +146,26 @@ impl<'ctx, 'heap, A: Allocator> CallStack<'ctx, 'heap, A> { Self { frames } } + /// Creates a new call stack with an initial call to the given body. + /// + /// # Errors + /// + /// Returns `E` if any argument in `args` is an `Err`. + pub fn new_in( + body: &'ctx Body<'heap>, + args: impl IntoIterator, E>, IntoIter: ExactSizeIterator>, + alloc: A, + ) -> Result + where + A: Allocator + Clone, + { + let frame = make_frame_in(body, args.into_iter(), alloc.clone())?; + let mut frames = Vec::new_in(alloc); + frames.push(frame); + + Ok(Self { frames }) + } + /// Unwinds the call stack to produce a trace of definition IDs and spans. /// /// Returns an iterator over `(DefId, SpanId)` pairs, starting from the @@ -102,15 +177,98 @@ impl<'ctx, 'heap, A: Allocator> CallStack<'ctx, 'heap, A> { pub fn unwind(&self) -> impl Iterator { self.frames.iter().rev().map(|frame| { let body = frame.body.id; - let span = if frame.current_statement >= frame.current_block.statements.len() { - frame.current_block.terminator.span + let span = if frame.current_statement >= frame.current_block.block.statements.len() { + frame.current_block.block.terminator.span } else { - frame.current_block.statements[frame.current_statement].span + frame.current_block.block.statements[frame.current_statement].span }; (body, span) }) } + + /// Returns the local variable storage for the innermost active call. + /// + /// # Errors + /// + /// Returns [`RuntimeError::CallstackEmpty`] if there are no active calls. + pub fn locals( + &self, + ) -> Result<&Locals<'ctx, 'heap, A>, RuntimeError<'heap, E, R>> { + self.frames + .last() + .ok_or(RuntimeError::CallstackEmpty) + .map(|frame| &frame.locals) + } + + /// Returns mutable access to the local variable storage for the current call. + /// + /// # Errors + /// + /// Returns [`RuntimeError::CallstackEmpty`] if there are no active calls. + pub fn locals_mut( + &mut self, + ) -> Result<&mut Locals<'ctx, 'heap, A>, RuntimeError<'heap, !, R>> { + self.frames + .last_mut() + .ok_or(RuntimeError::CallstackEmpty) + .map(|frame| &mut frame.locals) + } + + /// Returns the [`BasicBlockId`] of the current block. + /// + /// # Errors + /// + /// Returns [`RuntimeError::CallstackEmpty`] if there are no active calls. + pub fn current_block(&self) -> Result> { + self.frames + .last() + .map(|frame| frame.current_block.id) + .ok_or(RuntimeError::CallstackEmpty) + } + + /// Sets the current block and resets the statement counter to zero. + /// + /// The caller must ensure that `block_id` is a valid transition target + /// in the current execution context. The block itself is bounds-checked + /// against the body's block storage, but reachability is not verified. + /// + /// # Errors + /// + /// Returns [`RuntimeError::CallstackEmpty`] if there are no active calls. + pub fn set_current_block_unchecked( + &mut self, + block_id: BasicBlockId, + ) -> Result<(), RuntimeError<'heap, E, A>> { + let frame = self.frames.last_mut().ok_or(RuntimeError::CallstackEmpty)?; + + let block = &frame.body.basic_blocks[block_id]; + + frame.current_block = CurrentBlock { + id: block_id, + block, + }; + frame.current_statement = 0; + + Ok(()) + } +} + +/// Result of running the interpreter until it can no longer make progress. +/// +/// The interpreter either completes with a final value or suspends at a point +/// where it needs external data (such as a database query result) before it +/// can continue. +#[derive(Debug)] +pub enum Yield<'ctx, 'heap, A: Allocator> { + /// The entry function returned a value. Interpretation is complete. + Return(Value<'heap, A>), + /// The interpreter suspended and needs external data to continue. + /// + /// The caller should inspect the [`Suspension`] to determine what is needed, + /// fulfill the request, and call [`Runtime::resume`] with the resulting + /// [`Continuation`]. + Suspension(Suspension<'ctx, 'heap>), } /// Internal signal indicating whether to pop the current frame after a terminator. @@ -167,7 +325,7 @@ pub struct Runtime<'ctx, 'heap, A: Allocator = Global> { /// All available function bodies, indexed by [`DefId`]. bodies: &'ctx DefIdSlice>, /// Input values available for [`InputOp::Load`] operations. - inputs: FastHashMap, Value<'heap, A>>, + inputs: &'ctx Inputs<'heap, A>, scratch: Scratch<'heap, A>, } @@ -182,18 +340,21 @@ impl<'ctx, 'heap> Runtime<'ctx, 'heap> { pub fn new( config: RuntimeConfig, bodies: &'ctx DefIdSlice>, - inputs: FastHashMap, Value<'heap>>, + inputs: &'ctx Inputs<'heap>, ) -> Self { Self::new_in(config, bodies, inputs, Global) } } impl<'ctx, 'heap, A: Allocator + Clone> Runtime<'ctx, 'heap, A> { + /// Creates a new runtime with the given configuration, bodies, inputs, and allocator. + /// + /// See [`Runtime::new`] for details on the parameters. #[must_use] pub fn new_in( config: RuntimeConfig, bodies: &'ctx DefIdSlice>, - inputs: FastHashMap, Value<'heap, A>>, + inputs: &'ctx Inputs<'heap, A>, alloc: A, ) -> Self { Self { @@ -210,26 +371,20 @@ impl<'ctx, 'heap, A: Allocator + Clone> Runtime<'ctx, 'heap, A> { func: DefId, args: impl ExactSizeIterator, E>>, ) -> Result, E> { - let body = &self.bodies[func]; - - let locals = Locals::new_in(body, args, self.alloc.clone())?; - - Ok(Frame { - locals, - body, - current_block: &body.basic_blocks[BasicBlockId::START], - current_statement: 0, - }) + make_frame_in(&self.bodies[func], args, self.alloc.clone()) } #[inline] - fn step_terminator_goto( + fn step_terminator_goto( &mut self, frame: &mut Frame<'ctx, 'heap, A>, Target { block, args }: Target<'heap>, - ) -> Result<(), RuntimeError<'heap, A>> { + ) -> Result<(), RuntimeError<'heap, E, A>> { if args.is_empty() { - frame.current_block = &frame.body.basic_blocks[block]; + frame.current_block = CurrentBlock { + id: block, + block: &frame.body.basic_blocks[block], + }; frame.current_statement = 0; return Ok(()); } @@ -259,17 +414,20 @@ impl<'ctx, 'heap, A: Allocator + Clone> Runtime<'ctx, 'heap, A> { frame.locals.insert(param, value); } - frame.current_block = &frame.body.basic_blocks[block]; + frame.current_block = CurrentBlock { + id: block, + block: &frame.body.basic_blocks[block], + }; frame.current_statement = 0; Ok(()) } - fn step_terminator( + fn step_terminator( &mut self, stack: &mut [Frame<'ctx, 'heap, A>], frame: &mut Frame<'ctx, 'heap, A>, - ) -> Result, PopFrame>, RuntimeError<'heap, A>> { - let terminator = &frame.current_block.terminator.kind; + ) -> Result, PopFrame>, RuntimeError<'heap, E, A>> { + let terminator = &frame.current_block.block.terminator.kind; match terminator { &TerminatorKind::Goto(Goto { target }) => { @@ -308,12 +466,12 @@ impl<'ctx, 'heap, A: Allocator + Clone> Runtime<'ctx, 'heap, A> { // one that we break on. cold_path(); - return Ok(ControlFlow::Break(value)); + return Ok(ControlFlow::Break(Yield::Return(value))); }; // The caller is suspended at an `Assign` statement with an `Apply` rvalue. // We write the return value to the LHS of that assignment and resume. - let statement = &caller.current_block.statements[caller.current_statement]; + let statement = &caller.current_block.block.statements[caller.current_statement]; let StatementKind::Assign(Assign { lhs, rhs }) = &statement.kind else { unreachable!("we can only be called from an apply"); }; @@ -326,17 +484,30 @@ impl<'ctx, 'heap, A: Allocator + Clone> Runtime<'ctx, 'heap, A> { Ok(ControlFlow::Continue(PopFrame::Yes)) } - TerminatorKind::GraphRead(_) => { - unimplemented!("GraphRead terminator not implemented") + TerminatorKind::GraphRead(read) => { + let axis = match read.head { + GraphReadHead::Entity { axis } => frame.locals.operand(&axis)?, + }; + + let axis = suspension::extract_axis(&axis)?; + + Ok(ControlFlow::Break(Yield::Suspension( + Suspension::GraphRead(GraphReadSuspension { + body: frame.body.id, + block: frame.current_block.id, + read, + axis, + }), + ))) } TerminatorKind::Unreachable => Err(RuntimeError::UnreachableReached), } } - fn eval_rvalue_binary( + fn eval_rvalue_binary( frame: &Frame<'ctx, 'heap, A>, Binary { op, left, right }: &Binary<'heap>, - ) -> Result, RuntimeError<'heap, A>> { + ) -> Result, RuntimeError<'heap, E, A>> { let lhs = frame.locals.operand(left)?; let rhs = frame.locals.operand(right)?; @@ -422,10 +593,10 @@ impl<'ctx, 'heap, A: Allocator + Clone> Runtime<'ctx, 'heap, A> { } } - fn eval_rvalue_unary( + fn eval_rvalue_unary( frame: &Frame<'ctx, 'heap, A>, Unary { op, operand }: &Unary<'heap>, - ) -> Result, RuntimeError<'heap, A>> { + ) -> Result, RuntimeError<'heap, E, A>> { let operand = frame.locals.operand(operand)?; match op { @@ -501,29 +672,29 @@ impl<'ctx, 'heap, A: Allocator + Clone> Runtime<'ctx, 'heap, A> { } } - fn eval_rvalue_input( + fn eval_rvalue_input( &self, Input { op, name }: &Input<'heap>, - ) -> Result, RuntimeError<'heap, A>> { + ) -> Result, RuntimeError<'heap, E, A>> { match op { // `required` is used only by static control-flow analysis; at runtime we always // error if the input is missing. - InputOp::Load { required: _ } => self.inputs.get(name).map_or_else( + InputOp::Load { required: _ } => self.inputs.get(*name).map_or_else( || Err(RuntimeError::InputNotFound { name: *name }), |value| Ok(value.clone()), ), - InputOp::Exists => Ok(Value::Integer(self.inputs.contains_key(name).into())), + InputOp::Exists => Ok(Value::Integer(self.inputs.contains(*name).into())), } } - fn eval_rvalue_apply( + fn eval_rvalue_apply( &self, frame: &Frame<'ctx, 'heap, A>, Apply { function, arguments, }: &Apply<'heap>, - ) -> Result, RuntimeError<'heap, A>> { + ) -> Result, RuntimeError<'heap, E, A>> { let function = frame.locals.operand(function)?; let Value::Pointer(pointer) = function.as_ref() else { return Err(RuntimeError::ApplyNonPointer { @@ -539,11 +710,12 @@ impl<'ctx, 'heap, A: Allocator + Clone> Runtime<'ctx, 'heap, A> { ) } - fn eval_rvalue( + fn eval_rvalue( &self, frame: &Frame<'ctx, 'heap, A>, rvalue: &RValue<'heap>, - ) -> Result, Value<'heap, A>>, RuntimeError<'heap, A>> { + ) -> Result, Value<'heap, A>>, RuntimeError<'heap, E, A>> + { match rvalue { RValue::Load(operand) => frame .locals @@ -564,11 +736,11 @@ impl<'ctx, 'heap, A: Allocator + Clone> Runtime<'ctx, 'heap, A> { } } - fn step_statement_assign( + fn step_statement_assign( &mut self, frame: &mut Frame<'ctx, 'heap, A>, Assign { lhs, rhs }: &Assign<'heap>, - ) -> Result>, RuntimeError<'heap, A>> { + ) -> Result>, RuntimeError<'heap, E, A>> { let value = self.eval_rvalue(frame, rhs)?; let value = match value { ControlFlow::Continue(value) => value, @@ -581,15 +753,15 @@ impl<'ctx, 'heap, A: Allocator + Clone> Runtime<'ctx, 'heap, A> { Ok(None) } - fn step( + fn step( &mut self, callstack: &mut CallStack<'ctx, 'heap, A>, - ) -> Result>, RuntimeError<'heap, A>> { + ) -> Result>, RuntimeError<'heap, E, A>> { let Some((frame, stack)) = callstack.frames.split_last_mut() else { return Err(RuntimeError::CallstackEmpty); }; - if frame.current_statement >= frame.current_block.statements.len() { + if frame.current_statement >= frame.current_block.block.statements.len() { let next = self.step_terminator(stack, frame)?; return match next { @@ -603,7 +775,7 @@ impl<'ctx, 'heap, A: Allocator + Clone> Runtime<'ctx, 'heap, A> { }; } - let statement = &frame.current_block.statements[frame.current_statement]; + let statement = &frame.current_block.block.statements[frame.current_statement]; let next_frame = match &statement.kind { StatementKind::Assign(assign) => self.step_statement_assign(frame, assign)?, StatementKind::Nop | StatementKind::StorageLive(_) | StatementKind::StorageDead(_) => { @@ -626,40 +798,176 @@ impl<'ctx, 'heap, A: Allocator + Clone> Runtime<'ctx, 'heap, A> { Ok(ControlFlow::Continue(())) } - /// Executes the MIR starting from the given call stack. + /// Steps the interpreter until it either returns a value or suspends. + /// + /// This is the low-level driver loop. It does **not** clear scratch state, + /// so callers must call [`reset`](Self::reset) before the first invocation. + /// Prefer [`start`](Self::start) for the initial invocation and + /// [`resume`](Self::resume) after fulfilling a suspension. + /// + /// # Errors + /// + /// Returns a runtime error if interpretation fails. + pub fn run_until_suspension( + &mut self, + callstack: &mut CallStack<'ctx, 'heap, A>, + ) -> Result, RuntimeError<'heap, E, A>> { + loop { + let next = self.step(callstack)?; + if let ControlFlow::Break(value) = next { + return Ok(value); + } + } + } + + /// Runs the interpreter until it hits a backend transition point. /// - /// Runs the interpreter until the entry function returns or an error occurs. - /// The call stack should be initialized with [`CallStack::new`] pointing to - /// the entry function. + /// The `continue` callback is invoked at each block boundary in the outermost + /// call frame. It receives the [`BasicBlockId`] just entered and returns + /// whether execution should continue on this backend. When it returns `false`, + /// the method returns [`ControlFlow::Break`] without executing any statements + /// in that block. /// - /// # Returns + /// # Return value /// - /// The value returned by the entry function. + /// - [`ControlFlow::Break`]: transition point reached. The callstack is positioned at the block + /// where `continue` returned `false`. + /// - [`ControlFlow::Continue`] with [`Yield::Return`]: interpretation completed. + /// - [`ControlFlow::Continue`] with [`Yield::Suspension`]: interpreter suspended for external + /// data. Apply the continuation and call this method again. /// /// # Errors /// - /// Returns a diagnostic if any runtime error occurs. The diagnostic includes - /// the error message and a call stack trace for error localization. + /// Returns a runtime error if interpretation fails. + pub fn run_until_transition( + &mut self, + callstack: &mut CallStack<'ctx, 'heap, A>, + mut r#continue: impl FnMut(BasicBlockId) -> bool, + ) -> Result>, RuntimeError<'heap, E, A>> { + loop { + // Check if we've entered a new block in the outermost frame. This must happen + // *before* stepping so that block transitions from `Continuation::apply` (which + // sets `current_statement = 0` on the target block) are visible on re-entry. + // During nested calls (multiple frames) the interpreter runs freely; only + // top-level block boundaries are transition candidates. + if let [frame] = &*callstack.frames + && frame.current_statement == 0 + && !r#continue(frame.current_block.id) + { + return Ok(ControlFlow::Break(frame.current_block.id)); + } + + let next = self.step(callstack)?; + if let ControlFlow::Break(value) = next { + return Ok(ControlFlow::Continue(value)); + } + } + } + + fn try_run( + &mut self, + callstack: &mut CallStack<'ctx, 'heap, A>, + mut on_suspension: impl FnMut( + Suspension<'ctx, 'heap>, + ) + -> Result, RuntimeError<'heap, !, A>>, + ) -> Result, RuntimeError<'heap, !, A>> { + self.scratch.clear(); + + loop { + match self.run_until_suspension(callstack)? { + Yield::Return(value) => return Ok(value), + Yield::Suspension(suspension) => { + let continuation = on_suspension(suspension)?; + continuation.apply(callstack)?; + } + } + } + } + + /// Runs the interpreter to completion, handling suspensions inline. + /// + /// This is a convenience method for callers that can handle all suspensions + /// synchronously via a closure. The `on_suspension` callback receives each + /// [`Suspension`], fulfills it, and returns the corresponding [`Continuation`]. + /// + /// For async or more complex orchestration, use [`start`](Self::start) and + /// [`resume`](Self::resume) instead. + /// + /// # Errors + /// + /// Returns a diagnostic if interpretation fails or if `on_suspension` returns + /// an error. pub fn run( &mut self, mut callstack: CallStack<'ctx, 'heap, A>, + on_suspension: impl FnMut( + Suspension<'ctx, 'heap>, + ) + -> Result, RuntimeError<'heap, !, A>>, ) -> Result, InterpretDiagnostic> { + self.try_run(&mut callstack, on_suspension) + .map_err(|error| { + let spans = callstack.unwind(); + + error.into_diagnostic(spans.map(|(_, span)| span), |suspension| suspension) + }) + } + + /// Clears ephemeral scratch state. + /// + /// Called automatically by [`start`](Self::start). Callers using the lower-level + /// [`run_until_suspension`](Self::run_until_suspension) directly must call this + /// before the first invocation. + pub fn reset(&mut self) { self.scratch.clear(); + } - loop { - let result = self.step(&mut callstack); - let next = match result { - Ok(next) => next, - Err(error) => { - let spans = callstack.unwind(); + /// Begins interpretation from the given call stack. + /// + /// Clears scratch state and runs until the interpreter either returns + /// or suspends. This should be used for the initial invocation; use + /// [`resume`](Self::resume) to continue after a suspension. + /// + /// # Errors + /// + /// Returns a diagnostic if any runtime error occurs during interpretation. + pub fn start( + &mut self, + callstack: &mut CallStack<'ctx, 'heap, A>, + ) -> Result, InterpretDiagnostic> { + self.reset(); + self.run_until_suspension(callstack).map_err(|error| { + let spans = callstack.unwind(); - return Err(error.into_diagnostic(spans.map(|(_, span)| span))); - } - }; + error.into_diagnostic(spans.map(|(_, span)| span), |suspension| suspension) + }) + } - if let ControlFlow::Break(value) = next { - return Ok(value); - } - } + /// Continues interpretation after a suspension has been fulfilled. + /// + /// Resolves the [`Continuation`] into the call stack and resumes stepping + /// until the interpreter returns or suspends again. + /// + /// # Errors + /// + /// Returns a diagnostic if the continuation is invalid or if a runtime + /// error occurs during interpretation. + pub fn resume( + &mut self, + callstack: &mut CallStack<'ctx, 'heap, A>, + continuation: Continuation<'ctx, 'heap, A>, + ) -> Result, InterpretDiagnostic> { + continuation.apply(callstack).map_err(|error| { + let spans = callstack.unwind(); + + error.into_diagnostic(spans.map(|(_, span)| span), |suspension| suspension) + })?; + + self.run_until_suspension(callstack).map_err(|error| { + let spans = callstack.unwind(); + + error.into_diagnostic(spans.map(|(_, span)| span), |suspension| suspension) + }) } } diff --git a/libs/@local/hashql/mir/src/interpret/suspension/graph_read.rs b/libs/@local/hashql/mir/src/interpret/suspension/graph_read.rs new file mode 100644 index 00000000000..316d18fcfcc --- /dev/null +++ b/libs/@local/hashql/mir/src/interpret/suspension/graph_read.rs @@ -0,0 +1,134 @@ +use core::{alloc::Allocator, ops::Bound}; + +use hashql_core::symbol::sym; + +use super::temporal::{TemporalAxesInterval, TemporalInterval, Timestamp}; +use crate::interpret::{RuntimeError, TypeName, value::Value}; + +fn extract_timestamp<'heap, E, A: Allocator>( + value: &Value<'heap, A>, +) -> Result> { + let Value::Opaque(opaque) = value else { + return Err(RuntimeError::UnexpectedValueType { + expected: TypeName::terse("Opaque"), + actual: value.type_name().into(), + }); + }; + debug_assert_eq!(opaque.name(), sym::path::Timestamp); + + let &Value::Integer(timestamp) = opaque.value() else { + return Err(RuntimeError::UnexpectedValueType { + expected: TypeName::terse("Integer"), + actual: opaque.value().type_name().into(), + }); + }; + + Ok(Timestamp::from(timestamp)) +} + +fn extract_bound<'heap, E, A: Allocator>( + value: &Value<'heap, A>, +) -> Result, RuntimeError<'heap, E, A>> { + let Value::Opaque(bound) = value else { + return Err(RuntimeError::UnexpectedValueType { + expected: TypeName::terse("Opaque"), + actual: value.type_name().into(), + }); + }; + + let make_bound = match bound.name().as_constant() { + Some(sym::path::UnboundedTemporalBound::CONST) => return Ok(Bound::Unbounded), + Some(sym::path::InclusiveTemporalBound::CONST) => Bound::Included, + Some(sym::path::ExclusiveTemporalBound::CONST) => Bound::Excluded, + _ => { + return Err(RuntimeError::InvalidConstructor { name: bound.name() }); + } + }; + + let value = extract_timestamp(bound.value())?; + Ok(make_bound(value)) +} + +fn extract_interval<'heap, E, A: Allocator>( + value: &Value<'heap, A>, +) -> Result<(Bound, Bound), RuntimeError<'heap, E, A>> { + let Value::Opaque(opaque) = value else { + return Err(RuntimeError::UnexpectedValueType { + expected: TypeName::terse("Opaque"), + actual: value.type_name().into(), + }); + }; + debug_assert_eq!(opaque.name(), sym::path::Interval); + + let value = opaque.value(); + + let start = value.project_by_name(sym::start)?; + let end = value.project_by_name(sym::end)?; + + let start = extract_bound(start)?; + let end = extract_bound(end)?; + + Ok((start, end)) +} + +pub(crate) fn extract_axis<'heap, E, A: Allocator>( + value: &Value<'heap, A>, +) -> Result> { + let Value::Opaque(opaque) = value else { + return Err(RuntimeError::UnexpectedValueType { + expected: TypeName::terse("Opaque"), + actual: value.type_name().into(), + }); + }; + + // The resulting value must be a `QueryTemporalAxes`, this means it's either a + // `PinnedTransactionTimeTemporalAxes` or `PinnedDecisionTimeTemporalAxes`. + let (pinned, variable) = match opaque.name().as_constant() { + Some( + sym::path::PinnedTransactionTimeTemporalAxes::CONST + | sym::path::PinnedDecisionTimeTemporalAxes::CONST, + ) => { + let value = opaque.value(); + + let pinned = value.project_by_name(sym::pinned)?; + let variable = value.project_by_name(sym::variable)?; + + (pinned, variable) + } + _ => { + return Err(RuntimeError::InvalidConstructor { + name: opaque.name(), + }); + } + }; + + let Value::Opaque(pinned) = pinned else { + return Err(RuntimeError::UnexpectedValueType { + expected: TypeName::terse("Opaque"), + actual: pinned.type_name().into(), + }); + }; + let Value::Opaque(variable) = variable else { + return Err(RuntimeError::UnexpectedValueType { + expected: TypeName::terse("Opaque"), + actual: variable.type_name().into(), + }); + }; + + let timestamp = extract_timestamp(pinned.value())?; + let interval = extract_interval(variable.value())?; + + match pinned.name().as_constant() { + Some(sym::path::TransactionTime::CONST) => Ok(TemporalAxesInterval { + transaction_time: TemporalInterval::point(timestamp), + decision_time: TemporalInterval::interval(interval), + }), + Some(sym::path::DecisionTime::CONST) => Ok(TemporalAxesInterval { + transaction_time: TemporalInterval::interval(interval), + decision_time: TemporalInterval::point(timestamp), + }), + _ => Err(RuntimeError::InvalidConstructor { + name: pinned.name(), + }), + } +} diff --git a/libs/@local/hashql/mir/src/interpret/suspension/mod.rs b/libs/@local/hashql/mir/src/interpret/suspension/mod.rs new file mode 100644 index 00000000000..e86885d5343 --- /dev/null +++ b/libs/@local/hashql/mir/src/interpret/suspension/mod.rs @@ -0,0 +1,138 @@ +//! Suspension and continuation types for interpreter yield points. +//! +//! When the interpreter encounters an operation that requires external data +//! (such as a graph database query), it suspends execution and yields a +//! [`Suspension`] describing what it needs. The caller fulfills the request +//! and constructs a [`Continuation`] to resume interpretation. +//! +//! # Protocol +//! +//! 1. [`Runtime::start`] or [`Runtime::resume`] returns [`Yield::Suspension`] +//! 2. The caller inspects the [`Suspension`] variant to determine what is needed +//! 3. The caller fulfills the request and calls [`GraphReadSuspension::resolve`] to produce a +//! [`Continuation`] +//! 4. The caller passes the [`Continuation`] to [`Runtime::resume`] +//! +//! [`Runtime::start`]: super::runtime::Runtime::start +//! [`Runtime::resume`]: super::runtime::Runtime::resume +//! [`Yield::Suspension`]: super::runtime::Yield::Suspension + +mod graph_read; +mod temporal; + +use core::{alloc::Allocator, debug_assert_matches}; + +pub(crate) use self::graph_read::extract_axis; +pub use self::temporal::{TemporalAxesInterval, TemporalInterval, Timestamp}; +use super::{CallStack, RuntimeError, value::Value}; +use crate::{ + body::{basic_block::BasicBlockId, terminator::GraphRead}, + def::DefId, + interpret::runtime::CurrentBlock, +}; + +/// A request for external data that the interpreter cannot produce on its own. +/// +/// The caller must inspect the variant, fulfill the request, and pass +/// the result back via [`Runtime::resume`](super::runtime::Runtime::resume). +#[derive(Debug)] +pub enum Suspension<'ctx, 'heap> { + /// The interpreter needs the result of a graph read operation. + GraphRead(GraphReadSuspension<'ctx, 'heap>), +} + +/// Suspension state for a [`GraphRead`] terminator. +/// +/// Contains the MIR graph read definition and the evaluated temporal axis, +/// which together provide everything the caller needs to execute the query. +/// +/// Call [`resolve`](Self::resolve) with the query result to produce a +/// [`Continuation`] for resuming the interpreter. +#[derive(Debug)] +pub struct GraphReadSuspension<'ctx, 'heap> { + pub body: DefId, + pub block: BasicBlockId, + + /// The graph read terminator that caused the suspension. + pub read: &'ctx GraphRead<'heap>, + /// The evaluated temporal axis for the query. + pub axis: TemporalAxesInterval, +} + +impl<'ctx, 'heap> GraphReadSuspension<'ctx, 'heap> { + /// Resolves this suspension with the query result, producing a [`Continuation`]. + pub const fn resolve( + self, + value: Value<'heap, A>, + ) -> Continuation<'ctx, 'heap, A> { + Continuation::GraphRead(GraphReadContinuation { + read: self.read, + value, + }) + } +} + +/// The fulfilled result of a [`Suspension`], ready to be fed back into the +/// interpreter via [`Runtime::resume`](super::runtime::Runtime::resume). +pub enum Continuation<'ctx, 'heap, A: Allocator> { + /// Fulfilled result of a [`GraphRead`] suspension. + GraphRead(GraphReadContinuation<'ctx, 'heap, A>), +} + +impl<'ctx, 'heap, A: Allocator> Continuation<'ctx, 'heap, A> { + /// Applies a [`Continuation`] to the suspended call stack. + /// + /// Writes the continuation's result value into the target block's parameter + /// and advances the frame to that block. + /// + /// # Errors + /// + /// Returns [`RuntimeError::CallstackEmpty`] if the call stack has no frames. + pub fn apply( + self, + callstack: &mut CallStack<'ctx, 'heap, A>, + ) -> Result<(), RuntimeError<'heap, E, A>> { + match self { + Continuation::GraphRead(GraphReadContinuation { read, value }) => { + let Some(frame) = callstack.frames.last_mut() else { + return Err(RuntimeError::CallstackEmpty); + }; + + #[cfg(debug_assertions)] + { + use crate::body::terminator::TerminatorKind; + + let current_block = frame.current_block; + let current_statement = frame.current_statement; + debug_assert_eq!(current_block.block.statements.len(), current_statement); + + debug_assert_matches!( + current_block.block.terminator.kind, + TerminatorKind::GraphRead(_) + ); + } + + let next_block = &frame.body.basic_blocks[read.target]; + let params = next_block.params; + debug_assert_eq!(params.len(), 1); + + frame.locals.insert(params[0], value); + + frame.current_block = CurrentBlock { + id: read.target, + block: next_block, + }; + frame.current_statement = 0; + + Ok(()) + } + } + } +} + +/// Carries the result of a graph read query back to the interpreter. +#[expect(clippy::field_scoped_visibility_modifiers)] +pub struct GraphReadContinuation<'ctx, 'heap, A: Allocator> { + pub(crate) read: &'ctx GraphRead<'heap>, + pub(crate) value: Value<'heap, A>, +} diff --git a/libs/@local/hashql/mir/src/interpret/suspension/temporal.rs b/libs/@local/hashql/mir/src/interpret/suspension/temporal.rs new file mode 100644 index 00000000000..5a9f27e6659 --- /dev/null +++ b/libs/@local/hashql/mir/src/interpret/suspension/temporal.rs @@ -0,0 +1,63 @@ +//! Temporal types for bi-temporal graph queries. +//! +//! These types represent evaluated temporal axes that are extracted from +//! interpreter [`Value`]s during [`GraphRead`] suspension. They provide +//! a concrete, backend-agnostic representation of the temporal context +//! needed to execute a graph query. +//! +//! [`Value`]: crate::interpret::value::Value +//! [`GraphRead`]: crate::body::terminator::GraphRead + +use core::ops::Bound; + +use crate::interpret::value::Int; + +/// An evaluated timestamp value, in milliseconds since the Unix epoch. +#[derive(Debug, Copy, Clone)] +pub struct Timestamp(Int); + +impl From for Timestamp { + fn from(value: Int) -> Self { + Self(value) + } +} + +impl From for Int { + fn from(value: Timestamp) -> Self { + value.0 + } +} + +/// A half-open or closed interval over [`Timestamp`]s. +#[derive(Debug, Clone)] +pub struct TemporalInterval { + pub start: Bound, + pub end: Bound, +} + +impl TemporalInterval { + /// Creates a point interval `[value, value]`. + pub(crate) const fn point(value: Timestamp) -> Self { + Self { + start: Bound::Included(value), + end: Bound::Included(value), + } + } + + /// Creates an interval from explicit bounds. + pub(crate) const fn interval((start, end): (Bound, Bound)) -> Self { + Self { start, end } + } +} + +/// The evaluated temporal axes for a bi-temporal graph query. +/// +/// HashQL's graph store is bi-temporal: every fact is tracked along both +/// a decision time axis (when the fact was decided) and a transaction time +/// axis (when it was recorded). A query must specify intervals on both axes +/// to determine which version of the data is visible. +#[derive(Debug, Clone)] +pub struct TemporalAxesInterval { + pub decision_time: TemporalInterval, + pub transaction_time: TemporalInterval, +} diff --git a/libs/@local/hashql/mir/src/interpret/tests.rs b/libs/@local/hashql/mir/src/interpret/tests.rs index d902a107b1e..263b08ad8ed 100644 --- a/libs/@local/hashql/mir/src/interpret/tests.rs +++ b/libs/@local/hashql/mir/src/interpret/tests.rs @@ -14,18 +14,22 @@ clippy::similar_names )] +use alloc::rc::Rc; +use core::{assert_matches, ops::ControlFlow}; + use hashql_core::{ - collections::FastHashMap, - heap::{FromIteratorIn as _, Heap}, + heap::{self, FromIteratorIn as _, Heap}, id::{Id as _, IdVec}, - symbol::Symbol, - r#type::{TypeId, environment::Environment}, + symbol::sym, + r#type::{TypeBuilder, TypeId, environment::Environment}, }; use super::{ - CallStack, Runtime, RuntimeConfig, + CallStack, Inputs, Runtime, RuntimeConfig, error::InterpretDiagnostic, - value::{Int, Num, Value}, + runtime::Yield, + suspension::Suspension, + value::{Int, Num, Opaque, Struct, Value}, }; use crate::{ body::{ @@ -33,29 +37,32 @@ use crate::{ constant::Constant, operand::Operand, rvalue::{Aggregate, AggregateKind, RValue}, + terminator::{GraphRead, GraphReadHead, GraphReadTail, TerminatorKind}, }, builder::{BodyBuilder, body}, def::{DefId, DefIdSlice}, intern::Interner, interpret::error::InterpretDiagnosticCategory, + op, }; fn run_body(body: Body<'_>) -> Result, InterpretDiagnostic> { - run_body_with_inputs(body, FastHashMap::default()) + run_body_with_inputs(body, Inputs::new()) } +#[expect(clippy::needless_pass_by_value)] fn run_body_with_inputs<'heap>( body: Body<'heap>, - inputs: FastHashMap, Value<'heap>>, + inputs: Inputs<'heap>, ) -> Result, InterpretDiagnostic> { assert_eq!(body.id, DefId::new(0)); let bodies = [body]; let bodies = DefIdSlice::from_raw(&bodies); - let mut runtime = Runtime::new(RuntimeConfig::default(), bodies, inputs); + let mut runtime = Runtime::new(RuntimeConfig::default(), bodies, &inputs); let callstack = CallStack::new(&runtime, DefId::new(0), []); - runtime.run(callstack) + runtime.run(callstack, |_| unreachable!()) } fn run_bodies<'heap>( @@ -63,10 +70,11 @@ fn run_bodies<'heap>( entry: DefId, args: impl IntoIterator, IntoIter: ExactSizeIterator>, ) -> Result, InterpretDiagnostic> { - let mut runtime = Runtime::new(RuntimeConfig::default(), bodies, FastHashMap::default()); + let inputs = Inputs::default(); + let mut runtime = Runtime::new(RuntimeConfig::default(), bodies, &inputs); let callstack = CallStack::new(&runtime, entry, args); - runtime.run(callstack) + runtime.run(callstack, |_| unreachable!()) } // ============================================================================= @@ -164,14 +172,17 @@ fn entry_function_with_args() { let bodies = [body]; let bodies = DefIdSlice::from_raw(&bodies); - let mut runtime = Runtime::new(RuntimeConfig::default(), bodies, FastHashMap::default()); + let inputs = Inputs::default(); + let mut runtime = Runtime::new(RuntimeConfig::default(), bodies, &inputs); let args = [ Value::Integer(Int::from(10_i128)), Value::Integer(Int::from(20_i128)), ]; let callstack = CallStack::new(&runtime, DefId::new(0), args); - let result = runtime.run(callstack).expect("should succeed"); + let result = runtime + .run(callstack, |_| unreachable!()) + .expect("should succeed"); assert_eq!(result, Value::Integer(Int::from(true))); } @@ -976,6 +987,80 @@ fn struct_projection() { assert_eq!(result, Value::Integer(Int::from(200_i128))); } +#[test] +fn opaque_struct_projection_by_name() { + use hashql_core::symbol::sym; + + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + let body = body!(interner, env; fn@0/0 -> Int { + decl inner: (x: Int, y: Int), wrapped: [Opaque sym::path::Entity; ?], result: Int; + @proj y_field = wrapped.y: Int; + + bb0() { + inner = struct x: 100, y: 200; + wrapped = opaque (sym::path::Entity), inner; + result = load y_field; + return result; + } + }); + + let result = run_body(body).expect("should succeed"); + assert_eq!(result, Value::Integer(Int::from(200_i128))); +} + +#[test] +fn opaque_tuple_projection_by_index() { + use hashql_core::symbol::sym; + + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + let body = body!(interner, env; fn@0/0 -> Int { + decl inner: (Int, Int), wrapped: [Opaque sym::path::Entity; ?], result: Int; + @proj second = wrapped.1: Int; + + bb0() { + inner = tuple 10, 20; + wrapped = opaque (sym::path::Entity), inner; + result = load second; + return result; + } + }); + + let result = run_body(body).expect("should succeed"); + assert_eq!(result, Value::Integer(Int::from(20_i128))); +} + +#[test] +fn nested_opaque_projection_by_name() { + use hashql_core::symbol::sym; + + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + let body = body!(interner, env; fn@0/0 -> Int { + decl inner: (val: Int), mid: [Opaque sym::path::EntityId; ?], + outer: [Opaque sym::path::Entity; ?], result: Int; + @proj val_field = outer.val: Int; + + bb0() { + inner = struct val: 42; + mid = opaque (sym::path::EntityId), inner; + outer = opaque (sym::path::Entity), mid; + result = load val_field; + return result; + } + }); + + let result = run_body(body).expect("should succeed"); + assert_eq!(result, Value::Integer(Int::from(42_i128))); +} + // ============================================================================= // Input Operations // ============================================================================= @@ -995,7 +1080,7 @@ fn input_load_returns_value() { } }); - let mut inputs = FastHashMap::default(); + let mut inputs = Inputs::default(); inputs.insert( heap.intern_symbol("my_input"), Value::Integer(Int::from(999_i128)), @@ -1020,7 +1105,7 @@ fn input_exists_returns_true() { } }); - let mut inputs = FastHashMap::default(); + let mut inputs = Inputs::default(); inputs.insert( heap.intern_symbol("my_input"), Value::Integer(Int::from(1_i128)), @@ -1094,13 +1179,14 @@ fn recursion_limit_exceeded() { let bodies = [body]; let bodies = DefIdSlice::from_raw(&bodies); + let inputs = Inputs::default(); let config = RuntimeConfig { recursion_limit: 5 }; - let mut runtime = Runtime::new(config, bodies, FastHashMap::default()); + let mut runtime = Runtime::new(config, bodies, &inputs); let callstack = CallStack::new(&runtime, DefId::new(0), []); let result = runtime - .run(callstack) + .run(callstack, |_| unreachable!()) .expect_err("should fail with recursion limit"); assert_eq!(result.category, InterpretDiagnosticCategory::RuntimeLimit); } @@ -1595,3 +1681,560 @@ fn ice_struct_field_length_mismatch() { InterpretDiagnosticCategory::StructuralInvariant ); } + +// ============================================================================= +// Helpers for suspension tests +// ============================================================================= + +/// Constructs a minimal valid temporal axes value for `PinnedTransactionTimeTemporalAxes`. +/// +/// The structure mirrors the HashQL type system's temporal axes representation: +/// +/// ```text +/// Opaque(PinnedTransactionTimeTemporalAxes, +/// Struct { pinned, variable } +/// ) +/// ``` +/// +/// where `pinned` = `Opaque(TransactionTime, Opaque(Timestamp, Integer(pinned_ms)))` and +/// `variable` wraps an interval with inclusive start and unbounded end. +fn make_temporal_axes<'heap>( + interner: &Interner<'heap>, + pinned_ms: i128, + variable_start_ms: i128, +) -> Value<'heap> { + // Timestamp(Integer) + let pinned_timestamp = Value::Opaque(Opaque::new( + sym::path::Timestamp, + Rc::new(Value::Integer(Int::from(pinned_ms))), + )); + + // TransactionTime(Timestamp) + let pinned = Value::Opaque(Opaque::new( + sym::path::TransactionTime, + Rc::new(pinned_timestamp), + )); + + // Variable interval start: InclusiveTemporalBound(Timestamp(Integer)) + let start_timestamp = Value::Opaque(Opaque::new( + sym::path::Timestamp, + Rc::new(Value::Integer(Int::from(variable_start_ms))), + )); + let start_bound = Value::Opaque(Opaque::new( + sym::path::InclusiveTemporalBound, + Rc::new(start_timestamp), + )); + + // Variable interval end: UnboundedTemporalBound(Unit) + let end_bound = Value::Opaque(Opaque::new( + sym::path::UnboundedTemporalBound, + Rc::new(Value::Unit), + )); + + // 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])); + let interval = Value::Opaque(Opaque::new( + sym::path::Interval, + Rc::new(Value::Struct(interval_struct)), + )); + + // DecisionTime(Interval(...)) + let variable = Value::Opaque(Opaque::new(sym::path::DecisionTime, Rc::new(interval))); + + // 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])); + + Value::Opaque(Opaque::new( + sym::path::PinnedTransactionTimeTemporalAxes, + Rc::new(Value::Struct(axes_struct)), + )) +} + +/// Builds a body: `bb0` loads axis from input, `GraphRead → bb1`, `bb1` returns the result. +/// +/// Must be called with `DefId::new(0)` and an "axis" input containing a temporal axes value. +fn make_graph_read_body<'heap>( + heap: &'heap Heap, + interner: &Interner<'heap>, + env: &Environment<'heap>, +) -> Body<'heap> { + let int_ty = TypeBuilder::synthetic(env).integer(); + let mut builder = BodyBuilder::new(interner); + + let axis = builder.local("axis", int_ty); + let graph_result = builder.local("graph_result", int_ty); + + let bb0 = builder.reserve_block([]); + let bb1 = builder.reserve_block([graph_result.local]); + + builder + .build_block(bb0) + .assign_place(axis, |rv| { + rv.input( + hashql_hir::node::operation::InputOp::Load { required: true }, + "axis", + ) + }) + .finish_with_terminator(TerminatorKind::GraphRead(GraphRead { + head: GraphReadHead::Entity { + axis: Operand::Place(axis), + }, + body: heap::Vec::new_in(heap), + tail: GraphReadTail::Collect, + target: bb1, + })); + + builder.build_block(bb1).ret(graph_result); + + let mut body = builder.finish(0, int_ty); + body.id = DefId::new(0); + + body +} + +fn run_graph_read_body<'heap>( + heap: &'heap Heap, + interner: &Interner<'heap>, + env: &Environment<'heap>, + result_value: &Value<'heap>, +) -> Result, InterpretDiagnostic> { + let body = make_graph_read_body(heap, interner, env); + let axis_value = make_temporal_axes(interner, 1000, 500); + + let bodies = [body]; + let bodies = DefIdSlice::from_raw(&bodies); + + let mut inputs = Inputs::default(); + inputs.insert(heap.intern_symbol("axis"), axis_value); + + let mut runtime = Runtime::new(RuntimeConfig::default(), bodies, &inputs); + let callstack = CallStack::new(&runtime, DefId::new(0), []); + + runtime.run(callstack, |suspension| { + let Suspension::GraphRead(graph_read) = suspension; + Ok(graph_read.resolve(result_value.clone())) + }) +} + +// ============================================================================= +// Suspension / Continuation Protocol +// ============================================================================= + +#[test] +fn start_suspend_resume_return() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + let body = make_graph_read_body(&heap, &interner, &env); + let axis_value = make_temporal_axes(&interner, 1000, 500); + + let bodies = [body]; + let bodies = DefIdSlice::from_raw(&bodies); + + let mut inputs = Inputs::default(); + inputs.insert(heap.intern_symbol("axis"), axis_value); + + let mut runtime = Runtime::new(RuntimeConfig::default(), bodies, &inputs); + let mut callstack = CallStack::new(&runtime, DefId::new(0), []); + + // start → should suspend at the GraphRead + let result = runtime.start(&mut callstack).expect("start should succeed"); + let Yield::Suspension(Suspension::GraphRead(suspension)) = result else { + panic!("expected GraphRead suspension, got return"); + }; + + // Resolve with a value and resume + let continuation = suspension.resolve(Value::Integer(Int::from(42_i128))); + let result = runtime + .resume(&mut callstack, continuation) + .expect("resume should succeed"); + + let Yield::Return(value) = result else { + panic!("expected return after resume, got suspension"); + }; + assert_eq!(value, Value::Integer(Int::from(42_i128))); +} + +#[test] +fn run_with_suspension_handler() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + let result = run_graph_read_body(&heap, &interner, &env, &Value::Integer(Int::from(99_i128))) + .expect("should succeed"); + assert_eq!(result, Value::Integer(Int::from(99_i128))); +} + +#[test] +fn multi_suspension_round_trip() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + // Build a body with two sequential GraphReads: + // bb0: load axis, GraphRead → bb1 + // bb1: receive first result, GraphRead → bb2 + // bb2: receive second result, add first + second, return + let int_ty = TypeBuilder::synthetic(&env).integer(); + let mut builder = BodyBuilder::new(&interner); + + let axis = builder.local("axis", int_ty); + let first_result = builder.local("first_result", int_ty); + let second_result = builder.local("second_result", int_ty); + let sum = builder.local("sum", int_ty); + + let bb0 = builder.reserve_block([]); + let bb1 = builder.reserve_block([first_result.local]); + let bb2 = builder.reserve_block([second_result.local]); + + builder + .build_block(bb0) + .assign_place(axis, |rv| { + rv.input( + hashql_hir::node::operation::InputOp::Load { required: true }, + "axis", + ) + }) + .finish_with_terminator(TerminatorKind::GraphRead(GraphRead { + head: GraphReadHead::Entity { + axis: Operand::Place(axis), + }, + body: heap::Vec::new_in(&heap), + tail: GraphReadTail::Collect, + target: bb1, + })); + + builder + .build_block(bb1) + .finish_with_terminator(TerminatorKind::GraphRead(GraphRead { + head: GraphReadHead::Entity { + axis: Operand::Place(axis), + }, + body: heap::Vec::new_in(&heap), + tail: GraphReadTail::Collect, + target: bb2, + })); + + builder + .build_block(bb2) + .assign_place(sum, |rv| rv.binary(first_result, op![+], second_result)) + .ret(sum); + + let mut body = builder.finish(0, int_ty); + body.id = DefId::new(0); + + let bodies = [body]; + let bodies = DefIdSlice::from_raw(&bodies); + + let mut inputs = Inputs::default(); + inputs.insert( + heap.intern_symbol("axis"), + make_temporal_axes(&interner, 1000, 500), + ); + + let mut runtime = Runtime::new(RuntimeConfig::default(), bodies, &inputs); + let mut callstack = CallStack::new(&runtime, DefId::new(0), []); + + // First suspension + let result = runtime.start(&mut callstack).expect("start should succeed"); + let Yield::Suspension(Suspension::GraphRead(suspension)) = result else { + panic!("expected first GraphRead suspension"); + }; + let continuation = suspension.resolve(Value::Integer(Int::from(10_i128))); + + // Second suspension + let result = runtime + .resume(&mut callstack, continuation) + .expect("first resume should succeed"); + let Yield::Suspension(Suspension::GraphRead(suspension)) = result else { + panic!("expected second GraphRead suspension"); + }; + let continuation = suspension.resolve(Value::Integer(Int::from(32_i128))); + + // Final return: 10 + 32 = 42 + let result = runtime + .resume(&mut callstack, continuation) + .expect("second resume should succeed"); + let Yield::Return(value) = result else { + panic!("expected return after second resume"); + }; + assert_eq!(value, Value::Integer(Int::from(42_i128))); +} + +// ============================================================================= +// run_until_transition +// ============================================================================= + +#[test] +fn transition_breaks_at_target_block() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + // bb0 → bb1 → bb2 (return) + // Transition fires on bb1 (continue returns false). + let body = body!(interner, env; fn@0/0 -> Int { + decl x: Int; + + bb0() { + goto bb1(); + }, + bb1() { + goto bb2(42); + }, + bb2(x) { + return x; + } + }); + + let bodies = [body]; + let bodies = DefIdSlice::from_raw(&bodies); + let inputs = Inputs::default(); + + let bb1 = crate::body::basic_block::BasicBlockId::new(1); + + let mut runtime = Runtime::new(RuntimeConfig::default(), bodies, &inputs); + let mut callstack = CallStack::new(&runtime, DefId::new(0), []); + runtime.reset(); + + let result = runtime.run_until_transition::(&mut callstack, |block| block != bb1); + assert_matches!(result, Ok(ControlFlow::Break(_))); + + // Callstack should be positioned at bb1 + let current = callstack + .current_block::<()>() + .expect("callstack should not be empty"); + assert_eq!(current, bb1); +} + +#[test] +fn transition_runs_to_completion_when_continue_always_true() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + let body = body!(interner, env; fn@0/0 -> Int { + decl; + + bb0() { + goto bb1(); + }, + bb1() { + return 42; + } + }); + + let bodies = [body]; + let bodies = DefIdSlice::from_raw(&bodies); + let inputs = Inputs::default(); + + let mut runtime = Runtime::new(RuntimeConfig::default(), bodies, &inputs); + let mut callstack = CallStack::new(&runtime, DefId::new(0), []); + runtime.reset(); + + let result = runtime.run_until_transition::(&mut callstack, |_| true); + assert_matches!(result, Ok(ControlFlow::Continue(Yield::Return(value))) if value == Value::Integer(Int::from(42_i128))); +} + +#[test] +fn transition_fires_on_reentry_after_continuation_apply() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + // bb0: load axis, GraphRead → bb1 + // bb1: return result + // Transition fires on bb1 (continue returns false on bb1). + let int_ty = TypeBuilder::synthetic(&env).integer(); + let mut builder = BodyBuilder::new(&interner); + + let axis = builder.local("axis", int_ty); + let graph_result = builder.local("graph_result", int_ty); + + let bb0 = builder.reserve_block([]); + let bb1 = builder.reserve_block([graph_result.local]); + + let bb1_id = bb1; + + builder + .build_block(bb0) + .assign_place(axis, |rv| { + rv.input( + hashql_hir::node::operation::InputOp::Load { required: true }, + "axis", + ) + }) + .finish_with_terminator(TerminatorKind::GraphRead(GraphRead { + head: GraphReadHead::Entity { + axis: Operand::Place(axis), + }, + body: heap::Vec::new_in(&heap), + tail: GraphReadTail::Collect, + target: bb1, + })); + + builder.build_block(bb1).ret(graph_result); + + let mut body = builder.finish(0, int_ty); + body.id = DefId::new(0); + + let bodies = [body]; + let bodies = DefIdSlice::from_raw(&bodies); + + let mut inputs = Inputs::default(); + inputs.insert( + heap.intern_symbol("axis"), + make_temporal_axes(&interner, 1000, 500), + ); + + let mut runtime = Runtime::new(RuntimeConfig::default(), bodies, &inputs); + let mut callstack = CallStack::new(&runtime, DefId::new(0), []); + runtime.reset(); + + // First call: should suspend at GraphRead (bb0 is allowed) + let result = runtime.run_until_transition::(&mut callstack, |block| block != bb1_id); + let Ok(ControlFlow::Continue(Yield::Suspension(Suspension::GraphRead(suspension)))) = result + else { + panic!("expected suspension, got {result:?}"); + }; + + // Apply continuation → sets current block to bb1 + let continuation = suspension.resolve(Value::Integer(Int::from(42_i128))); + continuation + .apply::(&mut callstack) + .expect("apply should succeed"); + + // Second call: transition should fire immediately on bb1 (before stepping) + let result = runtime.run_until_transition::(&mut callstack, |block| block != bb1_id); + assert_matches!(result, Ok(ControlFlow::Break(_))); + + let current = callstack + .current_block::<()>() + .expect("callstack should not be empty"); + assert_eq!(current, bb1_id); +} + +// ============================================================================= +// CallStack edge cases +// ============================================================================= + +#[test] +fn unwind_produces_correct_frames() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + // main (DefId 0) calls inner (DefId 1), inner triggers an error. + let inner_id = DefId::new(1); + + let main = body!(interner, env; fn@0/0 -> Int { + decl result: Int; + + bb0() { + result = apply inner_id; + return result; + } + }); + + let inner = body!(interner, env; fn@inner_id/0 -> Int { + decl x: Int; + + bb0() { + return x; + } + }); + + let result = run_bodies(DefIdSlice::from_raw(&[main, inner]), DefId::new(0), []); + let error = result.expect_err("should fail with uninitialized local"); + + // The error should include stack trace info (manifested as labels in the diagnostic) + assert_eq!(error.category, InterpretDiagnosticCategory::LocalAccess); + // Primary label from inner + secondary "called from here" from main = at least 2 labels + assert!( + error.labels.len() >= 2, + "expected at least 2 labels (error site + call site), got {}", + error.labels.len() + ); +} + +#[test] +fn block_param_aliasing_swap() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + // goto bb1(b, a) where bb1 params are (a, b) + // Without the scratch-based staging, naive sequential assignment would clobber. + let body = body!(interner, env; fn@0/0 -> Int { + decl a: Int, b: Int, result: Int; + + bb0() { + a = load 1; + b = load 2; + goto bb1(b, a); + }, + bb1(a, b) { + result = bin.- a b; + return result; + } + }); + + let result = run_body(body).expect("should succeed"); + // After swap: a=2, b=1. result = 2 - 1 = 1. + assert_eq!(result, Value::Integer(Int::from(1_i128))); +} + +// ============================================================================= +// Minor gaps +// ============================================================================= + +#[test] +fn unary_neg_number() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + let body = body!(interner, env; fn@0/0 -> Num { + decl result: Num; + + bb0() { + result = un.neg 3.5; + return result; + } + }); + + let result = run_body(body).expect("should succeed"); + assert_eq!(result, Value::Number(Num::from(-3.5))); +} + +#[test] +fn callstack_new_in_runs_to_completion() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + let body = body!(interner, env; fn@0/0 -> Int { + decl; + + bb0() { + return 77; + } + }); + + let bodies = [body]; + let bodies = DefIdSlice::from_raw(&bodies); + let inputs = Inputs::default(); + + let mut runtime = Runtime::new(RuntimeConfig::default(), bodies, &inputs); + let callstack = CallStack::new_in::<()>(&bodies[DefId::new(0)], [], alloc::alloc::Global) + .expect("new_in should succeed"); + + let result = runtime + .run(callstack, |_| unreachable!()) + .expect("should succeed"); + assert_eq!(result, Value::Integer(Int::from(77_i128))); +} diff --git a/libs/@local/hashql/mir/src/interpret/value/int.rs b/libs/@local/hashql/mir/src/interpret/value/int.rs index 0e04839a950..0e7a47565b7 100644 --- a/libs/@local/hashql/mir/src/interpret/value/int.rs +++ b/libs/@local/hashql/mir/src/interpret/value/int.rs @@ -1,8 +1,30 @@ +//! Finite-precision integer constants for the HashQL MIR. +//! +//! [`Int`] represents compile-time integer and boolean values with size tracking. +//! Values carry their bit-width: 1 bit for booleans, 128 bits for integers. +//! This allows serialization to distinguish `true`/`false` from `0`/`1` without +//! external type information — critical for round-tripping through formats like jsonb +//! that have distinct boolean and number representations. +//! +//! # Size Invariants +//! +//! Only two sizes are valid: +//! - **1 bit**: boolean values (`0` or `1`) +//! - **128 bits**: integer values (full [`i128`] range) +//! +//! # Arithmetic Promotion +//! +//! All arithmetic operations produce 128-bit results, even when both operands are booleans. +//! Bitwise boolean operations (`BitAnd`, `BitOr`, `BitXor`) preserve the 1-bit size when +//! both operands are booleans. + use core::{ + cmp, debug_assert_matches, error::Error, fmt::{self, Display}, + hash::{Hash, Hasher}, hint, - num::TryFromIntError, + num::{NonZero, TryFromIntError}, ops::{Add, BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Neg, Not, Sub}, }; @@ -13,442 +35,235 @@ use crate::{ macros::{forward_ref_binop, forward_ref_op_assign, forward_ref_unop}, }; +/// Bit-width for boolean values. +const BOOL_BITS: NonZero = NonZero::new(1).unwrap(); + +/// Bit-width for integer values. +const INT_BITS: NonZero = NonZero::new(128).unwrap(); + /// A finite-precision integer constant in the MIR. /// -/// Unlike Rust, HashQL cannot differentiate between signed and unsigned integers at the type -/// level, so all values are stored as signed [`i128`]. -/// -/// # Conversion Methods -/// -/// **Range-checked conversions** (`as_i8`, `as_u8`, `as_i16`, etc.) return [`Some`] only if -/// the value fits in the target type's range. -/// -/// **Unchecked conversions** (`as_int`, `as_uint`) return the raw value without range checks. +/// Stores an [`i128`] value alongside its bit-width. The width distinguishes booleans +/// (1 bit, values `0` or `1`) from integers (128 bits, full [`i128`] range). /// /// # Examples /// /// ``` /// use hashql_mir::interpret::value::Int; /// -/// // Values that fit in the target range succeed -/// let small = Int::from(42_i64); -/// assert_eq!(small.as_i8(), Some(42)); -/// assert_eq!(small.as_i16(), Some(42)); +/// // Booleans are 1-bit integers +/// let t = Int::from(true); +/// assert_eq!(t.size(), 1); +/// assert_eq!(t.as_bool(), Some(true)); /// -/// // Values outside the target range return None -/// let large = Int::from(1000_i64); -/// assert_eq!(large.as_i8(), None); // 1000 > i8::MAX -/// assert_eq!(large.as_i16(), Some(1000)); +/// // Integers are 128-bit +/// let n = Int::from(42_i64); +/// assert_eq!(n.size(), 128); +/// assert_eq!(n.as_int(), 42); /// -/// // Unsigned conversions require non-negative values -/// let negative = Int::from(-1_i8); -/// assert_eq!(negative.as_i8(), Some(-1)); -/// assert_eq!(negative.as_u8(), None); -/// -/// // Raw value access always succeeds -/// assert_eq!(large.as_int(), 1000); +/// // Bool provenance is metadata, not identity: from(true) == from(1) +/// assert_eq!(Int::from(true), Int::from(1_i32)); /// ``` -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +// Uses `#[repr(packed)]` to avoid alignment padding, which would duplicate size, same as +// rust-lang's ScalarInt. +#[derive(Copy, Clone)] +#[repr(Rust, packed)] pub struct Int { + /// The raw integer value. + /// + /// For booleans (size == 1), only `0` and `1` are valid. + /// For integers (size == 128), any `i128` value is valid. value: i128, + + /// Bit-width of the value: `1` for booleans, `128` for integers. + size: NonZero, } -#[expect( - clippy::cast_possible_truncation, - clippy::cast_precision_loss, - clippy::cast_sign_loss -)] impl Int { - #[inline] - const fn from_value_unchecked(value: i128) -> Self { - Self { value } - } + /// Boolean constant `false`. + pub const FALSE: Self = Self { + value: 0, + size: BOOL_BITS, + }; + /// Integer constant `1`. + pub const ONE: Self = Self { + value: 1, + size: INT_BITS, + }; + /// Boolean constant `true`. + pub const TRUE: Self = Self { + value: 1, + size: BOOL_BITS, + }; + /// Integer constant `0`. + pub const ZERO: Self = Self { + value: 0, + size: INT_BITS, + }; - /// Converts this integer to a boolean if the value is 0 or 1. - /// - /// Returns `Some(false)` for 0, `Some(true)` for 1, or [`None`] for any other value. - /// - /// # Examples - /// - /// ``` - /// use hashql_mir::interpret::value::Int; - /// - /// assert_eq!(Int::from(true).as_bool(), Some(true)); - /// assert_eq!(Int::from(false).as_bool(), Some(false)); - /// assert_eq!(Int::from(1_i32).as_bool(), Some(true)); - /// assert_eq!(Int::from(0_i64).as_bool(), Some(false)); - /// - /// // Values other than 0 or 1 return None - /// assert_eq!(Int::from(2_i8).as_bool(), None); - /// assert_eq!(Int::from(-1_i8).as_bool(), None); - /// ``` + /// Creates a boolean `Int` from a `bool`. #[inline] - #[must_use] - pub const fn as_bool(self) -> Option { - match self.value { - 0 => Some(false), - 1 => Some(true), - _ => None, + const fn from_bool(value: bool) -> Self { + Self { + value: value as i128, + size: BOOL_BITS, } } - /// Converts this integer to [`i8`] if the value fits in the range `-128..=127`. - /// - /// # Examples - /// - /// ``` - /// use hashql_mir::interpret::value::Int; - /// - /// assert_eq!(Int::from(42_i8).as_i8(), Some(42)); - /// assert_eq!(Int::from(42_i64).as_i8(), Some(42)); - /// assert_eq!(Int::from(-128_i32).as_i8(), Some(-128)); - /// assert_eq!(Int::from(127_u8).as_i8(), Some(127)); - /// - /// // Value out of i8 range returns None - /// assert_eq!(Int::from(128_i32).as_i8(), None); - /// assert_eq!(Int::from(-129_i32).as_i8(), None); - /// ``` + /// Creates a 128-bit integer `Int` from an `i128`. #[inline] - #[must_use] - pub const fn as_i8(self) -> Option { - if self.value >= i8::MIN as i128 && self.value <= i8::MAX as i128 { - Some(self.value as i8) - } else { - None + const fn from_i128(value: i128) -> Self { + Self { + value, + size: INT_BITS, } } - /// Converts this integer to [`u8`] if the value fits in the range `0..=255`. + /// Validates the internal invariants in debug builds. /// - /// # Examples - /// - /// ``` - /// use hashql_mir::interpret::value::Int; - /// - /// assert_eq!(Int::from(42_i8).as_u8(), Some(42)); - /// assert_eq!(Int::from(255_u8).as_u8(), Some(255)); - /// assert_eq!(Int::from(200_i64).as_u8(), Some(200)); - /// - /// // Negative or too large values return None - /// assert_eq!(Int::from(-1_i8).as_u8(), None); - /// assert_eq!(Int::from(256_i32).as_u8(), None); - /// ``` - #[inline] - #[must_use] - pub const fn as_u8(self) -> Option { - if self.value >= 0 && self.value <= u8::MAX as i128 { - Some(self.value as u8) - } else { - None - } - } + /// - `size` must be 1 or 128 + /// - If `size == 1`, value must be 0 or 1 + #[expect( + clippy::inline_always, + reason = "mirrors rustc's check_data pattern — cheap assertion, always inlined" + )] + #[inline(always)] + fn check_data(self) { + let value = self.value; + let size = self.size.get(); - /// Converts this integer to [`i16`] if the value fits in the range `-32768..=32767`. - /// - /// # Examples - /// - /// ``` - /// use hashql_mir::interpret::value::Int; - /// - /// assert_eq!(Int::from(1000_i16).as_i16(), Some(1000)); - /// assert_eq!(Int::from(1000_i64).as_i16(), Some(1000)); - /// assert_eq!(Int::from(-1000_i32).as_i16(), Some(-1000)); - /// - /// // Value out of i16 range returns None - /// assert_eq!(Int::from(40000_i64).as_i16(), None); - /// ``` - #[inline] - #[must_use] - pub const fn as_i16(self) -> Option { - if self.value >= i16::MIN as i128 && self.value <= i16::MAX as i128 { - Some(self.value as i16) - } else { - None - } + debug_assert_matches!(size, 1 | 128, "Int size must be 1 or 128, got {size}"); + debug_assert!( + size == 128 || matches!(value, 0 | 1), + "Bool Int must have value 0 or 1, got {value}" + ); } - /// Converts this integer to [`u16`] if the value fits in the range `0..=65535`. - /// - /// # Examples - /// - /// ``` - /// use hashql_mir::interpret::value::Int; - /// - /// assert_eq!(Int::from(1000_i16).as_u16(), Some(1000)); - /// assert_eq!(Int::from(65535_u16).as_u16(), Some(65535)); - /// assert_eq!(Int::from(50000_i64).as_u16(), Some(50000)); - /// - /// // Negative or too large values return None - /// assert_eq!(Int::from(-1_i16).as_u16(), None); - /// assert_eq!(Int::from(70000_i64).as_u16(), None); - /// ``` + /// Returns the bit-width of this value: `1` for booleans, `128` for integers. #[inline] #[must_use] - pub const fn as_u16(self) -> Option { - if self.value >= 0 && self.value <= u16::MAX as i128 { - Some(self.value as u16) - } else { - None - } + pub const fn size(self) -> u8 { + self.size.get() } - /// Converts this integer to [`i32`] if the value fits in the [`i32`] range. - /// - /// # Examples - /// - /// ``` - /// use hashql_mir::interpret::value::Int; - /// - /// assert_eq!(Int::from(100_000_i32).as_i32(), Some(100_000)); - /// assert_eq!(Int::from(100_000_i64).as_i32(), Some(100_000)); - /// assert_eq!(Int::from(-100_000_i64).as_i32(), Some(-100_000)); - /// - /// // Value out of i32 range returns None - /// assert_eq!(Int::from(3_000_000_000_i64).as_i32(), None); - /// ``` + /// Returns `true` if this value has boolean width (1 bit). #[inline] #[must_use] - pub const fn as_i32(self) -> Option { - if self.value >= i32::MIN as i128 && self.value <= i32::MAX as i128 { - Some(self.value as i32) - } else { - None - } + pub const fn is_bool(self) -> bool { + self.size.get() == 1 } - /// Converts this integer to [`u32`] if the value fits in the [`u32`] range. + /// Converts this value to a `bool` if it has boolean width. /// - /// # Examples - /// - /// ``` - /// use hashql_mir::interpret::value::Int; - /// - /// assert_eq!(Int::from(100_000_i32).as_u32(), Some(100_000)); - /// assert_eq!(Int::from(3_000_000_000_u32).as_u32(), Some(3_000_000_000)); - /// assert_eq!(Int::from(100_000_i64).as_u32(), Some(100_000)); - /// - /// // Negative or too large values return None - /// assert_eq!(Int::from(-1_i32).as_u32(), None); - /// assert_eq!(Int::from(5_000_000_000_i64).as_u32(), None); - /// ``` - #[inline] - #[must_use] - pub const fn as_u32(self) -> Option { - if self.value >= 0 && self.value <= u32::MAX as i128 { - Some(self.value as u32) - } else { - None - } - } - - /// Converts this integer to [`i64`] if the value fits in the [`i64`] range. + /// Returns `None` for 128-bit integers, even if the value is 0 or 1. /// /// # Examples /// /// ``` /// use hashql_mir::interpret::value::Int; /// - /// assert_eq!(Int::from(10_000_000_000_i64).as_i64(), Some(10_000_000_000)); - /// assert_eq!( - /// Int::from(-10_000_000_000_i64).as_i64(), - /// Some(-10_000_000_000) - /// ); - /// assert_eq!(Int::from(100_i32).as_i64(), Some(100)); + /// assert_eq!(Int::from(true).as_bool(), Some(true)); + /// assert_eq!(Int::from(false).as_bool(), Some(false)); /// - /// // Value out of i64 range returns None - /// assert_eq!(Int::from(10_000_000_000_000_000_000_u64).as_i64(), None); + /// // Integer 1 is NOT a bool — different size + /// assert_eq!(Int::from(1_i32).as_bool(), None); /// ``` #[inline] #[must_use] - pub const fn as_i64(self) -> Option { - if self.value >= i64::MIN as i128 && self.value <= i64::MAX as i128 { - Some(self.value as i64) - } else { - None + pub const fn as_bool(self) -> Option { + if !self.is_bool() { + return None; } - } - /// Converts this integer to [`u64`] if the value fits in the [`u64`] range. - /// - /// # Examples - /// - /// ``` - /// use hashql_mir::interpret::value::Int; - /// - /// assert_eq!(Int::from(10_000_000_000_i64).as_u64(), Some(10_000_000_000)); - /// assert_eq!(Int::from(100_i32).as_u64(), Some(100)); - /// - /// // Negative or too large values return None - /// assert_eq!(Int::from(-1_i64).as_u64(), None); - /// ``` - #[inline] - #[must_use] - pub const fn as_u64(self) -> Option { - if self.value >= 0 && self.value <= u64::MAX as i128 { - Some(self.value as u64) - } else { - None + match self.value { + 0 => Some(false), + 1 => Some(true), + _ => { + // The check_data invariant guarantees boolean values are 0 or 1. This branch is + // unreachable in valid programs. + unreachable!() + } } } - /// Returns the value as [`i128`]. + /// Returns the value as a signed `i128`. /// - /// This always succeeds since the internal representation is [`i128`]. + /// For booleans, returns `0` or `1`. For integers, returns the raw value. + /// This always succeeds regardless of the bit-width. /// /// # Examples /// /// ``` /// use hashql_mir::interpret::value::Int; /// - /// assert_eq!(Int::from(i128::MAX).as_i128(), i128::MAX); - /// assert_eq!(Int::from(i128::MIN).as_i128(), i128::MIN); - /// assert_eq!(Int::from(42_i8).as_i128(), 42); + /// assert_eq!(Int::from(42_i64).as_int(), 42); + /// assert_eq!(Int::from(-1_i128).as_int(), -1); + /// assert_eq!(Int::from(true).as_int(), 1); /// ``` #[inline] #[must_use] - pub const fn as_i128(self) -> i128 { + pub const fn as_int(self) -> i128 { self.value } - /// Converts this integer to [`u128`] if the value is non-negative. + /// Returns the value reinterpreted as unsigned `u128`. /// - /// # Examples - /// - /// ``` - /// use hashql_mir::interpret::value::Int; - /// - /// assert_eq!(Int::from(i128::MAX).as_u128(), Some(i128::MAX as u128)); - /// assert_eq!(Int::from(42_i8).as_u128(), Some(42)); - /// - /// // Negative values return None - /// assert_eq!(Int::from(-1_i128).as_u128(), None); - /// ``` - #[inline] - #[must_use] - pub const fn as_u128(self) -> Option { - if self.value >= 0 { - Some(self.value as u128) - } else { - None - } - } - - /// Converts this integer to [`isize`] if the value fits in the platform's [`isize`] range. + /// For booleans, returns `0` or `1`. For integers, performs a two's complement + /// bit-cast (negative values wrap to large unsigned values). /// /// # Examples /// /// ``` /// use hashql_mir::interpret::value::Int; /// - /// assert_eq!(Int::from(42_isize).as_isize(), Some(42)); - /// assert_eq!(Int::from(-42_i32).as_isize(), Some(-42)); - /// assert_eq!(Int::from(1000_i64).as_isize(), Some(1000)); + /// assert_eq!(Int::from(42_i64).as_uint(), 42); + /// assert_eq!(Int::from(true).as_uint(), 1); + /// assert_eq!(Int::from(-1_i128).as_uint(), u128::MAX); /// ``` #[inline] #[must_use] - pub const fn as_isize(self) -> Option { - if self.value >= isize::MIN as i128 && self.value <= isize::MAX as i128 { - Some(self.value as isize) - } else { - None - } + #[expect( + clippy::cast_sign_loss, + reason = "intentional two's complement reinterpretation" + )] + pub const fn as_uint(self) -> u128 { + self.as_int() as u128 } - /// Converts this integer to [`usize`] if the value fits in the platform's [`usize`] range. - /// - /// # Examples - /// - /// ``` - /// use hashql_mir::interpret::value::Int; - /// - /// assert_eq!(Int::from(42_usize).as_usize(), Some(42)); - /// assert_eq!(Int::from(1000_i64).as_usize(), Some(1000)); + /// Checked integer addition. Returns `None` on overflow. /// - /// // Negative values return None - /// assert_eq!(Int::from(-1_isize).as_usize(), None); - /// ``` + /// Always produces a 128-bit result (arithmetic promotes booleans). #[inline] #[must_use] - pub const fn as_usize(self) -> Option { - if self.value >= 0 && self.value <= usize::MAX as i128 { - Some(self.value as usize) - } else { - None + pub const fn checked_add(self, rhs: Self) -> Option { + match self.as_int().checked_add(rhs.as_int()) { + Some(result) => Some(Self::from_i128(result)), + None => None, } } - /// Returns the raw signed value. - /// - /// This always succeeds and returns the internal [`i128`] representation directly. - /// - /// # Examples - /// - /// ``` - /// use hashql_mir::interpret::value::Int; + /// Checked integer subtraction. Returns `None` on overflow. /// - /// assert_eq!(Int::from(42_i8).as_int(), 42); - /// assert_eq!(Int::from(-1_i64).as_int(), -1); - /// assert_eq!(Int::from(i128::MAX).as_int(), i128::MAX); - /// ``` + /// Always produces a 128-bit result (arithmetic promotes booleans). #[inline] #[must_use] - pub const fn as_int(self) -> i128 { - self.value - } - - /// Returns the raw value reinterpreted as unsigned. - /// - /// This performs a direct bit-cast from [`i128`] to [`u128`], preserving the - /// two's complement representation. For negative values, this produces the - /// corresponding unsigned value with the sign bit set. - /// - /// This is primarily useful for operations like [`SwitchInt`] that work with - /// unsigned discriminant values. - /// - /// [`SwitchInt`]: crate::body::terminator::SwitchInt - /// - /// # Sign Overflow Behavior - /// - /// Negative signed values wrap around to large unsigned values: - /// - `-1_i8` becomes `u128::MAX` (all bits set) - /// - `-128_i8` becomes `u128::MAX - 127` - /// - /// This is intentional and matches Rust's `as` cast semantics for signed-to-unsigned - /// conversions. - /// - /// # Examples - /// - /// ``` - /// use hashql_mir::interpret::value::Int; - /// - /// // Positive values convert directly - /// assert_eq!(Int::from(42_i8).as_uint(), 42); - /// - /// // Negative values wrap (two's complement) - /// assert_eq!(Int::from(-1_i8).as_uint(), u128::MAX); - /// assert_eq!(Int::from(-1_i128).as_uint(), u128::MAX); - /// ``` - #[inline] - #[must_use] - pub const fn as_uint(self) -> u128 { - self.value as u128 + pub const fn checked_sub(self, rhs: Self) -> Option { + match self.as_int().checked_sub(rhs.as_int()) { + Some(result) => Some(Self::from_i128(result)), + None => None, + } } /// Converts this integer to [`f32`]. /// /// This may lose precision for values that cannot be exactly represented /// as a 32-bit floating point number. - /// - /// # Examples - /// - /// ``` - /// use hashql_mir::interpret::value::Int; - /// - /// assert_eq!(Int::from(42_i32).as_f32(), 42.0_f32); - /// assert_eq!(Int::from(-1_i8).as_f32(), -1.0_f32); - /// ``` #[inline] #[must_use] + #[expect( + clippy::cast_precision_loss, + reason = "intentional lossy conversion to float" + )] pub const fn as_f32(self) -> f32 { self.as_int() as f32 } @@ -457,58 +272,104 @@ impl Int { /// /// This may lose precision for values that cannot be exactly represented /// as a 64-bit floating point number. - /// - /// # Examples - /// - /// ``` - /// use hashql_mir::interpret::value::Int; - /// - /// assert_eq!(Int::from(42_i64).as_f64(), 42.0_f64); - /// assert_eq!(Int::from(-1_i8).as_f64(), -1.0_f64); - /// ``` #[inline] #[must_use] + #[expect( + clippy::cast_precision_loss, + reason = "intentional lossy conversion to float" + )] pub const fn as_f64(self) -> f64 { self.as_int() as f64 } } +impl fmt::Debug for Int { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let this = *self; + this.check_data(); + + match this.as_bool() { + Some(value) => f.debug_tuple("Bool").field(&value).finish(), + None => f.debug_tuple("Int").field(&this.as_int()).finish(), + } + } +} + impl Display for Int { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - Display::fmt(&self.value, fmt) + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let this = *self; + this.check_data(); + + match this.as_bool() { + Some(value) => Display::fmt(&value, f), + None => Display::fmt(&this.as_int(), f), + } } } -macro_rules! impl_from { - ($($ty:ty),*) => { - $(impl_from!(@impl $ty);)* - }; +impl PartialEq for Int { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.as_int() == other.as_int() + } +} + +impl Eq for Int {} + +impl PartialOrd for Int { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Int { + #[inline] + fn cmp(&self, other: &Self) -> cmp::Ordering { + self.as_int().cmp(&other.as_int()) + } +} + +impl Hash for Int { + #[inline] + fn hash(&self, state: &mut H) { + self.as_int().hash(state); + } +} + +impl const From for Int { + #[inline] + fn from(value: bool) -> Self { + Self::from_bool(value) + } +} - (@impl $ty:ty) => { - impl const From<$ty> for Int { - #[inline] - fn from(value: $ty) -> Self { - Self::from_value_unchecked(i128::from(value)) +macro_rules! impl_from_int { + ($($ty:ty),*) => { + $( + impl const From<$ty> for Int { + #[inline] + fn from(value: $ty) -> Self { + Self::from_i128(i128::from(value)) + } } - } + )* }; } -impl_from!(bool, u8, u16, u32, u64, i8, i16, i32, i64, i128); +impl_from_int!(u8, u16, u32, u64, i8, i16, i32, i64, i128); -// `usize` and `isize` cannot use the macro because `i128::from()` doesn't accept -// platform-dependent types. impl const From for Int { #[inline] fn from(value: usize) -> Self { - Self::from_value_unchecked(value as i128) + Self::from_i128(value as i128) } } impl const From for Int { #[inline] fn from(value: isize) -> Self { - Self::from_value_unchecked(value as i128) + Self::from_i128(value as i128) } } @@ -518,7 +379,7 @@ impl const TryFrom for Int { #[inline] fn try_from(value: u128) -> Result { match i128::try_from(value) { - Ok(value) => Ok(Self::from_value_unchecked(value)), + Ok(value) => Ok(Self::from_i128(value)), Err(error) => Err(error), } } @@ -545,7 +406,7 @@ impl TryFrom> for Int { fn try_from(value: Integer<'_>) -> Result { value .as_i128() - .map(From::from) + .map(Self::from_i128) .ok_or(TryFromIntegerError(())) } } @@ -602,7 +463,7 @@ impl<'heap> TryFrom> for Int { fn try_from(value: Primitive<'heap>) -> Result { match value { - Primitive::Boolean(bool) => Ok(bool.into()), + Primitive::Boolean(bool) => Ok(Self::from_bool(bool)), Primitive::Integer(integer) => { integer.try_into().map_err(|_err| TryFromPrimitiveError { kind: TryFromPrimitiveErrorKind::OutOfRange, @@ -622,9 +483,19 @@ impl<'heap> TryFrom> for Int { impl Not for Int { type Output = Self; + /// Boolean NOT for 1-bit values, bitwise NOT for 128-bit values. + /// + /// For booleans: `!true == false`, `!false == true`. + /// For integers: flips all 128 bits (two's complement: `!x == -(x + 1)`). #[inline] fn not(self) -> Self::Output { - Self::from_value_unchecked(!self.as_int()) + if self.is_bool() { + // Boolean NOT: flip the single bit + Self::from_bool(self.as_int() == 0) + } else { + // Bitwise NOT on full 128-bit value + Self::from_i128(!self.as_int()) + } } } @@ -633,14 +504,14 @@ impl Neg for Int { #[expect(clippy::cast_precision_loss, clippy::float_arithmetic)] fn neg(self) -> Self::Output { - let (value, overflow) = self.as_int().overflowing_neg(); + let value = self.as_int(); + let (result, overflow) = value.overflowing_neg(); - if overflow { - // There's only a single reason why this overflowed: the value was i128::MIN, in this - // case we return `i128::MAX + 1` as a Num. + if hint::unlikely(overflow) { + // Only i128::MIN overflows: return i128::MAX + 1 as float. Numeric::Num(Num::from((i128::MAX as f64) + 1.0)) } else { - Numeric::Int(Self::from_value_unchecked(value)) + Numeric::Int(Self::from_i128(result)) } } } @@ -650,12 +521,13 @@ impl Add for Int { #[expect(clippy::float_arithmetic)] fn add(self, rhs: Self) -> Self::Output { - let (value, overflow) = self.as_int().overflowing_add(rhs.as_int()); + let (lhs, rhs_val) = (self.as_int(), rhs.as_int()); + let (result, overflow) = lhs.overflowing_add(rhs_val); if hint::unlikely(overflow) { Numeric::Num(Num::from(self.as_f64() + rhs.as_f64())) } else { - Numeric::Int(Self::from_value_unchecked(value)) + Numeric::Int(Self::from_i128(result)) } } } @@ -675,12 +547,13 @@ impl Sub for Int { #[expect(clippy::float_arithmetic)] fn sub(self, rhs: Self) -> Self::Output { - let (value, overflow) = self.as_int().overflowing_sub(rhs.as_int()); + let (lhs, rhs_val) = (self.as_int(), rhs.as_int()); + let (result, overflow) = lhs.overflowing_sub(rhs_val); if hint::unlikely(overflow) { Numeric::Num(Num::from(self.as_f64() - rhs.as_f64())) } else { - Numeric::Int(Self::from_value_unchecked(value)) + Numeric::Int(Self::from_i128(result)) } } } @@ -695,19 +568,32 @@ impl Sub for Int { } } +/// Returns `BOOL_BITS` if both operands are bools, `INT_BITS` otherwise. +#[inline] +const fn bitwise_result_size(lhs: Int, rhs: Int) -> NonZero { + if lhs.is_bool() && rhs.is_bool() { + BOOL_BITS + } else { + INT_BITS + } +} + impl BitOr for Int { type Output = Self; #[inline] fn bitor(self, rhs: Self) -> Self::Output { - Self::from_value_unchecked(self.as_int() | rhs.as_int()) + Self { + value: self.as_int() | rhs.as_int(), + size: bitwise_result_size(self, rhs), + } } } impl BitOrAssign for Int { #[inline] fn bitor_assign(&mut self, rhs: Self) { - self.value |= rhs.value; + *self = *self | rhs; } } @@ -716,14 +602,17 @@ impl BitAnd for Int { #[inline] fn bitand(self, rhs: Self) -> Self::Output { - Self::from_value_unchecked(self.as_int() & rhs.as_int()) + Self { + value: self.as_int() & rhs.as_int(), + size: bitwise_result_size(self, rhs), + } } } impl BitAndAssign for Int { #[inline] fn bitand_assign(&mut self, rhs: Self) { - self.value &= rhs.value; + *self = *self & rhs; } } @@ -732,14 +621,17 @@ impl BitXor for Int { #[inline] fn bitxor(self, rhs: Self) -> Self::Output { - Self::from_value_unchecked(self.as_int() ^ rhs.as_int()) + Self { + value: self.as_int() ^ rhs.as_int(), + size: bitwise_result_size(self, rhs), + } } } impl BitXorAssign for Int { #[inline] fn bitxor_assign(&mut self, rhs: Self) { - self.value ^= rhs.value; + *self = *self ^ rhs; } } @@ -764,46 +656,256 @@ mod tests { clippy::float_cmp )] + use core::hash::BuildHasher as _; + use crate::interpret::value::{Int, Numeric}; + #[test] + fn layout() { + assert_eq!(size_of::(), 17); + assert_eq!(align_of::(), 1); + } + + #[test] + fn from_bool_preserves_size() { + assert_eq!(Int::from(true).size(), 1); + assert_eq!(Int::from(false).size(), 1); + } + + #[test] + fn from_integer_preserves_size() { + assert_eq!(Int::from(0_i32).size(), 128); + assert_eq!(Int::from(1_i32).size(), 128); + assert_eq!(Int::from(42_i64).size(), 128); + assert_eq!(Int::from(i128::MAX).size(), 128); + assert_eq!(Int::from(i128::MIN).size(), 128); + } + + #[test] + fn bool_int_equality_is_numeric() { + // Bool provenance (size) does not affect equality — only the numeric value matters. + // The type checker prevents comparing bools with ints; at the value level, + // same numeric content means same value. + assert_eq!(Int::from(true), Int::from(1_i32)); + assert_eq!(Int::from(false), Int::from(0_i32)); + } + + #[test] + fn as_bool_only_for_bools() { + assert_eq!(Int::from(true).as_bool(), Some(true)); + assert_eq!(Int::from(false).as_bool(), Some(false)); + + // Integer 1 is NOT a bool + assert_eq!(Int::from(1_i32).as_bool(), None); + assert_eq!(Int::from(0_i32).as_bool(), None); + } + + #[test] + fn as_int_works_for_all() { + assert_eq!(Int::from(42_i64).as_int(), 42); + assert_eq!(Int::from(-1_i128).as_int(), -1); + assert_eq!(Int::from(i128::MAX).as_int(), i128::MAX); + assert_eq!(Int::from(true).as_int(), 1); + assert_eq!(Int::from(false).as_int(), 0); + } + + #[test] + fn as_uint_works_for_all() { + assert_eq!(Int::from(42_i64).as_uint(), 42); + assert_eq!(Int::from(true).as_uint(), 1); + assert_eq!(Int::from(-1_i128).as_uint(), u128::MAX); + } + + #[test] + fn display_bool() { + assert_eq!(format!("{}", Int::from(true)), "true"); + assert_eq!(format!("{}", Int::from(false)), "false"); + } + + #[test] + fn display_int() { + assert_eq!(format!("{}", Int::from(42_i64)), "42"); + assert_eq!(format!("{}", Int::from(-1_i128)), "-1"); + } + + #[test] + fn equality_is_numeric() { + assert_eq!(Int::from(true), Int::from(true)); + assert_eq!(Int::from(42_i64), Int::from(42_i64)); + assert_eq!(Int::from(true), Int::from(1_i64)); + assert_eq!(Int::from(false), Int::from(0_i64)); + } + + #[test] + fn ordering_is_numeric() { + // Ordering is purely by numeric value, size is not considered. + assert_eq!( + Int::from(true).cmp(&Int::from(1_i32)), + core::cmp::Ordering::Equal + ); + assert!(Int::from(false) < Int::from(1_i32)); + assert!(Int::from(true) > Int::from(0_i32)); + } + + #[test] + fn constants() { + assert_eq!(Int::FALSE, Int::from(false)); + assert_eq!(Int::TRUE, Int::from(true)); + assert_eq!(Int::ZERO, Int::from(0_i32)); + assert_eq!(Int::ONE, Int::from(1_i32)); + + // Constants have correct sizes + assert!(Int::FALSE.is_bool()); + assert!(Int::TRUE.is_bool()); + assert!(!Int::ZERO.is_bool()); + assert!(!Int::ONE.is_bool()); + } + + #[test] + fn add_ints() { + let result = Int::from(2_i64) + Int::from(3_i64); + assert!(matches!(result, Numeric::Int(int) if int.as_int() == 5 && int.size() == 128)); + } + + #[test] + fn add_bools_promotes() { + let result = Int::from(true) + Int::from(true); + assert!(matches!(result, Numeric::Int(int) if int.as_int() == 2 && int.size() == 128)); + } + + #[test] + fn sub_ints() { + let result = Int::from(5_i64) - Int::from(3_i64); + assert!(matches!(result, Numeric::Int(int) if int.as_int() == 2 && int.size() == 128)); + } + #[test] fn neg_positive() { - let int = Int::from(42_i64); - let result = -int; - assert!(matches!(result, Numeric::Int(int) if int.as_i64() == Some(-42))); + let result = -Int::from(42_i64); + assert!(matches!(result, Numeric::Int(int) if int.as_int() == -42)); } #[test] fn neg_negative() { - let int = Int::from(-100_i64); - let result = -int; - assert!(matches!(result, Numeric::Int(int) if int.as_i64() == Some(100))); + let result = -Int::from(-100_i64); + assert!(matches!(result, Numeric::Int(int) if int.as_int() == 100)); } #[test] fn neg_zero() { - let int = Int::from(0_i64); - let result = -int; - assert!(matches!(result, Numeric::Int(int) if int.as_i64() == Some(0))); + let result = -Int::from(0_i64); + assert!(matches!(result, Numeric::Int(int) if int.as_int() == 0)); } #[test] fn neg_i128_max() { - let int = Int::from(i128::MAX); - let result = -int; + let result = -Int::from(i128::MAX); assert!(matches!(result, Numeric::Int(int) if int.as_int() == -i128::MAX)); } #[test] fn neg_i128_min_overflows_to_float() { - let int = Int::from(i128::MIN); - let result = -int; - + let result = -Int::from(i128::MIN); let Numeric::Num(num) = result else { panic!("expected Numeric::Num for -i128::MIN, got {result:?}"); }; - let expected = -(i128::MIN as f64); assert_eq!(num.as_f64(), expected); } + + #[test] + fn bitand_bools_stays_bool() { + let result = Int::from(true) & Int::from(false); + assert_eq!(result.size(), 1); + assert_eq!(result.as_bool(), Some(false)); + } + + #[test] + fn bitor_bools_stays_bool() { + let result = Int::from(false) | Int::from(true); + assert_eq!(result.size(), 1); + assert_eq!(result.as_bool(), Some(true)); + } + + #[test] + fn bitxor_bools_stays_bool() { + let result = Int::from(true) ^ Int::from(true); + assert_eq!(result.size(), 1); + assert_eq!(result.as_bool(), Some(false)); + } + + #[test] + fn bitand_mixed_promotes_to_int() { + let result = Int::from(true) & Int::from(1_i32); + assert_eq!(result.size(), 128); + assert_eq!(result.as_int(), 1); + } + + #[test] + fn not_bool() { + assert_eq!(!Int::from(true), Int::from(false)); + assert_eq!(!Int::from(false), Int::from(true)); + } + + #[test] + fn eq_ord_transitivity_with_num() { + use core::cmp::Ordering; + + use crate::interpret::value::Num; + + let bool_one = Int::from(true); + let int_one = Int::from(1_i32); + let num_one = Num::from(1.0); + + // Transitivity: bool_one == num_one, num_one == int_one, therefore bool_one == int_one + assert_eq!(bool_one, num_one); + assert_eq!(num_one, int_one); + assert_eq!(bool_one, int_one); + + // Ord consistency + assert_eq!(num_one.cmp_int(&bool_one), Ordering::Equal); + assert_eq!(num_one.cmp_int(&int_one), Ordering::Equal); + assert_eq!(bool_one.cmp(&int_one), Ordering::Equal); + } + + #[test] + fn hash_consistent_with_eq() { + use hashql_core::collections::FastHasher; + + let build = FastHasher::default(); + + // Equal values must have equal hashes + assert_eq!( + build.hash_one(Int::from(true)), + build.hash_one(Int::from(1_i32)) + ); + assert_eq!( + build.hash_one(Int::from(false)), + build.hash_one(Int::from(0_i32)) + ); + } + + #[test] + fn try_from_primitive_bool() { + use hashql_core::value::Primitive; + + let int = Int::try_from(Primitive::Boolean(true)).expect("should be able to convert bool"); + assert_eq!(int.size(), 1); + assert_eq!(int.as_bool(), Some(true)); + } + + #[test] + fn try_from_primitive_integer() { + use hashql_core::{ + heap::Heap, + value::{Integer, Primitive}, + }; + + let heap = Heap::new(); + let integer = Integer::new_unchecked(heap.intern_symbol("42")); + let int = + Int::try_from(Primitive::Integer(integer)).expect("should be able to convert integer"); + assert_eq!(int.size(), 128); + assert_eq!(int.as_int(), 42); + } } diff --git a/libs/@local/hashql/mir/src/interpret/value/list.rs b/libs/@local/hashql/mir/src/interpret/value/list.rs index 50093304881..ce457e1ad3f 100644 --- a/libs/@local/hashql/mir/src/interpret/value/list.rs +++ b/libs/@local/hashql/mir/src/interpret/value/list.rs @@ -42,7 +42,7 @@ impl<'heap, A: Allocator> List<'heap, A> { /// Returns a reference to the element at the given `index`. #[must_use] pub fn get(&self, index: Int) -> Option<&Value<'heap, A>> { - let index = index.as_isize()?; + let index = isize::try_from(index.as_int()).ok()?; if index.is_negative() { let abs = index.unsigned_abs(); @@ -63,7 +63,7 @@ impl<'heap, A: Allocator> List<'heap, A> { where A: Clone, { - let index = index.as_isize()?; + let index = isize::try_from(index.as_int()).ok()?; if index.is_negative() { let abs = index.unsigned_abs(); diff --git a/libs/@local/hashql/mir/src/interpret/value/mod.rs b/libs/@local/hashql/mir/src/interpret/value/mod.rs index 9b22a5fea1c..8f9f94781d4 100644 --- a/libs/@local/hashql/mir/src/interpret/value/mod.rs +++ b/libs/@local/hashql/mir/src/interpret/value/mod.rs @@ -55,7 +55,7 @@ pub use self::{ opaque::Opaque, ptr::Ptr, str::Str, - r#struct::Struct, + r#struct::{Struct, StructBuilder}, tuple::Tuple, }; use super::error::{RuntimeError, TypeName}; @@ -125,7 +125,7 @@ pub enum Value<'heap, A: Allocator = Global> { impl<'heap, A: Allocator> Value<'heap, A> { const UNIT: Self = Self::Unit; - pub(crate) fn type_name(&self) -> ValueTypeName<'_, 'heap, A> { + pub fn type_name(&self) -> ValueTypeName<'_, 'heap, A> { ValueTypeName::from(self) } @@ -154,10 +154,10 @@ impl<'heap, A: Allocator> Value<'heap, A> { /// 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. #[inline] - pub fn subscript<'this, 'index>( + pub fn subscript<'this, 'index, E>( &'this self, index: &'index Self, - ) -> Result<&'this Self, RuntimeError<'heap, A>> { + ) -> Result<&'this Self, RuntimeError<'heap, E, A>> { match self { Self::List(list) if let &Self::Integer(value) = index => { Ok(list.get(value).unwrap_or(&Self::UNIT)) @@ -189,10 +189,10 @@ impl<'heap, A: Allocator> Value<'heap, A> { /// /// Returns an error if this value is not subscriptable, if the index type /// is invalid, or if a list index is out of bounds. - pub fn subscript_mut<'this>( + pub fn subscript_mut<'this, E>( &'this mut self, index: &Self, - ) -> Result<&'this mut Self, RuntimeError<'heap, A>> + ) -> Result<&'this mut Self, RuntimeError<'heap, E, A>> where A: Clone, { @@ -232,11 +232,12 @@ impl<'heap, A: Allocator> Value<'heap, A> { /// /// Returns an error if this value is not projectable or the field index is invalid. #[inline] - pub fn project<'this>( + pub fn project<'this, E>( &'this self, index: FieldIndex, - ) -> Result<&'this Self, RuntimeError<'heap, A>> { + ) -> Result<&'this Self, RuntimeError<'heap, E, A>> { match self { + Self::Opaque(opaque) => opaque.value().project(index), Self::Struct(r#struct) => { r#struct .get_by_index(index) @@ -254,7 +255,6 @@ impl<'heap, A: Allocator> Value<'heap, A> { | Self::Number(_) | Self::String(_) | Self::Pointer(_) - | Self::Opaque(_) | Self::List(_) | Self::Dict(_) => Err(RuntimeError::InvalidProjectionType { base: self.type_name().into(), @@ -269,10 +269,10 @@ impl<'heap, A: Allocator> Value<'heap, A> { /// # Errors /// /// Returns an error if this value is not projectable or the field index is invalid. - pub fn project_mut<'this>( + pub fn project_mut<'this, E>( &'this mut self, index: FieldIndex, - ) -> Result<&'this mut Self, RuntimeError<'heap, A>> + ) -> Result<&'this mut Self, RuntimeError<'heap, E, A>> where A: Clone, { @@ -293,12 +293,12 @@ impl<'heap, A: Allocator> Value<'heap, A> { base: TypeName::terse(terse_name), field: index, }), + Self::Opaque(opaque) => opaque.value_mut().project_mut(index), Self::Unit | Self::Integer(_) | Self::Number(_) | Self::String(_) | Self::Pointer(_) - | Self::Opaque(_) | Self::List(_) | Self::Dict(_) => Err(RuntimeError::InvalidProjectionType { base: self.type_name().into(), @@ -313,22 +313,31 @@ impl<'heap, A: Allocator> Value<'heap, A> { /// # Errors /// /// Returns an error if this value is not a struct or the field name is not found. - pub fn project_by_name<'this>( + pub fn project_by_name<'this, E>( &'this self, index: Symbol<'heap>, - ) -> Result<&'this Self, RuntimeError<'heap, A>> { - let Self::Struct(r#struct) = self else { - return Err(RuntimeError::InvalidProjectionByNameType { - base: self.type_name().into(), - }); - }; - - r#struct - .get_by_name(index) - .ok_or_else(|| RuntimeError::UnknownFieldByName { + ) -> Result<&'this Self, RuntimeError<'heap, E, A>> { + match self { + Value::Opaque(opaque) => opaque.value().project_by_name(index), + Value::Struct(r#struct) => { + r#struct + .get_by_name(index) + .ok_or_else(|| RuntimeError::UnknownFieldByName { + base: self.type_name().into(), + field: index, + }) + } + Value::Unit + | Value::Integer(_) + | Value::Number(_) + | Value::String(_) + | Value::Pointer(_) + | Value::Tuple(_) + | Value::List(_) + | Value::Dict(_) => Err(RuntimeError::InvalidProjectionByNameType { base: self.type_name().into(), - field: index, - }) + }), + } } /// Mutably projects a field from this value by name. @@ -338,28 +347,37 @@ impl<'heap, A: Allocator> Value<'heap, A> { /// # Errors /// /// Returns an error if this value is not a struct or the field name is not found. - pub fn project_by_name_mut<'this>( + pub fn project_by_name_mut<'this, E>( &'this mut self, index: Symbol<'heap>, - ) -> Result<&'this mut Self, RuntimeError<'heap, A>> + ) -> Result<&'this mut Self, RuntimeError<'heap, E, A>> where A: Clone, { let terse_name = self.type_name_terse(); - let Self::Struct(r#struct) = self else { - return Err(RuntimeError::InvalidProjectionByNameType { - base: self.type_name().into(), - }); - }; + match self { + Value::Opaque(opaque) => opaque.value_mut().project_by_name_mut(index), + Value::Struct(r#struct) => { + if let Some(value) = r#struct.get_by_name_mut(index) { + return Ok(value); + } - if let Some(value) = r#struct.get_by_name_mut(index) { - return Ok(value); + Err(RuntimeError::UnknownFieldByName { + base: TypeName::terse(terse_name), + field: index, + }) + } + Value::Unit + | Value::Integer(_) + | Value::Number(_) + | Value::String(_) + | Value::Pointer(_) + | Value::Tuple(_) + | Value::List(_) + | Value::Dict(_) => Err(RuntimeError::InvalidProjectionByNameType { + base: self.type_name().into(), + }), } - - Err(RuntimeError::UnknownFieldByName { - base: TypeName::terse(terse_name), - field: index, - }) } } @@ -539,3 +557,36 @@ impl<'value, 'heap, A: Allocator> From<&'value Value<'heap, A>> } } } + +#[cfg(test)] +mod tests { + #![expect(clippy::min_ident_chars)] + use core::cmp::Ordering; + + use super::{Int, Num, Value}; + + #[test] + fn value_eq_transitivity_bool_num_int() { + // Regression test: Value::Eq must be transitive across Integer/Number variants. + // Previously, Integer(from(true)) == Number(1.0) and Number(1.0) == Integer(from(1)), + // but Integer(from(true)) != Integer(from(1)) — violating transitivity. + let a: Value = Value::Integer(Int::from(true)); + let b: Value = Value::Number(Num::from(1.0)); + let c: Value = Value::Integer(Int::from(1_i32)); + + assert_eq!(a, b, "bool-int == num"); + assert_eq!(b, c, "num == int"); + assert_eq!(a, c, "bool-int == int (transitivity)"); + } + + #[test] + fn value_ord_transitivity_bool_num_int() { + let a: Value = Value::Integer(Int::from(true)); + let b: Value = Value::Number(Num::from(1.0)); + let c: Value = Value::Integer(Int::from(1_i32)); + + assert_eq!(a.cmp(&b), Ordering::Equal); + assert_eq!(b.cmp(&c), Ordering::Equal); + assert_eq!(a.cmp(&c), Ordering::Equal); + } +} diff --git a/libs/@local/hashql/mir/src/interpret/value/opaque.rs b/libs/@local/hashql/mir/src/interpret/value/opaque.rs index 1b746ac10e6..dfbae62c0af 100644 --- a/libs/@local/hashql/mir/src/interpret/value/opaque.rs +++ b/libs/@local/hashql/mir/src/interpret/value/opaque.rs @@ -44,6 +44,14 @@ impl<'heap, A: Allocator> Opaque<'heap, A> { &self.value } + #[must_use] + pub fn value_mut(&mut self) -> &mut Value<'heap, A> + where + A: Clone, + { + Rc::make_mut(&mut self.value) + } + /// Returns a displayable representation of this opaque type's name. pub fn type_name(&self) -> impl Display { fmt::from_fn(|fmt| { diff --git a/libs/@local/hashql/mir/src/interpret/value/str.rs b/libs/@local/hashql/mir/src/interpret/value/str.rs index 9dcd6357e67..ee211df869d 100644 --- a/libs/@local/hashql/mir/src/interpret/value/str.rs +++ b/libs/@local/hashql/mir/src/interpret/value/str.rs @@ -1,12 +1,12 @@ //! String representation for the MIR interpreter. use alloc::{alloc::Global, rc::Rc}; -use core::{alloc::Allocator, cmp}; +use core::{alloc::Allocator, cmp, fmt}; use hashql_core::{symbol::Symbol, value::String}; /// Internal storage for string values. -#[derive(Debug, Clone)] +#[derive(Clone)] enum StrInner<'heap, A: Allocator> { Owned(Rc), Interned(Symbol<'heap>), @@ -46,7 +46,7 @@ impl Ord for StrInner<'_, A> { /// 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. -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct Str<'heap, A: Allocator = Global> { inner: StrInner<'heap, A>, } @@ -89,6 +89,20 @@ impl<'heap, A: Allocator> From<&String<'heap>> for Str<'heap, A> { } } +impl From> for Str<'_, A> { + fn from(value: Rc) -> Self { + Self { + inner: StrInner::Owned(value), + } + } +} + +impl core::fmt::Debug for Str<'_, A> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("Str").field(&self.as_str()).finish() + } +} + impl PartialEq for Str<'_, A> { fn eq(&self, other: &Self) -> bool { let Self { inner } = self; diff --git a/libs/@local/hashql/mir/src/interpret/value/struct.rs b/libs/@local/hashql/mir/src/interpret/value/struct.rs index 6475652ef4b..4e265b00983 100644 --- a/libs/@local/hashql/mir/src/interpret/value/struct.rs +++ b/libs/@local/hashql/mir/src/interpret/value/struct.rs @@ -5,12 +5,14 @@ use core::{ alloc::Allocator, cmp, fmt::{self, Display}, + mem::MaybeUninit, + ptr, }; -use hashql_core::{id::Id as _, intern::Interned, symbol::Symbol}; +use hashql_core::{algorithms::co_sort, id::Id as _, intern::Interned, symbol::Symbol}; use super::Value; -use crate::body::place::FieldIndex; +use crate::{body::place::FieldIndex, intern::Interner}; /// A named-field struct value. /// @@ -36,6 +38,7 @@ impl<'heap, A: Allocator> Struct<'heap, A> { values: Rc<[Value<'heap, A>], A>, ) -> Self { debug_assert_eq!(fields.len(), values.len()); + debug_assert!(fields.is_sorted()); Self { fields, values } } @@ -205,3 +208,357 @@ impl DoubleEndedIterator for StructIter<'_, '_, A> { } impl ExactSizeIterator for StructIter<'_, '_, A> {} + +/// A builder for [`Struct`] values with capacity for `N` fields. +pub struct StructBuilder<'heap, A: Allocator, const N: usize> { + /// Number of initialized field-value pairs. Only elements in + /// `[..initialized]` are considered live for dropping. + initialized: usize, + + fields: [MaybeUninit>; N], + values: [MaybeUninit>; N], +} + +#[expect(unsafe_code)] +impl<'heap, A: Allocator, const N: usize> StructBuilder<'heap, A, N> { + /// Creates an empty builder with capacity for `N` fields. + #[must_use] + pub const fn new() -> Self { + Self { + initialized: 0, + fields: MaybeUninit::uninit().transpose(), + values: MaybeUninit::uninit().transpose(), + } + } + + /// Returns the field names pushed so far. + #[must_use] + pub fn fields(&self) -> &[Symbol<'heap>] { + // SAFETY: `fields[..initialized]` is fully initialized by invariant. + unsafe { self.fields[..self.initialized].assume_init_ref() } + } + + /// Returns the field values pushed so far. + #[must_use] + pub fn values(&self) -> &[Value<'heap, A>] { + // SAFETY: `values[..initialized]` is fully initialized by invariant. + unsafe { self.values[..self.initialized].assume_init_ref() } + } + + /// Returns the number of fields pushed so far. + #[must_use] + pub const fn len(&self) -> usize { + self.initialized + } + + /// Returns `true` if no fields have been pushed. + #[must_use] + pub const fn is_empty(&self) -> bool { + self.initialized == 0 + } + + /// Pushes a field-value pair without checking capacity or uniqueness. + /// + /// # Safety + /// + /// The caller must ensure that `self.initialized < N` (the builder is not full), + /// and that `field` has not already been pushed. + pub const unsafe fn push_unchecked(&mut self, field: Symbol<'heap>, value: Value<'heap, A>) { + // Both `MaybeUninit::write` calls complete without panicking, so + // incrementing `initialized` afterwards preserves the invariant. + self.fields[self.initialized].write(field); + self.values[self.initialized].write(value); + + self.initialized += 1; + } + + /// Pushes a field-value pair. + /// + /// # Panics + /// + /// - If the builder is full (`initialized == N`) + /// - If `field` has already been pushed + 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"); + + // SAFETY: we just asserted `initialized < N`. + unsafe { + self.push_unchecked(field, value); + } + } + + /// Consumes the builder and produces a [`Struct`]. + pub fn finish(mut self, interner: &Interner<'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. + let values_mut = unsafe { self.values[..self.initialized].assume_init_mut() }; + + // The `Struct` expects that fields are sorted by their symbol. + // `co_sort` only swaps elements in-place and never leaves holes, so the + // initialization invariant is preserved even if it were to unwind. + co_sort(fields_mut, values_mut); + + let fields = interner.symbols.intern_slice(self.fields()); + + // Allocate an uninitialized Rc slice for the values. + // + // No drop guard is needed here because: + // - Any panic before `copy_nonoverlapping` leaves ownership with `self`, and + // `self.initialized` is unchanged, so `Drop` frees everything. + // - There is no panicking operation between `copy_nonoverlapping` and `self.initialized = + // 0`. + let mut values = Rc::new_uninit_slice_in(self.initialized, alloc); + + // SAFETY: `values` was just created so the refcount is 1 and no other references exist. + let destination = unsafe { Rc::get_mut_unchecked(&mut values) }; + + // SAFETY: we copy exactly `self.initialized` initialized elements from + // the builder's stack array into the Rc allocation. The source and + // destination do not overlap (stack vs heap). + unsafe { + ptr::copy_nonoverlapping( + self.values.as_ptr(), + destination.as_mut_ptr(), + self.initialized, + ); + }; + + // Ownership of the values has been moved into the Rc via bitwise copy. + // We must clear the drop frontier so `Drop` does not double-free them. + self.initialized = 0; + + // SAFETY: all elements in the Rc slice were initialized by the + // `copy_nonoverlapping` above. + let values = unsafe { values.assume_init() }; + + Struct { fields, values } + } +} + +impl Default for StructBuilder<'_, A, N> { + fn default() -> Self { + Self::new() + } +} + +#[expect(unsafe_code)] +impl Drop for StructBuilder<'_, A, N> { + fn drop(&mut self) { + // SAFETY: by invariant, `[..initialized]` is fully initialized. + // After `finish()` sets `initialized = 0`, this is a no-op. + unsafe { + self.fields[..self.initialized].assume_init_drop(); + self.values[..self.initialized].assume_init_drop(); + } + } +} + +#[cfg(test)] +mod tests { + use alloc::alloc::Global; + + use hashql_core::heap::Heap; + + use super::*; + use crate::interpret::value::{Int, Str, Value}; + + fn int(value: i128) -> Value<'static> { + Value::Integer(Int::from(value)) + } + + fn string(value: &str) -> Value<'static> { + Value::String(Str::from(Rc::::from(value))) + } + + #[test] + fn finish_produces_sorted_fields() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + + let sym_b = heap.intern_symbol("b"); + let sym_a = heap.intern_symbol("a"); + + // Push in reverse order; finish must sort by symbol. + let mut builder = StructBuilder::<'_, Global, 2>::new(); + builder.push(sym_b, int(2)); + builder.push(sym_a, int(1)); + + let result = builder.finish(&interner, Global); + + // Fields should be sorted: a before b. + assert_eq!(result.fields().len(), 2); + assert_eq!(result.get_by_name(sym_a), Some(&int(1))); + assert_eq!(result.get_by_name(sym_b), Some(&int(2))); + } + + #[test] + fn finish_empty_builder() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + + let builder = StructBuilder::<'_, Global, 0>::new(); + let result = builder.finish(&interner, Global); + + assert!(result.is_empty()); + assert_eq!(result.len(), 0); + } + + #[test] + fn finish_single_field() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + + let sym = heap.intern_symbol("only"); + + let mut builder = StructBuilder::<'_, Global, 1>::new(); + builder.push(sym, int(42)); + + let result = builder.finish(&interner, Global); + + assert_eq!(result.len(), 1); + assert_eq!(result.get_by_name(sym), Some(&int(42))); + } + + #[test] + fn drop_partial_builder_no_double_free() { + let heap = Heap::new(); + + let sym_x = heap.intern_symbol("x"); + let sym_y = heap.intern_symbol("y"); + + // Push values with Drop (String contains Rc), then drop the + // builder without finishing. Miri detects double-free or leak. + let mut builder = StructBuilder::<'_, Global, 3>::new(); + builder.push(sym_x, string("hello")); + builder.push(sym_y, string("world")); + // Capacity is 3 but only 2 are filled; drop must handle this. + drop(builder); + } + + #[test] + fn drop_empty_builder() { + // Zero initialized elements; Drop should be a no-op. + let _builder = StructBuilder::<'_, Global, 4>::new(); + } + + #[test] + fn drop_full_builder_without_finish() { + let heap = Heap::new(); + + let sym_a = heap.intern_symbol("a"); + let sym_b = heap.intern_symbol("b"); + + let mut builder = StructBuilder::<'_, Global, 2>::new(); + builder.push(sym_a, string("val_a")); + builder.push(sym_b, string("val_b")); + // Full but never finished; Drop must free both. + drop(builder); + } + + #[test] + fn finish_with_drop_values_no_double_free() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + + let sym_a = heap.intern_symbol("a"); + let sym_b = heap.intern_symbol("b"); + + let mut builder = StructBuilder::<'_, Global, 2>::new(); + builder.push(sym_a, string("alpha")); + builder.push(sym_b, string("beta")); + + // finish moves values into Rc; builder Drop must not re-drop them. + let result = builder.finish(&interner, Global); + + assert_eq!(result.len(), 2); + // Verify values survived the move. + let Value::String(ref value) = *result.get_by_name(sym_a).expect("field should exist") + else { + panic!("expected String"); + }; + assert_eq!(value.as_str(), "alpha"); + } + + #[test] + fn finish_sorts_drop_values_correctly() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + + // Create symbols that sort in a known order. + let sym_c = heap.intern_symbol("c"); + let sym_a = heap.intern_symbol("a"); + let sym_b = heap.intern_symbol("b"); + + // Push in c, a, b order. + let mut builder = StructBuilder::<'_, Global, 3>::new(); + builder.push(sym_c, string("charlie")); + builder.push(sym_a, string("alpha")); + builder.push(sym_b, string("bravo")); + + let result = builder.finish(&interner, Global); + + // After sorting: a, b, c. + let pairs: Vec<_> = result.iter().collect(); + assert_eq!(pairs.len(), 3); + assert_eq!(pairs[0].0, sym_a); + assert_eq!(pairs[1].0, sym_b); + assert_eq!(pairs[2].0, sym_c); + + // Values must follow their fields. + let Value::String(ref value) = *pairs[0].1 else { + panic!("expected String"); + }; + assert_eq!(value.as_str(), "alpha"); + } + + #[test] + fn fields_and_values_reflect_push_count() { + let heap = Heap::new(); + + let sym_a = heap.intern_symbol("a"); + let sym_b = heap.intern_symbol("b"); + + let mut builder = StructBuilder::<'_, Global, 3>::new(); + assert!(builder.is_empty()); + assert_eq!(builder.len(), 0); + assert!(builder.fields().is_empty()); + assert!(builder.values().is_empty()); + + builder.push(sym_a, int(1)); + assert_eq!(builder.len(), 1); + assert_eq!(builder.fields(), &[sym_a]); + assert_eq!(builder.values(), &[int(1)]); + + builder.push(sym_b, int(2)); + assert_eq!(builder.len(), 2); + } + + #[test] + #[should_panic(expected = "struct is full")] + fn push_panics_when_full() { + let heap = Heap::new(); + + let sym_a = heap.intern_symbol("a"); + let sym_b = heap.intern_symbol("b"); + + let mut builder = StructBuilder::<'_, Global, 1>::new(); + builder.push(sym_a, int(1)); + // This must panic, and the builder's Drop must still free sym_a's value. + builder.push(sym_b, int(2)); + } + + #[test] + #[should_panic(expected = "field already exists")] + fn push_panics_on_duplicate_field() { + let heap = Heap::new(); + + let sym = heap.intern_symbol("dup"); + + let mut builder = StructBuilder::<'_, Global, 2>::new(); + builder.push(sym, string("first")); + // This must panic. "first" must still be freed by Drop. + builder.push(sym, string("second")); + } +} diff --git a/libs/@local/hashql/mir/src/lib.rs b/libs/@local/hashql/mir/src/lib.rs index 744d289dd47..dcbb5b2831a 100644 --- a/libs/@local/hashql/mir/src/lib.rs +++ b/libs/@local/hashql/mir/src/lib.rs @@ -28,6 +28,7 @@ temporary_niche_types, try_trait_v2, variant_count, + maybe_uninit_uninit_array_transpose )] #![cfg_attr(test, feature( // Library Features diff --git a/libs/@local/hashql/mir/src/pass/execution/island/graph/mod.rs b/libs/@local/hashql/mir/src/pass/execution/island/graph/mod.rs index e3e661d377b..c3c4f6a54dc 100644 --- a/libs/@local/hashql/mir/src/pass/execution/island/graph/mod.rs +++ b/libs/@local/hashql/mir/src/pass/execution/island/graph/mod.rs @@ -279,6 +279,12 @@ impl IslandGraph { .filter(move |node| node.data.target == target) .map(|node| (IslandId::new(node.id().as_u32()), &node.data)) } + + pub fn lookup(&self, block: BasicBlockId) -> (IslandId, &IslandNode) { + let id = self.lookup[block]; + + (id, &self[id]) + } } impl DirectedGraph for IslandGraph { diff --git a/libs/@local/hashql/mir/src/pass/execution/island/schedule/mod.rs b/libs/@local/hashql/mir/src/pass/execution/island/schedule/mod.rs index 26099f9396d..143842e4498 100644 --- a/libs/@local/hashql/mir/src/pass/execution/island/schedule/mod.rs +++ b/libs/@local/hashql/mir/src/pass/execution/island/schedule/mod.rs @@ -84,8 +84,9 @@ impl IslandGraph { /// strictly lower levels. Islands at the same level have no direct dependencies and /// can execute concurrently. #[expect(clippy::cast_possible_truncation)] - pub fn schedule_in(&self, scratch: S, alloc: A) -> IslandSchedule + pub fn schedule_in(&self, scratch: S, alloc: B) -> IslandSchedule where + B: Allocator, S: Allocator + Clone, { let node_count = self.node_count(); diff --git a/libs/@local/hashql/mir/src/pass/execution/traversal/analysis/mod.rs b/libs/@local/hashql/mir/src/pass/execution/traversal/analysis/mod.rs index d84d03d0994..82b2411ec51 100644 --- a/libs/@local/hashql/mir/src/pass/execution/traversal/analysis/mod.rs +++ b/libs/@local/hashql/mir/src/pass/execution/traversal/analysis/mod.rs @@ -25,9 +25,6 @@ pub(crate) enum TraversalResult { /// /// Walks a body's places, finds uses of [`Local::VERTEX`], resolves the projection chain /// via [`EntityPath::resolve`], and calls `on_traversal` with the [`Location`] and result. -// TODO: Each consumer (statement placement per target, island placement) resolves traversal paths -// independently. Consider caching resolved paths per body to avoid redundant work. -// See: https://linear.app/hash/issue/BE-435 pub(crate) struct TraversalAnalysisVisitor { vertex: VertexType, on_traversal: F, diff --git a/libs/@local/hashql/mir/src/pass/execution/traversal/entity.rs b/libs/@local/hashql/mir/src/pass/execution/traversal/entity.rs index 2e9b2f77646..05078445d0b 100644 --- a/libs/@local/hashql/mir/src/pass/execution/traversal/entity.rs +++ b/libs/@local/hashql/mir/src/pass/execution/traversal/entity.rs @@ -6,6 +6,7 @@ use hashql_core::{ bit_vec::{BitRelations as _, FiniteBitSet}, }, symbol::{ConstantSymbol, Symbol, sym}, + r#type::{TypeBuilder, TypeId, environment::Environment}, }; use super::{ @@ -229,6 +230,127 @@ impl EntityPath { } } + /// The sequence of struct field names from the entity root to this path's position + /// in the type hierarchy. + /// + /// Each element corresponds to a field name in a nested struct. For example, + /// [`WebId`](Self::WebId) returns `[metadata, record_id, entity_id, web_id]`, + /// meaning the resolution walks: `Entity` → `metadata` field → `EntityMetadata` → + /// `record_id` field → `EntityRecordId` → `entity_id` field → `EntityId` → `web_id` field. + /// + /// Used by [`Self::resolve_type`] to navigate the entity type structure. + #[must_use] + pub const fn field_path(self) -> &'static [Symbol<'static>] { + match self { + Self::Properties => &[sym::properties], + Self::Vectors => &[sym::encodings, sym::vectors], + Self::RecordId => &[sym::metadata, sym::record_id], + Self::EntityId => &[sym::metadata, sym::record_id, sym::entity_id], + Self::WebId => &[sym::metadata, sym::record_id, sym::entity_id, sym::web_id], + Self::EntityUuid => &[ + sym::metadata, + sym::record_id, + sym::entity_id, + sym::entity_uuid, + ], + Self::DraftId => &[sym::metadata, sym::record_id, sym::entity_id, sym::draft_id], + Self::EditionId => &[sym::metadata, sym::record_id, sym::edition_id], + Self::TemporalVersioning => &[sym::metadata, sym::temporal_versioning], + Self::DecisionTime => &[sym::metadata, sym::temporal_versioning, sym::decision_time], + Self::TransactionTime => &[ + sym::metadata, + sym::temporal_versioning, + sym::transaction_time, + ], + Self::EntityTypeIds => &[sym::metadata, sym::entity_type_ids], + Self::Archived => &[sym::metadata, sym::archived], + Self::Confidence => &[sym::metadata, sym::confidence], + Self::ProvenanceInferred => &[sym::metadata, sym::provenance, sym::inferred], + Self::ProvenanceEdition => &[sym::metadata, sym::provenance, sym::edition], + Self::PropertyMetadata => &[sym::metadata, sym::properties], + Self::LeftEntityWebId => &[sym::link_data, sym::left_entity_id, sym::web_id], + Self::LeftEntityUuid => &[sym::link_data, sym::left_entity_id, sym::entity_uuid], + Self::RightEntityWebId => &[sym::link_data, sym::right_entity_id, sym::web_id], + Self::RightEntityUuid => &[sym::link_data, sym::right_entity_id, sym::entity_uuid], + Self::LeftEntityConfidence => &[sym::link_data, sym::left_entity_confidence], + Self::RightEntityConfidence => &[sym::link_data, sym::right_entity_confidence], + Self::LeftEntityProvenance => &[sym::link_data, sym::left_entity_provenance], + Self::RightEntityProvenance => &[sym::link_data, sym::right_entity_provenance], + } + } + + /// Returns the type of this path. + /// + /// Every path except [`Properties`](Self::Properties) has a fixed type that does not + /// depend on the entity being queried. This method is a convenience for callers that + /// know the path is not `Properties`. + /// + /// # Panics + /// + /// Panics if called on [`Properties`](Self::Properties), which has no fixed type. + pub fn expect_type(self, env: &Environment<'_>) -> TypeId { + self.resolve_type(env) + .expect("called `expect_type` on `Properties`, which has no fixed type") + } + + /// Returns the type of this path, or `None` for [`Properties`](Self::Properties). + /// + /// Every path except `Properties` has a fixed type determined by the entity schema — + /// it doesn't depend on which `Entity` is being queried. `Properties` returns `None` + /// because its type is the generic `T` parameter, which varies per entity type. + /// + /// Types are constructed from the canonical factory functions in the standard library, + /// ensuring they match the definitions registered by the module system. + pub fn resolve_type(self, env: &Environment<'_>) -> Option { + use hashql_core::module::std_lib::{ + core::option::types as option, + graph::{ + temporal::types as temporal, + types::{ + knowledge::entity::types as entity, ontology::types as ontology, + principal::actor_group::web::types as web, + }, + }, + }; + + let ty = TypeBuilder::synthetic(env); + + let r#type = match self { + Self::Properties => return None, + Self::Vectors => ty.unknown(), + Self::RecordId => entity::record_id(&ty, None), + Self::EntityId => entity::entity_id(&ty, None), + Self::WebId | Self::LeftEntityWebId | Self::RightEntityWebId => web::web_id(&ty, None), + Self::EntityUuid | Self::LeftEntityUuid | Self::RightEntityUuid => { + entity::entity_uuid(&ty, None) + } + Self::DraftId => entity::draft_id(&ty, None), + Self::EditionId => entity::entity_edition_id(&ty, None), + Self::TemporalVersioning => entity::temporal_metadata(&ty, None), + Self::DecisionTime => { + let interval = temporal::left_closed_temporal_interval(&ty); + temporal::decision_time(&ty, interval) + } + Self::TransactionTime => { + let interval = temporal::left_closed_temporal_interval(&ty); + temporal::transaction_time(&ty, interval) + } + Self::EntityTypeIds => ty.list(ontology::versioned_url(&ty, None)), + Self::Archived => ty.boolean(), + Self::Confidence | Self::LeftEntityConfidence | Self::RightEntityConfidence => { + option::option(&ty, entity::confidence(&ty)) + } + Self::ProvenanceInferred => entity::inferred_entity_provenance(&ty), + Self::ProvenanceEdition => entity::entity_edition_provenance(&ty), + Self::PropertyMetadata => entity::property_object_metadata(&ty), + Self::LeftEntityProvenance | Self::RightEntityProvenance => { + entity::property_provenance(&ty) + } + }; + + Some(r#type) + } + /// Returns the set of execution targets that natively serve this path. pub(crate) const fn origin(self) -> TargetBitSet { let mut set = TargetBitSet::new_empty(TargetId::VARIANT_COUNT_U32); diff --git a/libs/@local/hashql/mir/src/pass/execution/traversal/mod.rs b/libs/@local/hashql/mir/src/pass/execution/traversal/mod.rs index f72b02e0e75..37b5f21404f 100644 --- a/libs/@local/hashql/mir/src/pass/execution/traversal/mod.rs +++ b/libs/@local/hashql/mir/src/pass/execution/traversal/mod.rs @@ -19,7 +19,11 @@ mod analysis; mod tests; pub(crate) use analysis::{TraversalAnalysisVisitor, TraversalResult}; -use hashql_core::{id::IdArray, symbol::Symbol}; +use hashql_core::{ + id::IdArray, + symbol::Symbol, + r#type::{TypeId, environment::Environment}, +}; pub use self::entity::{EntityPath, EntityPathBitSet}; pub(crate) use self::{access::Access, entity::TransferCostConfig}; @@ -241,6 +245,19 @@ impl TraversalPath { } } + /// Returns the type of this path. + /// + /// Most paths have a fixed type determined by the graph schema. Paths whose type + /// depends on the specific vertex being queried (e.g. entity properties) return + /// [`None`]. + #[inline] + #[must_use] + pub fn resolve_type(self, env: &Environment<'_>) -> Option { + match self { + Self::Entity(path) => path.resolve_type(env), + } + } + /// Returns the set of execution targets that natively serve this path. #[inline] #[must_use] diff --git a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs index cb18f27e390..9081ad56b10 100644 --- a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs @@ -252,20 +252,19 @@ impl<'heap, A: Allocator> InstSimplifyVisitor<'_, 'heap, A> { /// Evaluates a binary operation on two constant integers. fn eval_bin_op(lhs: Int, op: BinOp, rhs: Int) -> Option { - let lhs = lhs.as_int(); - let rhs = rhs.as_int(); - let result = match op { - BinOp::Add => return lhs.checked_add(rhs).map(Int::from), - BinOp::Sub => return lhs.checked_sub(rhs).map(Int::from), - BinOp::BitAnd => lhs & rhs, - BinOp::BitOr => lhs | rhs, - BinOp::Eq => i128::from(lhs == rhs), - BinOp::Ne => i128::from(lhs != rhs), - BinOp::Lt => i128::from(lhs < rhs), - BinOp::Lte => i128::from(lhs <= rhs), - BinOp::Gt => i128::from(lhs > rhs), - BinOp::Gte => i128::from(lhs >= rhs), + BinOp::Add => return lhs.checked_add(rhs), + BinOp::Sub => return lhs.checked_sub(rhs), + // Bitwise ops preserve bool provenance via the Int operators + BinOp::BitAnd => return Some(lhs & rhs), + BinOp::BitOr => return Some(lhs | rhs), + // Comparisons produce booleans + BinOp::Eq => lhs.as_int() == rhs.as_int(), + BinOp::Ne => lhs.as_int() != rhs.as_int(), + BinOp::Lt => lhs.as_int() < rhs.as_int(), + BinOp::Lte => lhs.as_int() <= rhs.as_int(), + BinOp::Gt => lhs.as_int() > rhs.as_int(), + BinOp::Gte => lhs.as_int() >= rhs.as_int(), }; Some(Int::from(result)) @@ -273,21 +272,12 @@ impl<'heap, A: Allocator> InstSimplifyVisitor<'_, 'heap, A> { /// Evaluates a unary operation on a constant integer. fn eval_un_op(op: UnOp, operand: Int) -> Int { - let value = operand.as_int(); - - let result = match op { - UnOp::Not => { - let Some(value) = operand.as_bool() else { - unreachable!("only boolean values can be negated"); - }; - - i128::from(!value) - } - UnOp::Neg => -value, - UnOp::BitNot => !value, - }; - - Int::from(result) + match op { + // Both Not and BitNot use the `!` operator, which dispatches on size: + // booleans get logical NOT, integers get bitwise NOT. + UnOp::Not | UnOp::BitNot => !operand, + UnOp::Neg => Int::from(-operand.as_int()), + } } /// Attempts to simplify a binary operation with a constant left operand and place right diff --git a/libs/@local/hashql/mir/src/pass/transform/ssa_repair/mod.rs b/libs/@local/hashql/mir/src/pass/transform/ssa_repair/mod.rs index 91c91265a55..46eabd3cbee 100644 --- a/libs/@local/hashql/mir/src/pass/transform/ssa_repair/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/ssa_repair/mod.rs @@ -547,10 +547,13 @@ impl<'heap> VisitorMut<'heap> for RewireBody<'_, 'heap> { location: Location, params: &mut Interned<'heap, [Local]>, ) -> Self::Result<()> { - // We don't walk the params here, we handle the `Def` site differently in `visit_local`, so - // don't need to set `self.last_def`. + // Block parameters are definitions at the block header. They must be renamed and placed + // on the reaching-definition chain before any uses in the block are visited. This is + // independent of `block_top`: a block can have an existing param for the repaired local + // without being in the IDF (e.g. terminal blocks where the IDF is empty). + Ok(()) = visit::r#mut::walk_params(self, location, params); + let Some(&def) = self.block_top.lookup(location.block) else { - // No `FindDefFromTop` result is required in the body return Ok(()); }; @@ -753,6 +756,19 @@ impl<'heap> Visitor<'heap> for UseBeforeDef { visit::r#ref::walk_statement(self, location, statement) } + fn visit_terminator( + &mut self, + location: Location, + terminator: &crate::body::terminator::Terminator<'heap>, + ) -> Self::Result { + // Same thing applies as in `visit_statement`. + if location.statement_index >= self.def_statement_index { + return ControlFlow::Continue(()); + } + + visit::r#ref::walk_terminator(self, location, terminator) + } + fn visit_statement_assign( &mut self, location: Location, diff --git a/libs/@local/hashql/mir/src/pass/transform/ssa_repair/tests.rs b/libs/@local/hashql/mir/src/pass/transform/ssa_repair/tests.rs index 58f938054d6..c9110755da4 100644 --- a/libs/@local/hashql/mir/src/pass/transform/ssa_repair/tests.rs +++ b/libs/@local/hashql/mir/src/pass/transform/ssa_repair/tests.rs @@ -616,3 +616,83 @@ fn reassign_rodeo() { }, ); } + +/// Regression test for a bug where SSA repair panicked when a local was both +/// a block parameter in a terminal block and assigned in a sibling block. +/// +/// `UseBeforeDef` incorrectly reported a use-before-def in the block-param +/// block (the terminator use was not guarded), and `RewireBody` failed to +/// recognize existing block-param definitions without a `block_top` entry. +#[test] +fn block_param_def_with_sibling_assignment() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + // bb0 branches to bb1 or bb2. bb1 assigns x and returns it directly. + // bb2 receives x as a block parameter and returns it. Both blocks are + // terminal, so the IDF of {bb1, bb2} is empty. + let body = body!(interner, env; fn@0/0 -> Int { + decl x: Int, cond: Bool; + + bb0() { + cond = load true; + if cond then bb2() else bb1(0); + }, + bb1(x) { + return x; + }, + bb2() { + x = load 1; + return x; + } + }); + + assert_ssa_pass( + "block_param_def_with_sibling_assignment", + body, + MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} + +#[test] +fn block_param_def_with_sibling_assignment2() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + // bb0 branches to bb1 or bb2. bb1 assigns x and returns it directly. + // bb2 receives x as a block parameter and returns it. Both blocks are + // terminal, so the IDF of {bb1, bb2} is empty. + let body = body!(interner, env; fn@0/0 -> Int { + decl x: Int, cond: Bool; + + bb0() { + cond = load true; + if cond then bb1() else bb2(0); + }, + bb1() { + x = load 1; + return x; + }, + bb2(x) { + return x; + } + }); + + assert_ssa_pass( + "block_param_def_with_sibling_assignment2", + body, + MirContext { + heap: &heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }, + ); +} diff --git a/libs/@local/hashql/mir/tests/ui/pass/administrative_reduction/closure-chain.stdout b/libs/@local/hashql/mir/tests/ui/pass/administrative_reduction/closure-chain.stdout index ec54c5c2065..b8737ef3c01 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/administrative_reduction/closure-chain.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/administrative_reduction/closure-chain.stdout @@ -96,7 +96,7 @@ thunk f4:0() -> (Boolean) -> Boolean { bb0(): { %0 = apply (f4:0 as FnPtr) - %1 = apply %0.0 %0.1 1 + %1 = apply %0.0 %0.1 true return %1 } @@ -284,7 +284,7 @@ thunk f4:0() -> (Boolean) -> Boolean { %2 = closure(({closure#18} as FnPtr), %3) %0 = %2 %4 = %0.1 - %5 = 1 + %5 = true %9 = () %8 = closure(({closure#16} as FnPtr), %9) %6 = %8 diff --git a/libs/@local/hashql/mir/tests/ui/pass/administrative_reduction/forwarding-closure.stdout b/libs/@local/hashql/mir/tests/ui/pass/administrative_reduction/forwarding-closure.stdout index 2461da1e6d2..930d83e63bf 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/administrative_reduction/forwarding-closure.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/administrative_reduction/forwarding-closure.stdout @@ -48,7 +48,7 @@ thunk f2:0() -> (Boolean) -> Boolean { bb0(): { %0 = apply (f2:0 as FnPtr) - %1 = apply %0.0 %0.1 1 + %1 = apply %0.0 %0.1 true return %1 } @@ -124,7 +124,7 @@ thunk f2:0() -> (Boolean) -> Boolean { %2 = closure(({closure#8} as FnPtr), %3) %0 = %2 %4 = %0.1 - %5 = 1 + %5 = true %9 = () %8 = closure(({closure#6} as FnPtr), %9) %6 = %8 diff --git a/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/cascade-switch-then-goto.stdout b/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/cascade-switch-then-goto.stdout index 151cd0c165a..971fd479e45 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/cascade-switch-then-goto.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/cascade-switch-then-goto.stdout @@ -14,7 +14,7 @@ thunk x:0() -> Integer { let %4: Boolean bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/closure-with-const-branch.stdout b/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/closure-with-const-branch.stdout index b08640bf168..cec6bcdb129 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/closure-with-const-branch.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/closure-with-const-branch.stdout @@ -6,7 +6,7 @@ fn {closure#6}(%0: (), %1: Integer) -> Boolean { let %4: Boolean bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/const-if-false.stdout b/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/const-if-false.stdout index 465657e22fb..967d64290e4 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/const-if-false.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/const-if-false.stdout @@ -4,7 +4,7 @@ let %0: String bb0(): { - switchInt(0) -> [0: bb2(), 1: bb1()] + switchInt(false) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/const-if-true.stdout b/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/const-if-true.stdout index 69bc106c5fa..bda9f9759e0 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/const-if-true.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/const-if-true.stdout @@ -4,7 +4,7 @@ let %0: String bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/const-nested-if.stdout b/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/const-nested-if.stdout index a96d05e1901..2c9874a64dc 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/const-nested-if.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/const-nested-if.stdout @@ -5,11 +5,11 @@ let %1: String bb0(): { - switchInt(1) -> [0: bb5(), 1: bb1()] + switchInt(true) -> [0: bb5(), 1: bb1()] } bb1(): { - switchInt(0) -> [0: bb3(), 1: bb2()] + switchInt(false) -> [0: bb3(), 1: bb2()] } bb2(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/dead-block-elimination.aux.svg b/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/dead-block-elimination.aux.svg index 53b98e858aa..7891000b325 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/dead-block-elimination.aux.svg +++ b/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/dead-block-elimination.aux.svg @@ -1,20 +1,20 @@ -Initial MIRMIR after CFG Simplification

thunk {thunk#4}() -> String

thunk {thunk#4}() -> String

-
bb0()
MIR
TswitchInt(0)
bb8()
MIR
Tgoto
bb1()
MIR
0%1 = 1 <= 2
TswitchInt(%1)
bb6()
MIR
Tgoto
bb2()
MIR
0%3 = 3 >= 4
TswitchInt(%3)
bb4()
MIR
Tgoto
bb3()
MIR
Tgoto
bb5(%4)
MIR
Tgoto
bb7(%2)
MIR
Tgoto
bb9(%0)
MIR
Treturn %0
bb0()
MIR
0%0 = "taken"
Treturn %0
()0()1()0()1()0()1("deeply-nested-then")("deeply-nested-else")(%4)("inner-else")(%2)("taken") +
bb0()
MIR
TswitchInt(false)
bb8()
MIR
Tgoto
bb1()
MIR
0%1 = 1 <= 2
TswitchInt(%1)
bb6()
MIR
Tgoto
bb2()
MIR
0%3 = 3 >= 4
TswitchInt(%3)
bb4()
MIR
Tgoto
bb3()
MIR
Tgoto
bb5(%4)
MIR
Tgoto
bb7(%2)
MIR
Tgoto
bb9(%0)
MIR
Treturn %0
bb0()
MIR
0%0 = "taken"
Treturn %0
()0()1()0()1()0()1("deeply-nested-then")("deeply-nested-else")(%4)("inner-else")(%2)("taken") - - + + diff --git a/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/dead-block-elimination.stdout b/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/dead-block-elimination.stdout index 42b1ef1172a..8cf1f126a8b 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/dead-block-elimination.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/dead-block-elimination.stdout @@ -8,7 +8,7 @@ let %4: String bb0(): { - switchInt(0) -> [0: bb8(), 1: bb1()] + switchInt(false) -> [0: bb8(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/let-in-branch.stdout b/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/let-in-branch.stdout index e62094ae923..e96f5f37813 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/let-in-branch.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/let-in-branch.stdout @@ -6,7 +6,7 @@ let %2: Boolean bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { @@ -17,7 +17,7 @@ } bb2(): { - goto -> bb3(0) + goto -> bb3(false) } bb3(%0): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/mixed-const-runtime-if.stdout b/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/mixed-const-runtime-if.stdout index de40de8866a..fcb1d0d8c4f 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/mixed-const-runtime-if.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/mixed-const-runtime-if.stdout @@ -13,7 +13,7 @@ thunk x:0() -> Integer { let %3: String bb0(): { - switchInt(1) -> [0: bb5(), 1: bb1()] + switchInt(true) -> [0: bb5(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/noop_block_multiple_predecessors.snap b/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/noop_block_multiple_predecessors.snap index 0574d53f42a..3e01c57eb61 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/noop_block_multiple_predecessors.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/cfg_simplify/noop_block_multiple_predecessors.snap @@ -6,7 +6,7 @@ fn {closure@4294967040}() -> Null { let %0: Boolean bb0(): { - %0 = 1 + %0 = true switchInt(%0) -> [0: bb2(), 1: bb1()] } @@ -34,7 +34,7 @@ fn {closure@4294967040}() -> Null { let %0: Boolean bb0(): { - %0 = 1 + %0 = true switchInt(%0) -> [0: bb2(), 1: bb1()] } diff --git a/libs/@local/hashql/mir/tests/ui/pass/copy_propagation/block_param_copy.snap b/libs/@local/hashql/mir/tests/ui/pass/copy_propagation/block_param_copy.snap index 59e943e3318..88d0220260a 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/copy_propagation/block_param_copy.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/copy_propagation/block_param_copy.snap @@ -9,7 +9,7 @@ fn {closure@4294967040}() -> Boolean { let %3: Boolean bb0(): { - %1 = 1 + %1 = true switchInt(%1) -> [0: bb2(), 1: bb1()] } @@ -38,9 +38,9 @@ fn {closure@4294967040}() -> Boolean { let %3: Boolean bb0(): { - %1 = 1 + %1 = true - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/copy_propagation/block_param_copy_disagreement.snap b/libs/@local/hashql/mir/tests/ui/pass/copy_propagation/block_param_copy_disagreement.snap index 860c8f29c6d..1a1c694a316 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/copy_propagation/block_param_copy_disagreement.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/copy_propagation/block_param_copy_disagreement.snap @@ -10,7 +10,7 @@ fn {closure@4294967040}() -> Boolean { let %4: Boolean bb0(): { - %2 = 1 + %2 = true switchInt(%2) -> [0: bb2(), 1: bb1()] } @@ -40,9 +40,9 @@ fn {closure@4294967040}() -> Boolean { let %4: Boolean bb0(): { - %2 = 1 + %2 = true - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/copy_propagation/block_param_disagreement.snap b/libs/@local/hashql/mir/tests/ui/pass/copy_propagation/block_param_disagreement.snap index 10ceceb2c04..4a4dfa65892 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/copy_propagation/block_param_disagreement.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/copy_propagation/block_param_disagreement.snap @@ -1,5 +1,5 @@ --- -source: libs/@local/hashql/mir/src/pass/transform/cp/tests.rs +source: libs/@local/hashql/mir/src/pass/transform/copy_propagation/tests.rs expression: value --- fn {closure@4294967040}() -> Boolean { @@ -8,7 +8,7 @@ fn {closure@4294967040}() -> Boolean { let %2: Boolean bb0(): { - %0 = 1 + %0 = true switchInt(%0) -> [0: bb2(), 1: bb1()] } @@ -36,9 +36,9 @@ fn {closure@4294967040}() -> Boolean { let %2: Boolean bb0(): { - %0 = 1 + %0 = true - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/copy_propagation/block_param_unanimous.snap b/libs/@local/hashql/mir/tests/ui/pass/copy_propagation/block_param_unanimous.snap index 56584f9df79..08fd9cdcc8c 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/copy_propagation/block_param_unanimous.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/copy_propagation/block_param_unanimous.snap @@ -1,5 +1,5 @@ --- -source: libs/@local/hashql/mir/src/pass/transform/cp/tests.rs +source: libs/@local/hashql/mir/src/pass/transform/copy_propagation/tests.rs expression: value --- fn {closure@4294967040}() -> Boolean { @@ -8,7 +8,7 @@ fn {closure@4294967040}() -> Boolean { let %2: Boolean bb0(): { - %0 = 1 + %0 = true switchInt(%0) -> [0: bb2(), 1: bb1()] } @@ -36,9 +36,9 @@ fn {closure@4294967040}() -> Boolean { let %2: Boolean bb0(): { - %0 = 1 + %0 = true - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/copy_propagation/loop_back_edge.snap b/libs/@local/hashql/mir/tests/ui/pass/copy_propagation/loop_back_edge.snap index 2fe78eb4bbf..be7571e9dc3 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/copy_propagation/loop_back_edge.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/copy_propagation/loop_back_edge.snap @@ -1,5 +1,5 @@ --- -source: libs/@local/hashql/mir/src/pass/transform/cp/tests.rs +source: libs/@local/hashql/mir/src/pass/transform/copy_propagation/tests.rs expression: value --- fn {closure@4294967040}() -> Boolean { @@ -8,7 +8,7 @@ fn {closure@4294967040}() -> Boolean { let %2: Boolean bb0(): { - %2 = 1 + %2 = true goto -> bb1(1) } @@ -32,7 +32,7 @@ fn {closure@4294967040}() -> Boolean { let %2: Boolean bb0(): { - %2 = 1 + %2 = true goto -> bb1(1) } @@ -40,7 +40,7 @@ fn {closure@4294967040}() -> Boolean { bb1(%0): { %1 = %0 == %0 - switchInt(1) -> [0: bb2(), 1: bb1(2)] + switchInt(true) -> [0: bb2(), 1: bb1(2)] } bb2(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/binary-operation.stdout b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/binary-operation.stdout index 367837f42c0..129ac52aa20 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/binary-operation.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/binary-operation.stdout @@ -7,7 +7,7 @@ let %3: Boolean bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { @@ -19,7 +19,7 @@ } bb2(): { - goto -> bb3(0) + goto -> bb3(false) } bb3(%0): { @@ -29,7 +29,7 @@ ════ Data Dependency Graph ═════════════════════════════════════════════════════ %0 -> %3 [Param] -%0 -> 0 [Param] +%0 -> false [Param] %1 -> 1 [Load] %2 -> 2 [Load] @@ -37,7 +37,7 @@ ════ Transient Data Dependency Graph ═══════════════════════════════════════════ %0 -> %3 [Param] -%0 -> 0 [Param] +%0 -> false [Param] %1 -> 1 [Load] %2 -> 2 [Load] diff --git a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/closure-construction.stdout b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/closure-construction.stdout index 658bfeff336..b2d1b2be62d 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/closure-construction.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/closure-construction.stdout @@ -17,7 +17,7 @@ fn f:0(%0: (Integer,)) -> Integer { let %3: (Integer,) bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/comparison-operators.stdout b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/comparison-operators.stdout index 5062959e61a..652fdf7cbb5 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/comparison-operators.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/comparison-operators.stdout @@ -7,7 +7,7 @@ let %3: Boolean bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { @@ -19,7 +19,7 @@ } bb2(): { - goto -> bb3(0) + goto -> bb3(false) } bb3(%0): { @@ -29,7 +29,7 @@ ════ Data Dependency Graph ═════════════════════════════════════════════════════ %0 -> %3 [Param] -%0 -> 0 [Param] +%0 -> false [Param] %1 -> 3 [Load] %2 -> 2 [Load] @@ -37,7 +37,7 @@ ════ Transient Data Dependency Graph ═══════════════════════════════════════════ %0 -> %3 [Param] -%0 -> 0 [Param] +%0 -> false [Param] %1 -> 3 [Load] %2 -> 2 [Load] diff --git a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/deeply-nested-tuple.stdout b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/deeply-nested-tuple.stdout index e6a6ac20b02..acc6ed392d4 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/deeply-nested-tuple.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/deeply-nested-tuple.stdout @@ -8,7 +8,7 @@ let %4: (((Integer,),),) bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/function-apply.stdout b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/function-apply.stdout index 94f829f2da1..c4d7feef4aa 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/function-apply.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/function-apply.stdout @@ -30,7 +30,7 @@ fn max:0(%0: (), %1: Integer, %2: Integer) -> Integer { let %3: Integer bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/function-multiple-args.stdout b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/function-multiple-args.stdout index 5514c7ccc4b..897cad77778 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/function-multiple-args.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/function-multiple-args.stdout @@ -46,7 +46,7 @@ fn f:0(%0: (), %1: Integer, %2: Integer, %3: Integer) -> Integer { let %3: Integer bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/graph-read-filter.stdout b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/graph-read-filter.stdout index 73fda5d494b..e7f6d50da7d 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/graph-read-filter.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/graph-read-filter.stdout @@ -19,7 +19,7 @@ fn {graph::read::filter@11}(%0: (Boolean,), %1: ::graph::types::knowledge::entit let %5: (Boolean,) bb0(): { - switchInt(1) -> [0: bb3(), 1: bb1()] + switchInt(true) -> [0: bb3(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/list-construction.stdout b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/list-construction.stdout index 3423ffa34f5..aa4e2540ee9 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/list-construction.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/list-construction.stdout @@ -8,7 +8,7 @@ let %4: List bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/mixed-projection-chain.stdout b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/mixed-projection-chain.stdout index 20c66f72ad4..012018bc584 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/mixed-projection-chain.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/mixed-projection-chain.stdout @@ -7,7 +7,7 @@ let %3: (field: (Integer,)) bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/multiple-uses.stdout b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/multiple-uses.stdout index 8ca4fd36973..82877ab9d26 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/multiple-uses.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/multiple-uses.stdout @@ -6,7 +6,7 @@ let %2: Boolean bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { @@ -17,7 +17,7 @@ } bb2(): { - goto -> bb3(0) + goto -> bb3(false) } bb3(%0): { @@ -27,13 +27,13 @@ ════ Data Dependency Graph ═════════════════════════════════════════════════════ %0 -> %2 [Param] -%0 -> 0 [Param] +%0 -> false [Param] %1 -> 5 [Load] ════ Transient Data Dependency Graph ═══════════════════════════════════════════ %0 -> %2 [Param] -%0 -> 0 [Param] +%0 -> false [Param] %1 -> 5 [Load] diff --git a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/nested-tuple-projection.stdout b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/nested-tuple-projection.stdout index 6859a829e38..d5d59b6e4e6 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/nested-tuple-projection.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/nested-tuple-projection.stdout @@ -7,7 +7,7 @@ let %3: ((Integer, Integer), Integer) bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/struct-construction.stdout b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/struct-construction.stdout index 777ffdc17f1..e717f20b13a 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/struct-construction.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/struct-construction.stdout @@ -7,7 +7,7 @@ let %3: (bar: Integer, foo: Integer) bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/struct-projection.stdout b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/struct-projection.stdout index 50b026f1818..3fa1685fcf4 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/struct-projection.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/struct-projection.stdout @@ -7,7 +7,7 @@ let %3: (bar: Integer, foo: Integer) bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/tuple-construction.stdout b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/tuple-construction.stdout index ba13b6e042c..cf9ff36b065 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/tuple-construction.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/tuple-construction.stdout @@ -7,7 +7,7 @@ let %3: (Integer, Integer) bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/tuple-projection.stdout b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/tuple-projection.stdout index f3f236ed07f..97a45936f11 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/data-dependency/tuple-projection.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/data-dependency/tuple-projection.stdout @@ -7,7 +7,7 @@ let %3: (Integer, Integer) bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/dse/live-in-branch.stdout b/libs/@local/hashql/mir/tests/ui/pass/dse/live-in-branch.stdout index 8833518c770..9b3362268ec 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/dse/live-in-branch.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/dse/live-in-branch.stdout @@ -6,13 +6,13 @@ let %2: Integer bb0(): { - switchInt(1) -> [0: bb5(), 1: bb1()] + switchInt(true) -> [0: bb5(), 1: bb1()] } bb1(): { %1 = 42 - switchInt(1) -> [0: bb3(), 1: bb2()] + switchInt(true) -> [0: bb3(), 1: bb2()] } bb2(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/dse/nested-tuple-projection.stdout b/libs/@local/hashql/mir/tests/ui/pass/dse/nested-tuple-projection.stdout index 4ebd7c806cb..40a99fde5d0 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/dse/nested-tuple-projection.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/dse/nested-tuple-projection.stdout @@ -7,7 +7,7 @@ let %3: (Integer, Integer) bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/dse/showcase.aux.svg b/libs/@local/hashql/mir/tests/ui/pass/dse/showcase.aux.svg index c5ce3a75f53..890396a5dbf 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/dse/showcase.aux.svg +++ b/libs/@local/hashql/mir/tests/ui/pass/dse/showcase.aux.svg @@ -1,20 +1,20 @@ -Initial MIRMIR after DSE

thunk {thunk#7}() -> Integer

thunk {thunk#7}() -> Integer

-
bb0()
MIR
TswitchInt(1)
bb5()
MIR
Tgoto
bb1()
MIR
0%1 = 10
1%2 = 20
2%3 = %1 == %2
3%4 = %1 < %2
4%5 = %1 > %2
5%6 = 999
TswitchInt(%3)
bb3()
MIR
Tgoto
bb2()
MIR
Tgoto
bb4(%7)
MIR
Tgoto
bb6(%0)
MIR
Treturn %0
bb0()
MIR
0%0 = 0
TswitchInt(%0)
bb1(%1)
MIR
Treturn %1
()0()1()0()1(1)(0)(%7)(0)(0)0(1)1 +
bb0()
MIR
TswitchInt(true)
bb5()
MIR
Tgoto
bb1()
MIR
0%1 = 10
1%2 = 20
2%3 = %1 == %2
3%4 = %1 < %2
4%5 = %1 > %2
5%6 = 999
TswitchInt(%3)
bb3()
MIR
Tgoto
bb2()
MIR
Tgoto
bb4(%7)
MIR
Tgoto
bb6(%0)
MIR
Treturn %0
bb0()
MIR
0%0 = false
TswitchInt(%0)
bb1(%1)
MIR
Treturn %1
()0()1()0()1(1)(0)(%7)(0)(0)0(1)1 - - + + diff --git a/libs/@local/hashql/mir/tests/ui/pass/dse/showcase.stdout b/libs/@local/hashql/mir/tests/ui/pass/dse/showcase.stdout index eb3cbefe936..9682eb28dc7 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/dse/showcase.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/dse/showcase.stdout @@ -11,7 +11,7 @@ let %7: Integer bb0(): { - switchInt(1) -> [0: bb5(), 1: bb1()] + switchInt(true) -> [0: bb5(), 1: bb1()] } bb1(): { @@ -52,7 +52,7 @@ let %1: Integer bb0(): { - %0 = 0 + %0 = false switchInt(%0) -> [0: bb1(0), 1: bb1(1)] } diff --git a/libs/@local/hashql/mir/tests/ui/pass/dse/simple-dead-local.stdout b/libs/@local/hashql/mir/tests/ui/pass/dse/simple-dead-local.stdout index 51be480acc6..9bab20b0019 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/dse/simple-dead-local.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/dse/simple-dead-local.stdout @@ -6,7 +6,7 @@ let %2: Integer bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/dse/used-local-preserved.stdout b/libs/@local/hashql/mir/tests/ui/pass/dse/used-local-preserved.stdout index b05f129d103..1fcab6043ea 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/dse/used-local-preserved.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/dse/used-local-preserved.stdout @@ -5,7 +5,7 @@ let %1: Integer bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/execution/fusion/fuse_backward_chain.snap b/libs/@local/hashql/mir/tests/ui/pass/execution/fusion/fuse_backward_chain.snap index 49532f6b102..e24ae2e5856 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/execution/fusion/fuse_backward_chain.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/execution/fusion/fuse_backward_chain.snap @@ -8,7 +8,7 @@ fn {closure@4294967040}() -> Integer { let %2: Integer bb0(): { - %0 = 1 + %0 = true switchInt(%0) -> [0: bb2(), 1: bb1()] } diff --git a/libs/@local/hashql/mir/tests/ui/pass/execution/fusion/fuse_diamond_non_monotonic_rpo.snap b/libs/@local/hashql/mir/tests/ui/pass/execution/fusion/fuse_diamond_non_monotonic_rpo.snap index 0665de1d9fa..a11ef434516 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/execution/fusion/fuse_diamond_non_monotonic_rpo.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/execution/fusion/fuse_diamond_non_monotonic_rpo.snap @@ -11,7 +11,7 @@ fn {closure@4294967040}() -> Integer { let %5: Integer bb0(): { - %0 = 1 + %0 = true switchInt(%0) -> [0: bb1(), 1: bb2()] } diff --git a/libs/@local/hashql/mir/tests/ui/pass/execution/fusion/fuse_does_not_fuse_join_points.snap b/libs/@local/hashql/mir/tests/ui/pass/execution/fusion/fuse_does_not_fuse_join_points.snap index b29f7c070c7..736dae66f33 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/execution/fusion/fuse_does_not_fuse_join_points.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/execution/fusion/fuse_does_not_fuse_join_points.snap @@ -9,7 +9,7 @@ fn {closure@4294967040}() -> Integer { let %3: Boolean bb0(): { - %3 = 1 + %3 = true switchInt(%3) -> [0: bb2(), 1: bb1()] } diff --git a/libs/@local/hashql/mir/tests/ui/pass/execution/fusion/fuse_updates_branch_references.snap b/libs/@local/hashql/mir/tests/ui/pass/execution/fusion/fuse_updates_branch_references.snap index 0236a965f84..a192b9defd8 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/execution/fusion/fuse_updates_branch_references.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/execution/fusion/fuse_updates_branch_references.snap @@ -10,7 +10,7 @@ fn {closure@4294967040}() -> Integer { bb0(): { %0 = 1 - %3 = 1 + %3 = true switchInt(%3) -> [0: bb2(), 1: bb1()] } diff --git a/libs/@local/hashql/mir/tests/ui/pass/execution/splitting/split_block_references_updated.snap b/libs/@local/hashql/mir/tests/ui/pass/execution/splitting/split_block_references_updated.snap index 86f2ee0a201..64d1ff3c3f9 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/execution/splitting/split_block_references_updated.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/execution/splitting/split_block_references_updated.snap @@ -1,5 +1,5 @@ --- -source: libs/@local/hashql/mir/src/pass/analysis/execution/splitting/tests.rs +source: libs/@local/hashql/mir/src/pass/execution/splitting/tests.rs expression: output --- fn {closure@4294967040}() -> Integer { @@ -20,7 +20,7 @@ fn {closure@4294967040}() -> Integer { } bb2(): { - %2 = 1 // IP + %2 = true // IP switchInt(%2) -> [0: bb4(), 1: bb3()] } diff --git a/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/embedding/other_operations_rejected.snap b/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/embedding/other_operations_rejected.snap index bcfb665f330..fb609ea3efe 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/embedding/other_operations_rejected.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/embedding/other_operations_rejected.snap @@ -24,7 +24,7 @@ fn {graph::read::filter@4294967040}(%0: (Integer,), %1: Entity) -> Boolean { %8 = %0 %9 = closure(({def@123} as FnPtr), %8) %10 = apply %9 1 - %11 = 1 + %11 = true return %11 } diff --git a/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/interpret/all_statements_supported.snap b/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/interpret/all_statements_supported.snap index 49428694348..b9de54f4bb3 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/interpret/all_statements_supported.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/interpret/all_statements_supported.snap @@ -26,7 +26,7 @@ fn {graph::read::filter@4294967040}(%0: (Integer,), %1: Entity) -> Boolean { %9 = closure(({def@42} as FnPtr), %8) // cost: 8 %10 = apply %9 5 // cost: 8 %11 = input LOAD param // cost: 8 - %12 = 1 // cost: 8 + %12 = true // cost: 8 return %12 } diff --git a/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/interpret/eq_opaque_entity_uuid.snap b/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/interpret/eq_opaque_entity_uuid.snap index 9e2c832baef..7ff2c1f6db5 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/interpret/eq_opaque_entity_uuid.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/interpret/eq_opaque_entity_uuid.snap @@ -1,5 +1,6 @@ --- source: libs/@local/hashql/mir/src/pass/execution/statement_placement/tests.rs +assertion_line: 92 expression: output --- fn {graph::read::filter@4294967040}(%0: (), %1: Entity) -> Boolean { diff --git a/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/aggregate_closure_rejected.snap b/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/aggregate_closure_rejected.snap index 2f9c56244ef..1f22a24cd1b 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/aggregate_closure_rejected.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/aggregate_closure_rejected.snap @@ -10,7 +10,7 @@ fn {graph::read::filter@4294967040}(%0: (Integer,), %1: Entity) -> Boolean { bb0(): { %2 = %0.0 // cost: 4 %3 = closure(({def@42} as FnPtr), %2) - %4 = 1 // cost: 4 + %4 = true // cost: 4 return %4 } diff --git a/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/aggregate_tuple_supported.snap b/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/aggregate_tuple_supported.snap index 5fbf3d9d5c2..ca2fefd4973 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/aggregate_tuple_supported.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/aggregate_tuple_supported.snap @@ -10,7 +10,7 @@ fn {graph::read::filter@4294967040}(%0: (), %1: Entity) -> Boolean { bb0(): { %2 = (1, 2) // cost: 4 %3 = (a: 10, b: 20) // cost: 4 - %4 = 1 // cost: 4 + %4 = true // cost: 4 return %4 } diff --git a/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/diamond_must_analysis.snap b/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/diamond_must_analysis.snap index 4d5a3e85593..8db2da9a59a 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/diamond_must_analysis.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/diamond_must_analysis.snap @@ -11,7 +11,7 @@ fn {graph::read::filter@4294967040}(%0: (Integer,), %1: Entity) -> Boolean { let %7: Boolean bb0(): { - %2 = 1 // cost: 4 + %2 = true // cost: 4 %3 = %0.0 // cost: 4 %4 = (%3) // cost: 4 %5 = closure(({def@77} as FnPtr), %4) diff --git a/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/env_dict_non_string_key_rejected.snap b/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/env_dict_non_string_key_rejected.snap index 7d7b5573690..bcceac667eb 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/env_dict_non_string_key_rejected.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/env_dict_non_string_key_rejected.snap @@ -8,7 +8,7 @@ fn {graph::read::filter@4294967040}(%0: (Dict,), %1: Entity) - bb0(): { %2 = %0.0 - %3 = 1 // cost: 4 + %3 = true // cost: 4 return %3 } diff --git a/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/env_dict_opaque_string_key_accepted.snap b/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/env_dict_opaque_string_key_accepted.snap index e43510fd97a..2344681e1dd 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/env_dict_opaque_string_key_accepted.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/env_dict_opaque_string_key_accepted.snap @@ -8,7 +8,7 @@ fn {graph::read::filter@4294967040}(%0: (Dict,), %1: Entity) -> bb0(): { %2 = %0.0 // cost: 4 - %3 = 1 // cost: 4 + %3 = true // cost: 4 return %3 } diff --git a/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/env_dict_string_key_accepted.snap b/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/env_dict_string_key_accepted.snap index 21b8df95642..7f68af1af40 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/env_dict_string_key_accepted.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/env_dict_string_key_accepted.snap @@ -8,7 +8,7 @@ fn {graph::read::filter@4294967040}(%0: (Dict,), %1: Entity) -> bb0(): { %2 = %0.0 // cost: 4 - %3 = 1 // cost: 4 + %3 = true // cost: 4 return %3 } diff --git a/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/fnptr_constant_rejected.snap b/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/fnptr_constant_rejected.snap index ca5104a18ab..2d8920c74a4 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/fnptr_constant_rejected.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/execution/statement_placement/postgres/fnptr_constant_rejected.snap @@ -8,7 +8,7 @@ fn {graph::read::filter@4294967040}(%0: (), %1: Entity) -> Boolean { bb0(): { %2 = ({def@99} as FnPtr) - %3 = 1 // cost: 4 + %3 = true // cost: 4 return %3 } diff --git a/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/chained-projection.stdout b/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/chained-projection.stdout index f9a82ff63c0..a81b474d8b6 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/chained-projection.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/chained-projection.stdout @@ -6,7 +6,7 @@ let %2: ((Integer, Integer), Integer) bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/closure-env-capture.aux.svg b/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/closure-env-capture.aux.svg index fde2ae74498..d4c00dd3e03 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/closure-env-capture.aux.svg +++ b/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/closure-env-capture.aux.svg @@ -1,20 +1,20 @@ -Initial MIRMIR after Forward Substitution

fn f:0(%0: (Integer,)) -> Integer

thunk {thunk#3}() -> Integer

fn f:0(%0: (Integer,)) -> Integer

thunk {thunk#3}() -> Integer

-
bb0()
MIR
0%1 = %0.0
Treturn %1
bb0()
MIR
TswitchInt(1)
bb2()
MIR
Tgoto
bb1()
MIR
0%1 = 42
1%3 = (%1)
2%2 = closure((f:0 as FnPtr), %3)
3%4 = apply %2.0 %2.1
Tgoto
bb3(%0)
MIR
Treturn %0
bb0()
MIR
0%1 = %0.0
Treturn %0.0
bb0()
MIR
0%1 = 42
1%3 = (42)
2%2 = closure((f:0 as FnPtr), %3)
3%4 = apply (f:0 as FnPtr) %3
4%0 = %4
Treturn %4
()0()1(%4)(0) +
bb0()
MIR
0%1 = %0.0
Treturn %1
bb0()
MIR
TswitchInt(true)
bb2()
MIR
Tgoto
bb1()
MIR
0%1 = 42
1%3 = (%1)
2%2 = closure((f:0 as FnPtr), %3)
3%4 = apply %2.0 %2.1
Tgoto
bb3(%0)
MIR
Treturn %0
bb0()
MIR
0%1 = %0.0
Treturn %0.0
bb0()
MIR
0%1 = 42
1%3 = (42)
2%2 = closure((f:0 as FnPtr), %3)
3%4 = apply (f:0 as FnPtr) %3
4%0 = %4
Treturn %4
()0()1(%4)(0) - - + +
diff --git a/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/closure-env-capture.stdout b/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/closure-env-capture.stdout index eed6d71dda2..7ce121cada7 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/closure-env-capture.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/closure-env-capture.stdout @@ -18,7 +18,7 @@ fn f:0(%0: (Integer,)) -> Integer { let %4: Integer bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/nested.stdout b/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/nested.stdout index 53dae269b67..16ff1730f33 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/nested.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/nested.stdout @@ -8,7 +8,7 @@ let %4: String bb0(): { - switchInt(0) -> [0: bb8(), 1: bb1()] + switchInt(false) -> [0: bb8(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/param-const-agree.stdout b/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/param-const-agree.stdout index 3355a32d5cf..43729ea555d 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/param-const-agree.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/param-const-agree.stdout @@ -7,7 +7,7 @@ let %3: (Integer,) bb0(): { - switchInt(1) -> [0: bb5(), 1: bb1()] + switchInt(true) -> [0: bb5(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/param-const-diverge.stdout b/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/param-const-diverge.stdout index ed77911f70b..01d866e87d0 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/param-const-diverge.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/param-const-diverge.stdout @@ -7,7 +7,7 @@ let %3: (Integer,) bb0(): { - switchInt(1) -> [0: bb5(), 1: bb1()] + switchInt(true) -> [0: bb5(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/tuple-projection.stdout b/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/tuple-projection.stdout index bce38641714..4a2a595ff75 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/tuple-projection.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/forward_substitution/tuple-projection.stdout @@ -6,7 +6,7 @@ let %2: (Integer,) bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/inline/heuristic-inline.stdout b/libs/@local/hashql/mir/tests/ui/pass/inline/heuristic-inline.stdout index a4cb383a913..1baf94ea0b9 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inline/heuristic-inline.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/inline/heuristic-inline.stdout @@ -44,7 +44,7 @@ fn {closure#10}(%0: ()) -> Boolean { } bb2(): { - goto -> bb3(0) + goto -> bb3(false) } bb3(%3): { @@ -115,7 +115,7 @@ fn {closure#10}(%0: ()) -> Boolean { } bb2(): { - return 0 + return false } } @@ -182,7 +182,7 @@ fn {closure#10}(%0: ()) -> Boolean { } bb2(): { - return 0 + return false } bb3(%2): { @@ -249,7 +249,7 @@ thunk use_flag:0() -> () -> Boolean { } bb4(): { - goto -> bb1(0) + goto -> bb1(false) } bb5(%3): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-and-false.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-and-false.stdout index 9caa65ae28a..d45548d02ea 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-and-false.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-and-false.stdout @@ -6,18 +6,18 @@ let %2: Boolean bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { %1 = 1 == 1 - %2 = %1 & 0 + %2 = %1 & false goto -> bb3(%2) } bb2(): { - goto -> bb3(1) + goto -> bb3(true) } bb3(%0): { @@ -32,8 +32,8 @@ let %2: Boolean bb0(): { - %1 = 1 - %2 = 0 + %1 = true + %2 = false %0 = %2 return %2 diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-or-true.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-or-true.stdout index 659b50f806b..5f208a87668 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-or-true.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/annihilator-or-true.stdout @@ -6,18 +6,18 @@ let %2: Boolean bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { %1 = 1 == 2 - %2 = %1 | 1 + %2 = %1 | true goto -> bb3(%2) } bb2(): { - goto -> bb3(0) + goto -> bb3(false) } bb3(%0): { @@ -32,8 +32,8 @@ let %2: Boolean bb0(): { - %1 = 0 - %2 = 1 + %1 = false + %2 = true %0 = %2 return %2 diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/block_param_predecessors_agree.snap b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/block_param_predecessors_agree.snap index 4076cb923c7..44cbdd05195 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/block_param_predecessors_agree.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/block_param_predecessors_agree.snap @@ -44,7 +44,7 @@ fn {closure@4294967040}(%0: Integer) -> Boolean { } bb3(%1): { - %2 = 1 + %2 = true return %2 } diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/block_param_single_predecessor.snap b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/block_param_single_predecessor.snap index ca811c89d69..53ecb4e7b4e 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/block_param_single_predecessor.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/block_param_single_predecessor.snap @@ -28,7 +28,7 @@ fn {closure@4294967040}() -> Boolean { } bb1(%0): { - %1 = 1 + %1 = true return %1 } diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/chained-const-fold.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/chained-const-fold.stdout index df3c51fa9a1..b8d5d5253a8 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/chained-const-fold.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/chained-const-fold.stdout @@ -6,18 +6,18 @@ let %2: Boolean bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { %1 = 1 == 2 - %2 = %1 == 0 + %2 = %1 == false goto -> bb3(%2) } bb2(): { - goto -> bb3(0) + goto -> bb3(false) } bb3(%0): { @@ -32,8 +32,8 @@ let %2: Boolean bb0(): { - %1 = 0 - %2 = 1 + %1 = false + %2 = true %0 = %2 return %2 diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-eq.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-eq.stdout index 99d9962fab4..651015cc1b4 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-eq.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-eq.stdout @@ -5,7 +5,7 @@ let %1: Boolean bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { @@ -15,7 +15,7 @@ } bb2(): { - goto -> bb3(0) + goto -> bb3(false) } bb3(%0): { @@ -29,7 +29,7 @@ let %1: Boolean bb0(): { - %1 = 0 + %1 = false %0 = %1 return %1 diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gt.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gt.stdout index 6119322d227..fa453f665e4 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gt.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gt.stdout @@ -5,7 +5,7 @@ let %1: Boolean bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { @@ -15,7 +15,7 @@ } bb2(): { - goto -> bb3(0) + goto -> bb3(false) } bb3(%0): { @@ -29,7 +29,7 @@ let %1: Boolean bb0(): { - %1 = 1 + %1 = true %0 = %1 return %1 diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gte.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gte.stdout index 800a6493c6b..5d70454773b 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gte.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-gte.stdout @@ -5,7 +5,7 @@ let %1: Boolean bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { @@ -15,7 +15,7 @@ } bb2(): { - goto -> bb3(0) + goto -> bb3(false) } bb3(%0): { @@ -29,7 +29,7 @@ let %1: Boolean bb0(): { - %1 = 1 + %1 = true %0 = %1 return %1 diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lt.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lt.stdout index dfd17f5efb1..7b6dfbb428d 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lt.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lt.stdout @@ -5,7 +5,7 @@ let %1: Boolean bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { @@ -15,7 +15,7 @@ } bb2(): { - goto -> bb3(0) + goto -> bb3(false) } bb3(%0): { @@ -29,7 +29,7 @@ let %1: Boolean bb0(): { - %1 = 1 + %1 = true %0 = %1 return %1 diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lte.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lte.stdout index 31efd47a78a..00799124358 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lte.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-lte.stdout @@ -5,7 +5,7 @@ let %1: Boolean bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { @@ -15,7 +15,7 @@ } bb2(): { - goto -> bb3(0) + goto -> bb3(false) } bb3(%0): { @@ -29,7 +29,7 @@ let %1: Boolean bb0(): { - %1 = 1 + %1 = true %0 = %1 return %1 diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-ne.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-ne.stdout index 83f22bbc9be..a53a7953bd9 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-ne.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-fold-ne.stdout @@ -5,7 +5,7 @@ let %1: Boolean bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { @@ -15,7 +15,7 @@ } bb2(): { - goto -> bb3(0) + goto -> bb3(false) } bb3(%0): { @@ -29,7 +29,7 @@ let %1: Boolean bb0(): { - %1 = 1 + %1 = true %0 = %1 return %1 diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-propagation-locals.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-propagation-locals.stdout index f1497ac6368..87c63fa0da7 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-propagation-locals.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const-propagation-locals.stdout @@ -7,7 +7,7 @@ let %3: Boolean bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { @@ -19,7 +19,7 @@ } bb2(): { - goto -> bb3(1) + goto -> bb3(true) } bb3(%0): { @@ -37,7 +37,7 @@ bb0(): { %1 = 5 %2 = 3 - %3 = 0 + %3 = false %0 = %3 return %3 diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const_fold_unary_not.snap b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const_fold_unary_not.snap index 5d9c5c55aac..69cf3ba4f19 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const_fold_unary_not.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/const_fold_unary_not.snap @@ -6,7 +6,7 @@ fn {closure@4294967040}() -> Boolean { let %0: Boolean bb0(): { - %0 = !1 + %0 = !true return %0 } @@ -18,7 +18,7 @@ fn {closure@4294967040}() -> Boolean { let %0: Boolean bb0(): { - %0 = 0 + %0 = false return %0 } diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/idempotent_to_const_forwarding.snap b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/idempotent_to_const_forwarding.snap index 7ec35a3e5b1..69766cf5317 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/idempotent_to_const_forwarding.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/idempotent_to_const_forwarding.snap @@ -26,7 +26,7 @@ fn {closure@4294967040}() -> Boolean { bb0(): { %0 = 42 %1 = 42 - %2 = 1 + %2 = true return %2 } diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-eq.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-eq.stdout index 47b282f0974..b5a5ad65590 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-eq.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-eq.stdout @@ -6,7 +6,7 @@ let %2: Boolean bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { @@ -17,7 +17,7 @@ } bb2(): { - goto -> bb3(0) + goto -> bb3(false) } bb3(%0): { @@ -33,7 +33,7 @@ bb0(): { %1 = 42 - %2 = 1 + %2 = true %0 = %2 return %2 diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gt.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gt.stdout index 87ce2f08dd9..9def0cf9661 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gt.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gt.stdout @@ -6,7 +6,7 @@ let %2: Boolean bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { @@ -17,7 +17,7 @@ } bb2(): { - goto -> bb3(1) + goto -> bb3(true) } bb3(%0): { @@ -33,7 +33,7 @@ bb0(): { %1 = 42 - %2 = 0 + %2 = false %0 = %2 return %2 diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gte.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gte.stdout index 5e8779c91ff..21d87c8319f 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gte.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-gte.stdout @@ -6,7 +6,7 @@ let %2: Boolean bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { @@ -17,7 +17,7 @@ } bb2(): { - goto -> bb3(0) + goto -> bb3(false) } bb3(%0): { @@ -33,7 +33,7 @@ bb0(): { %1 = 42 - %2 = 1 + %2 = true %0 = %2 return %2 diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lt.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lt.stdout index dcc74bb2a59..e2fc225ae45 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lt.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lt.stdout @@ -6,7 +6,7 @@ let %2: Boolean bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { @@ -17,7 +17,7 @@ } bb2(): { - goto -> bb3(1) + goto -> bb3(true) } bb3(%0): { @@ -33,7 +33,7 @@ bb0(): { %1 = 42 - %2 = 0 + %2 = false %0 = %2 return %2 diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lte.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lte.stdout index a582e4b8621..7b1b899d3af 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lte.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-lte.stdout @@ -6,7 +6,7 @@ let %2: Boolean bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { @@ -17,7 +17,7 @@ } bb2(): { - goto -> bb3(0) + goto -> bb3(false) } bb3(%0): { @@ -33,7 +33,7 @@ bb0(): { %1 = 42 - %2 = 1 + %2 = true %0 = %2 return %2 diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-ne.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-ne.stdout index f0ee0186aab..2691a09a28d 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-ne.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identical-operand-ne.stdout @@ -6,7 +6,7 @@ let %2: Boolean bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { @@ -17,7 +17,7 @@ } bb2(): { - goto -> bb3(1) + goto -> bb3(true) } bb3(%0): { @@ -33,7 +33,7 @@ bb0(): { %1 = 42 - %2 = 0 + %2 = false %0 = %2 return %2 diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-and-true.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-and-true.stdout index f66fb763a75..14620b2ae12 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-and-true.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-and-true.stdout @@ -6,18 +6,18 @@ let %2: Boolean bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { %1 = 1 == 1 - %2 = %1 & 1 + %2 = %1 & true goto -> bb3(%2) } bb2(): { - goto -> bb3(0) + goto -> bb3(false) } bb3(%0): { @@ -32,8 +32,8 @@ let %2: Boolean bb0(): { - %1 = 1 - %2 = 1 + %1 = true + %2 = true %0 = %2 return %2 diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-or-false.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-or-false.stdout index 81aae683325..999e55245c5 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-or-false.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/identity-or-false.stdout @@ -6,18 +6,18 @@ let %2: Boolean bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { %1 = 1 == 1 - %2 = %1 | 0 + %2 = %1 | false goto -> bb3(%2) } bb2(): { - goto -> bb3(0) + goto -> bb3(false) } bb3(%0): { @@ -32,8 +32,8 @@ let %2: Boolean bb0(): { - %1 = 1 - %2 = 1 + %1 = true + %2 = true %0 = %2 return %2 diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/showcase.aux.svg b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/showcase.aux.svg index 5d18449c258..fbab39c449e 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/showcase.aux.svg +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/showcase.aux.svg @@ -1,20 +1,20 @@ -Initial MIRMIR after InstSimplify

thunk {thunk#7}() -> Boolean

-

thunk {thunk#7}() -> Boolean

-
bb0()
MIR
TswitchInt(1)
bb2()
MIR
Tgoto
bb1()
MIR
0%1 = 1 == 2
1%2 = 3 < 5
2%3 = %2 & 1
3%4 = 42
4%5 = %4 == %4
5%6 = %1 | %3
6%7 = %6 & %5
Tgoto
bb3(%0)
MIR
Treturn %0
bb0()
MIR
0%1 = 0
1%2 = 1
2%3 = 1
3%4 = 42
4%5 = 1
5%6 = 1
6%7 = 1
7%0 = %7
Treturn %7
()0()1(%7)(0) - - - +Initial MIRMIR after InstSimplify

thunk {thunk#7}() -> Boolean

+

thunk {thunk#7}() -> Boolean

+
bb0()
MIR
TswitchInt(true)
bb2()
MIR
Tgoto
bb1()
MIR
0%1 = 1 == 2
1%2 = 3 < 5
2%3 = %2 & true
3%4 = 42
4%5 = %4 == %4
5%6 = %1 | %3
6%7 = %6 & %5
Tgoto
bb3(%0)
MIR
Treturn %0
bb0()
MIR
0%1 = false
1%2 = true
2%3 = true
3%4 = 42
4%5 = true
5%6 = true
6%7 = true
7%0 = %7
Treturn %7
()0()1(%7)(false) + + + - +
diff --git a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/showcase.stdout b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/showcase.stdout index 04d56dc87fc..21593fd4883 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/showcase.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/inst_simplify/showcase.stdout @@ -11,13 +11,13 @@ let %7: Boolean bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { %1 = 1 == 2 %2 = 3 < 5 - %3 = %2 & 1 + %3 = %2 & true %4 = 42 %5 = %4 == %4 %6 = %1 | %3 @@ -27,7 +27,7 @@ } bb2(): { - goto -> bb3(0) + goto -> bb3(false) } bb3(%0): { @@ -47,13 +47,13 @@ let %7: Boolean bb0(): { - %1 = 0 - %2 = 1 - %3 = 1 + %1 = false + %2 = true + %3 = true %4 = 42 - %5 = 1 - %6 = 1 - %7 = 1 + %5 = true + %6 = true + %7 = true %0 = %7 return %7 diff --git a/libs/@local/hashql/mir/tests/ui/pass/post_inline/cascading-simplification.stdout b/libs/@local/hashql/mir/tests/ui/pass/post_inline/cascading-simplification.stdout index f1ce901c23a..e2bf218f858 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/post_inline/cascading-simplification.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/post_inline/cascading-simplification.stdout @@ -354,18 +354,18 @@ thunk check_both:0() -> (Boolean, Boolean) -> Boolean { thunk {thunk#7}() -> Boolean { bb0(): { - return 1 + return true } } thunk {thunk#8}() -> Boolean { bb0(): { - return 1 + return true } } *thunk {thunk#9}() -> Boolean { bb0(): { - return 1 + return true } } \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/post_inline/closure-env-cleanup.stdout b/libs/@local/hashql/mir/tests/ui/pass/post_inline/closure-env-cleanup.stdout index 216c8f925b6..73938de717e 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/post_inline/closure-env-cleanup.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/post_inline/closure-env-cleanup.stdout @@ -246,6 +246,6 @@ thunk checker:0() -> (Integer) -> Boolean { *thunk {thunk#6}() -> Boolean { bb0(): { - return 1 + return true } } \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/post_inline/constant-propagation-after-inline.stdout b/libs/@local/hashql/mir/tests/ui/pass/post_inline/constant-propagation-after-inline.stdout index c0fce5dcfd5..b81e60f02af 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/post_inline/constant-propagation-after-inline.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/post_inline/constant-propagation-after-inline.stdout @@ -136,6 +136,6 @@ thunk is_positive:0() -> (Integer) -> Boolean { *thunk {thunk#3}() -> Boolean { bb0(): { - return 1 + return true } } \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/post_inline/dead-code-from-inline.stdout b/libs/@local/hashql/mir/tests/ui/pass/post_inline/dead-code-from-inline.stdout index 2d995977c96..06c6d41ace6 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/post_inline/dead-code-from-inline.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/post_inline/dead-code-from-inline.stdout @@ -38,7 +38,7 @@ thunk select_branch:0() -> (Boolean) -> String { bb0(): { %0 = apply (select_branch:0 as FnPtr) - %1 = apply %0.0 %0.1 1 + %1 = apply %0.0 %0.1 true return %1 } @@ -74,7 +74,7 @@ thunk select_branch:0() -> (Boolean) -> String { let %0: String bb0(): { - %0 = apply ({closure#4} as FnPtr) () 1 + %0 = apply ({closure#4} as FnPtr) () true return %0 } @@ -113,7 +113,7 @@ thunk select_branch:0() -> (Boolean) -> String { bb0(): { %1 = () - %2 = 1 + %2 = true goto -> bb2() } diff --git a/libs/@local/hashql/mir/tests/ui/pass/post_inline/nested-branch-elimination.stdout b/libs/@local/hashql/mir/tests/ui/pass/post_inline/nested-branch-elimination.stdout index 58d5b807f80..122a85c7110 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/post_inline/nested-branch-elimination.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/post_inline/nested-branch-elimination.stdout @@ -339,7 +339,7 @@ thunk check_low:0() -> (Integer) -> Boolean { thunk {thunk#8}() -> Boolean { bb0(): { - return 0 + return false } } diff --git a/libs/@local/hashql/mir/tests/ui/pass/pre_inline/basic-constant-folding.stdout b/libs/@local/hashql/mir/tests/ui/pass/pre_inline/basic-constant-folding.stdout index 2c28db7b180..23571c37f50 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/pre_inline/basic-constant-folding.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/pre_inline/basic-constant-folding.stdout @@ -4,7 +4,7 @@ let %0: String bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/pre_inline/chain-simplification.stdout b/libs/@local/hashql/mir/tests/ui/pass/pre_inline/chain-simplification.stdout index f11d588671d..13f25b39434 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/pre_inline/chain-simplification.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/pre_inline/chain-simplification.stdout @@ -8,21 +8,21 @@ let %4: String bb0(): { - switchInt(1) -> [0: bb11(), 1: bb1()] + switchInt(true) -> [0: bb11(), 1: bb1()] } bb1(): { - %1 = 1 + %1 = true switchInt(%1) -> [0: bb3(), 1: bb2()] } bb2(): { - goto -> bb4(1) + goto -> bb4(true) } bb3(): { - goto -> bb4(0) + goto -> bb4(false) } bb4(%2): { @@ -30,11 +30,11 @@ } bb5(): { - goto -> bb7(1) + goto -> bb7(true) } bb6(): { - goto -> bb7(0) + goto -> bb7(false) } bb7(%3): { @@ -54,7 +54,7 @@ } bb11(): { - goto -> bb12(0) + goto -> bb12(false) } bb12(%0): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/pre_inline/closure-with-dead-branch.stdout b/libs/@local/hashql/mir/tests/ui/pass/pre_inline/closure-with-dead-branch.stdout index 8f3929550d0..e79b71e2c40 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/pre_inline/closure-with-dead-branch.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/pre_inline/closure-with-dead-branch.stdout @@ -24,7 +24,7 @@ thunk {thunk#2}() -> Boolean { bb0(): { %0 = apply (identity:0 as FnPtr) - %1 = apply %0.0 %0.1 1 + %1 = apply %0.0 %0.1 true return %1 } @@ -73,7 +73,7 @@ thunk identity:0() -> (Boolean) -> Boolean { thunk {thunk#2}() -> Boolean { bb0(): { - return 1 + return true } } diff --git a/libs/@local/hashql/mir/tests/ui/pass/pre_inline/dead-code-after-propagation.stdout b/libs/@local/hashql/mir/tests/ui/pass/pre_inline/dead-code-after-propagation.stdout index 5fb3924f24d..20503ff7ed5 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/pre_inline/dead-code-after-propagation.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/pre_inline/dead-code-after-propagation.stdout @@ -6,7 +6,7 @@ let %2: Integer bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/pre_inline/inst-simplify-with-propagation.stdout b/libs/@local/hashql/mir/tests/ui/pass/pre_inline/inst-simplify-with-propagation.stdout index 896c41dacba..8af07a3e767 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/pre_inline/inst-simplify-with-propagation.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/pre_inline/inst-simplify-with-propagation.stdout @@ -2,7 +2,7 @@ thunk x:0() -> Boolean { bb0(): { - return 1 + return true } } @@ -33,7 +33,7 @@ thunk x:0() -> Boolean { thunk x:0() -> Boolean { bb0(): { - return 1 + return true } } diff --git a/libs/@local/hashql/mir/tests/ui/pass/pre_inline/nested-if-constant.stdout b/libs/@local/hashql/mir/tests/ui/pass/pre_inline/nested-if-constant.stdout index de1185c8bc2..ea8c79c10cd 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/pre_inline/nested-if-constant.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/pre_inline/nested-if-constant.stdout @@ -6,15 +6,15 @@ let %2: String bb0(): { - switchInt(1) -> [0: bb8(), 1: bb1()] + switchInt(true) -> [0: bb8(), 1: bb1()] } bb1(): { - switchInt(0) -> [0: bb2(), 1: bb5()] + switchInt(false) -> [0: bb2(), 1: bb5()] } bb2(): { - switchInt(1) -> [0: bb4(), 1: bb3()] + switchInt(true) -> [0: bb4(), 1: bb3()] } bb3(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/pre_inline/nested-let-cleanup.stdout b/libs/@local/hashql/mir/tests/ui/pass/pre_inline/nested-let-cleanup.stdout index 152ef88be46..1f773d15ae3 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/pre_inline/nested-let-cleanup.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/pre_inline/nested-let-cleanup.stdout @@ -7,7 +7,7 @@ let %3: Integer bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/pass/pre_inline/thunk-with-dead-code.stdout b/libs/@local/hashql/mir/tests/ui/pass/pre_inline/thunk-with-dead-code.stdout index c23b973b61c..d0802f2d6ba 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/pre_inline/thunk-with-dead-code.stdout +++ b/libs/@local/hashql/mir/tests/ui/pass/pre_inline/thunk-with-dead-code.stdout @@ -2,7 +2,7 @@ fn {closure#4}(%0: ()) -> Boolean { bb0(): { - return 1 + return true } } @@ -63,7 +63,7 @@ thunk unused:0() -> String { fn {closure#4}(%0: ()) -> Boolean { bb0(): { - return 1 + return true } } @@ -79,7 +79,7 @@ thunk get_flag:0() -> () -> Boolean { thunk flag:0() -> Boolean { bb0(): { - return 1 + return true } } diff --git a/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/block_param_def_with_sibling_assignment.snap b/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/block_param_def_with_sibling_assignment.snap new file mode 100644 index 00000000000..1346588c343 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/block_param_def_with_sibling_assignment.snap @@ -0,0 +1,48 @@ +--- +source: libs/@local/hashql/mir/src/pass/transform/ssa_repair/tests.rs +expression: value +--- +fn {closure@4294967040}() -> Integer { + let %0: Integer + let %1: Boolean + + bb0(): { + %1 = true + + switchInt(%1) -> [0: bb1(0), 1: bb2()] + } + + bb1(%0): { + return %0 + } + + bb2(): { + %0 = 1 + + return %0 + } +} + +================== Changed: Yes ================== + +fn {closure@4294967040}() -> Integer { + let %0: Integer + let %1: Boolean + let %2: Integer + + bb0(): { + %1 = true + + switchInt(%1) -> [0: bb1(0), 1: bb2()] + } + + bb1(%2): { + return %2 + } + + bb2(): { + %0 = 1 + + return %0 + } +} diff --git a/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/block_param_def_with_sibling_assignment2.snap b/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/block_param_def_with_sibling_assignment2.snap new file mode 100644 index 00000000000..a0bd400e2fb --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/block_param_def_with_sibling_assignment2.snap @@ -0,0 +1,48 @@ +--- +source: libs/@local/hashql/mir/src/pass/transform/ssa_repair/tests.rs +expression: value +--- +fn {closure@4294967040}() -> Integer { + let %0: Integer + let %1: Boolean + + bb0(): { + %1 = true + + switchInt(%1) -> [0: bb2(0), 1: bb1()] + } + + bb1(): { + %0 = 1 + + return %0 + } + + bb2(%0): { + return %0 + } +} + +================== Changed: Yes ================== + +fn {closure@4294967040}() -> Integer { + let %0: Integer + let %1: Boolean + let %2: Integer + + bb0(): { + %1 = true + + switchInt(%1) -> [0: bb2(0), 1: bb1()] + } + + bb1(): { + %2 = 1 + + return %2 + } + + bb2(%0): { + return %0 + } +} diff --git a/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/diamond_both_branches_define.snap b/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/diamond_both_branches_define.snap index f7e49fa775e..c9e1d33abec 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/diamond_both_branches_define.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/diamond_both_branches_define.snap @@ -7,7 +7,7 @@ fn {closure@4294967040}() -> Null { let %1: Boolean bb0(): { - %1 = 1 + %1 = true switchInt(%1) -> [0: bb2(), 1: bb1()] } @@ -41,7 +41,7 @@ fn {closure@4294967040}() -> Null { let %4: Boolean bb0(): { - %4 = 1 + %4 = true switchInt(%4) -> [0: bb2(), 1: bb1()] } diff --git a/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/diamond_one_branch_redefines.snap b/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/diamond_one_branch_redefines.snap index 70faaa5c3fb..78fed6cad3f 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/diamond_one_branch_redefines.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/diamond_one_branch_redefines.snap @@ -7,7 +7,7 @@ fn {closure@4294967040}() -> Null { let %1: Boolean bb0(): { - %1 = 1 + %1 = true %0 = 1 switchInt(%1) -> [0: bb2(), 1: bb1()] @@ -40,7 +40,7 @@ fn {closure@4294967040}() -> Null { let %4: Boolean bb0(): { - %4 = 1 + %4 = true %2 = 1 switchInt(%4) -> [0: bb2(), 1: bb1()] diff --git a/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/irreducible.snap b/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/irreducible.snap index 7e93e1ddf1b..d066eebf180 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/irreducible.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/irreducible.snap @@ -10,8 +10,8 @@ fn {closure@4294967040}() -> Null { bb0(): { %0 = 0 - %2 = 1 - %3 = 1 + %2 = true + %3 = true switchInt(%2) -> [0: bb2(), 1: bb1()] } @@ -53,8 +53,8 @@ fn {closure@4294967040}() -> Null { bb0(): { %4 = 0 - %2 = 1 - %3 = 1 + %2 = true + %3 = true switchInt(%2) -> [0: bb2(%4), 1: bb1(%4)] } diff --git a/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/loop_with_conditional_def.snap b/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/loop_with_conditional_def.snap index a0cd46b2f06..06591200ed4 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/loop_with_conditional_def.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/loop_with_conditional_def.snap @@ -10,8 +10,8 @@ fn {closure@4294967040}() -> Null { bb0(): { %0 = 0 - %2 = 1 - %3 = 1 + %2 = true + %3 = true goto -> bb1() } @@ -54,8 +54,8 @@ fn {closure@4294967040}() -> Null { bb0(): { %4 = 0 - %2 = 1 - %3 = 1 + %2 = true + %3 = true goto -> bb1(%4) } diff --git a/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/multiple_variables_violated.snap b/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/multiple_variables_violated.snap index a045cd4613c..b2ec7d60aa1 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/multiple_variables_violated.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/multiple_variables_violated.snap @@ -9,7 +9,7 @@ fn {closure@4294967040}() -> Null { let %3: Boolean bb0(): { - %3 = 1 + %3 = true switchInt(%3) -> [0: bb2(), 1: bb1()] } @@ -48,7 +48,7 @@ fn {closure@4294967040}() -> Null { let %7: Integer bb0(): { - %3 = 1 + %3 = true switchInt(%3) -> [0: bb2(), 1: bb1()] } diff --git a/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/nested_loop.snap b/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/nested_loop.snap index d5bf4d89060..a11cc0f6c76 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/nested_loop.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/nested_loop.snap @@ -9,8 +9,8 @@ fn {closure@4294967040}() -> Null { bb0(): { %0 = 0 - %1 = 1 - %2 = 1 + %1 = true + %2 = true goto -> bb1() } @@ -48,8 +48,8 @@ fn {closure@4294967040}() -> Null { bb0(): { %3 = 0 - %6 = 1 - %2 = 1 + %6 = true + %2 = true goto -> bb1(%3) } diff --git a/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/simple_loop.snap b/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/simple_loop.snap index 8d899c55a1c..cbf7269ff6c 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/simple_loop.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/ssa_repair/simple_loop.snap @@ -8,7 +8,7 @@ fn {closure@4294967040}() -> Null { bb0(): { %0 = 0 - %1 = 1 + %1 = true goto -> bb1() } @@ -36,7 +36,7 @@ fn {closure@4294967040}() -> Null { bb0(): { %2 = 0 - %4 = 1 + %4 = true goto -> bb1(%2) } diff --git a/libs/@local/hashql/mir/tests/ui/reify/closure-captured-var.stdout b/libs/@local/hashql/mir/tests/ui/reify/closure-captured-var.stdout index 3c40b9af226..fc4c68ce055 100644 --- a/libs/@local/hashql/mir/tests/ui/reify/closure-captured-var.stdout +++ b/libs/@local/hashql/mir/tests/ui/reify/closure-captured-var.stdout @@ -55,7 +55,7 @@ fn {ctor#::core::option::None}(%0: ()) -> ::core::option::None { let %8: ::core::option::None bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/reify/dict-computed-key.stdout b/libs/@local/hashql/mir/tests/ui/reify/dict-computed-key.stdout index fb5d0471fc3..5b5d74cd674 100644 --- a/libs/@local/hashql/mir/tests/ui/reify/dict-computed-key.stdout +++ b/libs/@local/hashql/mir/tests/ui/reify/dict-computed-key.stdout @@ -16,7 +16,7 @@ thunk foo:0() -> Boolean { bb0(): { %0 = apply (foo:0 as FnPtr) %1 = apply (foo:0 as FnPtr) - %2 = dict(%0: 42, 1: %1) + %2 = dict(%0: 42, true: %1) return %2 } diff --git a/libs/@local/hashql/mir/tests/ui/reify/nested-if.aux.svg b/libs/@local/hashql/mir/tests/ui/reify/nested-if.aux.svg index c62ff49a4b6..67d7bfaa53b 100644 --- a/libs/@local/hashql/mir/tests/ui/reify/nested-if.aux.svg +++ b/libs/@local/hashql/mir/tests/ui/reify/nested-if.aux.svg @@ -1,20 +1,20 @@ -

fn {ctor#::core::option::Some}(%0: (), %1: String) -> ::core::option::Some

fn {ctor#::core::option::None}(%0: ()) -> ::core::option::None

thunk {thunk#8}() -> ::core::option::None | ::core::option::Some

-
bb0()
MIR
0%2 = opaque(::core::option::Some, %1)
Treturn %2
bb0()
MIR
0%1 = opaque(::core::option::None, ())
Treturn %1
bb0()
MIR
TswitchInt(1)
bb8()
MIR
0%7 = closure(({ctor#::core::option::None} as FnPtr), ())
1%8 = apply %7.0 %7.1
Tgoto
bb1()
MIR
0%1 = 2 <= 3
TswitchInt(%1)
bb6()
MIR
Tgoto
bb2()
MIR
0%3 = 2 >= 3
TswitchInt(%3)
bb4()
MIR
Tgoto
bb3()
MIR
Tgoto
bb5(%4)
MIR
Tgoto
bb7(%2)
MIR
0%5 = closure(({ctor#::core::option::Some} as FnPtr), ())
1%6 = apply %5.0 %5.1 %2
Tgoto
bb9(%0)
MIR
Treturn %0
()0()1()0()1()0()1("baz")("qux")(%4)("bar")(%6)(%8) +
bb0()
MIR
0%2 = opaque(::core::option::Some, %1)
Treturn %2
bb0()
MIR
0%1 = opaque(::core::option::None, ())
Treturn %1
bb0()
MIR
TswitchInt(true)
bb8()
MIR
0%7 = closure(({ctor#::core::option::None} as FnPtr), ())
1%8 = apply %7.0 %7.1
Tgoto
bb1()
MIR
0%1 = 2 <= 3
TswitchInt(%1)
bb6()
MIR
Tgoto
bb2()
MIR
0%3 = 2 >= 3
TswitchInt(%3)
bb4()
MIR
Tgoto
bb3()
MIR
Tgoto
bb5(%4)
MIR
Tgoto
bb7(%2)
MIR
0%5 = closure(({ctor#::core::option::Some} as FnPtr), ())
1%6 = apply %5.0 %5.1 %2
Tgoto
bb9(%0)
MIR
Treturn %0
()0()1()0()1()0()1("baz")("qux")(%4)("bar")(%6)(%8) - - + + diff --git a/libs/@local/hashql/mir/tests/ui/reify/nested-if.stdout b/libs/@local/hashql/mir/tests/ui/reify/nested-if.stdout index e879da300d5..23055c69c41 100644 --- a/libs/@local/hashql/mir/tests/ui/reify/nested-if.stdout +++ b/libs/@local/hashql/mir/tests/ui/reify/nested-if.stdout @@ -30,7 +30,7 @@ fn {ctor#::core::option::None}(%0: ()) -> ::core::option::None { let %8: ::core::option::None bb0(): { - switchInt(1) -> [0: bb8(), 1: bb1()] + switchInt(true) -> [0: bb8(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/reify/nested-let.stdout b/libs/@local/hashql/mir/tests/ui/reify/nested-let.stdout index 9ced4555614..d8f4f4c48ae 100644 --- a/libs/@local/hashql/mir/tests/ui/reify/nested-let.stdout +++ b/libs/@local/hashql/mir/tests/ui/reify/nested-let.stdout @@ -28,7 +28,7 @@ fn {ctor#::core::option::None}(%0: ()) -> ::core::option::None { let %6: ::core::option::None bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): { diff --git a/libs/@local/hashql/mir/tests/ui/reify/reassign.stdout b/libs/@local/hashql/mir/tests/ui/reify/reassign.stdout index cadac5f3edc..ffa9c0f1aaa 100644 --- a/libs/@local/hashql/mir/tests/ui/reify/reassign.stdout +++ b/libs/@local/hashql/mir/tests/ui/reify/reassign.stdout @@ -28,7 +28,7 @@ fn {ctor#::core::option::None}(%0: ()) -> ::core::option::None { let %6: ::core::option::None bb0(): { - switchInt(1) -> [0: bb2(), 1: bb1()] + switchInt(true) -> [0: bb2(), 1: bb1()] } bb1(): {