diff --git a/libs/@local/graph/api/src/rest/hashql/compile.rs b/libs/@local/graph/api/src/rest/hashql/compile.rs index f871e725fed..2bee6e95276 100644 --- a/libs/@local/graph/api/src/rest/hashql/compile.rs +++ b/libs/@local/graph/api/src/rest/hashql/compile.rs @@ -130,6 +130,7 @@ impl<'heap> Compilation<'heap> { HashQlDiagnosticCategory::Mir(MirDiagnosticCategory::Reify(category)) }) .with_diagnostics(advisories)?; + scratch.reset(); // Lower the MIR let Success { diff --git a/libs/@local/hashql/core/src/symbol/mod.rs b/libs/@local/hashql/core/src/symbol/mod.rs index 15a53b1da66..994025410df 100644 --- a/libs/@local/hashql/core/src/symbol/mod.rs +++ b/libs/@local/hashql/core/src/symbol/mod.rs @@ -258,6 +258,15 @@ impl<'heap> Symbol<'heap> { } } +impl From for Symbol<'_> { + #[inline] + #[expect(unsafe_code)] + fn from(value: ConstantSymbol) -> Self { + // SAFETY: The constant symbol is already interned, so the repr is valid. + unsafe { Symbol::from_repr(Repr::constant(value.repr)) } + } +} + impl AsRef for Symbol<'_> { #[inline] fn as_ref(&self) -> &Self { diff --git a/libs/@local/hashql/core/src/symbol/sym.rs b/libs/@local/hashql/core/src/symbol/sym.rs index fcb3a6a25eb..7b7ee110f3b 100644 --- a/libs/@local/hashql/core/src/symbol/sym.rs +++ b/libs/@local/hashql/core/src/symbol/sym.rs @@ -206,6 +206,8 @@ hashql_macros::define_symbols! { json, JsonPath, JsonPathSegment, + lhs, + rhs, // [tidy] sort alphabetically end internal: { diff --git a/libs/@local/hashql/mir/src/lib.rs b/libs/@local/hashql/mir/src/lib.rs index 4df02d28c94..cf18ac793eb 100644 --- a/libs/@local/hashql/mir/src/lib.rs +++ b/libs/@local/hashql/mir/src/lib.rs @@ -11,6 +11,7 @@ macro_metavar_expr_concat, never_type, const_trait_impl, + stmt_expr_attributes, // Library Features allocator_api, diff --git a/libs/@local/hashql/mir/src/reify/atom.rs b/libs/@local/hashql/mir/src/reify/atom.rs index 5779e1d4dcb..7ba1f3c86d3 100644 --- a/libs/@local/hashql/mir/src/reify/atom.rs +++ b/libs/@local/hashql/mir/src/reify/atom.rs @@ -1,12 +1,15 @@ use core::alloc::Allocator; -use hashql_core::{id::Id as _, r#type::kind::TypeKind, value::Primitive}; -use hashql_hir::node::{ - Node, - access::{Access, FieldAccess, IndexAccess}, - data::Data, - kind::NodeKind, - variable::Variable, +use hashql_core::{id::Id as _, span::Spanned, r#type::kind::TypeKind, value::Primitive}; +use hashql_hir::{ + node::{ + Node, + access::{Access, FieldAccess, IndexAccess}, + data::Data, + kind::NodeKind, + variable::Variable, + }, + path::QualifiedPath, }; use super::{ @@ -20,6 +23,7 @@ use crate::{ operand::Operand, place::{FieldIndex, Place, Projection, ProjectionKind}, }, + def::DefId, interpret::value::{Int, TryFromPrimitiveError}, reify::{ error::{field_index_too_large, local_variable_unmapped}, @@ -154,12 +158,42 @@ impl<'heap, A: Allocator, S: Allocator> Reifier<'_, '_, '_, '_, 'heap, A, S> { } } + pub(super) fn operand_qualified_variable( + &mut self, + node: Node<'heap>, + path: QualifiedPath<'heap>, + ) -> Option { + let synthetic = self.state.synthetics.find_or_insert( + self.context, + &mut self.state.diagnostics, + Spanned { + span: node.span, + value: node.id, + }, + path, + )?; + + // The value is called as a thunk, and therefore must be generated as such + let thunk = synthetic.thunk(self.context.bodies, self.context.mir.heap); + Some(thunk) + } + pub(super) fn operand(&mut self, node: Node<'heap>) -> Operand<'heap> { match node.kind { - NodeKind::Variable(Variable::Qualified(_)) => { - self.state - .diagnostics - .push(external_modules_unsupported(node.span).generalize()); + NodeKind::Variable(Variable::Qualified(variable)) => { + let diagnostics = self.state.diagnostics.critical(); + + if let Some(thunk) = self.operand_qualified_variable(node, variable.path) { + return Operand::Constant(Constant::FnPtr(thunk)); + } + + // The variable used is not a qualified variable, and therefore we must issue a + // diagnostic. + if diagnostics == self.state.diagnostics.critical() { + self.state + .diagnostics + .push(external_modules_unsupported(node.span).generalize()); + } // Return a bogus value so that lowering can continue // In the future this would be a simple FnPtr diff --git a/libs/@local/hashql/mir/src/reify/error.rs b/libs/@local/hashql/mir/src/reify/error.rs index ffdbf04023e..b4abff2486d 100644 --- a/libs/@local/hashql/mir/src/reify/error.rs +++ b/libs/@local/hashql/mir/src/reify/error.rs @@ -247,3 +247,76 @@ pub(crate) fn fat_call_on_constant(span: SpanId) -> ReifyDiagnostic { diagnostic } + +// Synthetic body diagnostics + +/// ICE: monomorphized closure type has wrong parameter count for a synthetic binary +/// operation body. +#[coverage(off)] +pub(crate) fn synthetic_binary_arity_mismatch( + span: SpanId, + name: Symbol<'_>, + actual_params: usize, +) -> ReifyDiagnostic { + let mut diagnostic = Diagnostic::new(ReifyDiagnosticCategory::TypeInvariant, Critical::BUG) + .primary(Label::new( + span, + format!("monomorphized type of `{name}` has {actual_params} parameters, expected 2"), + )); + + diagnostic.add_message(Message::note( + "binary operations require exactly 2 parameters after monomorphization", + )); + + diagnostic.add_message(Message::help( + "type checking should have ensured the correct arity before reification", + )); + + diagnostic +} + +/// ICE: monomorphized closure type has wrong parameter count for a synthetic unary +/// operation body. +#[coverage(off)] +pub(crate) fn synthetic_unary_arity_mismatch( + span: SpanId, + name: Symbol<'_>, + actual_params: usize, +) -> ReifyDiagnostic { + let mut diagnostic = Diagnostic::new(ReifyDiagnosticCategory::TypeInvariant, Critical::BUG) + .primary(Label::new( + span, + format!("monomorphized type of `{name}` has {actual_params} parameters, expected 1"), + )); + + diagnostic.add_message(Message::note( + "unary operations require exactly 1 parameter after monomorphization", + )); + + diagnostic.add_message(Message::help( + "type checking should have ensured the correct arity before reification", + )); + + diagnostic +} + +/// Intrinsic cannot be used as a first-class value. +pub(crate) fn intrinsic_not_first_class( + span: SpanId, + name: Symbol<'_>, +) -> ReifyDiagnostic { + let mut diagnostic = + Diagnostic::new(ReifyDiagnosticCategory::UnsupportedFeature, Critical::ERROR).primary( + Label::new(span, format!("`{name}` cannot be used as a value")), + ); + + diagnostic.add_message(Message::note(format!( + "`{name}` is a syntactic form that is only valid at a call site" + ))); + + diagnostic.add_message(Message::help( + "call this intrinsic directly instead of passing it as an argument", + )); + + diagnostic +} diff --git a/libs/@local/hashql/mir/src/reify/mod.rs b/libs/@local/hashql/mir/src/reify/mod.rs index caeee8fd6d2..68e81187422 100644 --- a/libs/@local/hashql/mir/src/reify/mod.rs +++ b/libs/@local/hashql/mir/src/reify/mod.rs @@ -2,6 +2,7 @@ mod atom; mod current; mod error; mod rvalue; +mod synthetic; mod terminator; mod transform; mod types; @@ -43,6 +44,7 @@ use self::{ error::{ expected_anf_thunk, expected_anf_variable, external_modules_unsupported, local_not_thunk, }, + synthetic::Synthetics, types::unwrap_closure_type, }; use crate::{ @@ -106,6 +108,9 @@ struct CrossCompileState<'heap, S: Allocator> { /// Mapping of variable IDs to their thunk definitions. thunks: Thunks, + /// Synthetic bodies that have been generated during reification. + synthetics: Synthetics, + /// Collection of diagnostics encountered during reification. diagnostics: ReifyDiagnosticIssues, @@ -506,6 +511,7 @@ pub fn from_hir<'heap, A: Allocator, S: Allocator + Clone>( }; let mut state = CrossCompileState { thunks, + synthetics: Synthetics::new_in(context.scratch.clone()), ctor: FastHashMap::default(), diagnostics: ReifyDiagnosticIssues::new(), var_pool: MixedBitSetPool::with_recycler_in( diff --git a/libs/@local/hashql/mir/src/reify/rvalue.rs b/libs/@local/hashql/mir/src/reify/rvalue.rs index f5c82901a18..d60606e623d 100644 --- a/libs/@local/hashql/mir/src/reify/rvalue.rs +++ b/libs/@local/hashql/mir/src/reify/rvalue.rs @@ -2,6 +2,7 @@ use core::alloc::Allocator; use hashql_core::{ id::{Id as _, IdVec}, + span::Spanned, symbol::sym, r#type::{TypeBuilder, Typed, builder}, value::Primitive, @@ -17,6 +18,7 @@ use hashql_hir::node::{ BinaryOperation, InputOperation, Operation, TypeConstructor, TypeOperation, UnaryOperation, }, thunk::Thunk, + variable::Variable, }; use super::{ @@ -200,6 +202,10 @@ impl<'mir, 'heap, A: Allocator, S: Allocator> Reifier<'_, 'mir, '_, '_, 'heap, A function: Node<'heap>, call_arguments: &[CallArgument<'heap>], ) -> RValue<'heap> { + if let Some(value) = self.rvalue_call_thin_specialize(function, call_arguments) { + return value; + } + let mut arguments = IdVec::with_capacity_in(call_arguments.len(), self.context.mir.heap); for CallArgument { span: _, value } in call_arguments { @@ -214,6 +220,57 @@ impl<'mir, 'heap, A: Allocator, S: Allocator> Reifier<'_, 'mir, '_, '_, 'heap, A }) } + /// Attempts to beta-reduce a thin call to a qualified intrinsic into a closure aggregate. + /// + /// The thunking phase wraps every qualified variable in `Call(Thin, Qualified(...), [])`. + /// When the target is a known intrinsic, this produces the closure aggregate directly + /// instead of going through a thunk body that later passes would eliminate. + /// + /// Returns [`None`] if the call has arguments, the target is not a qualified variable, + /// or the path is not a known intrinsic. + fn rvalue_call_thin_specialize( + &mut self, + function: Node<'heap>, + call_arguments: &[CallArgument<'heap>], + ) -> Option> { + if !call_arguments.is_empty() { + return None; + } + + let NodeKind::Variable(Variable::Qualified(variable)) = function.kind else { + return None; + }; + + let diagnostics = self.state.diagnostics.critical(); + + let Some(synthetic) = self.state.synthetics.find_or_insert( + self.context, + &mut self.state.diagnostics, + Spanned { + span: function.span, + value: function.id, + }, + variable.path, + ) else { + // A diagnostic was already emitted (e.g. non-first-classable intrinsic). + // Return a bogus value to prevent the fallback from emitting a duplicate. + if diagnostics != self.state.diagnostics.critical() { + return Some(RValue::Load(Operand::Constant(Constant::Unit))); + } + + return None; + }; + + let mut operands = IdVec::with_capacity_in(2, self.context.mir.heap); + operands.push(Operand::Constant(Constant::FnPtr(synthetic.body))); + operands.push(Operand::Constant(Constant::Unit)); + + Some(RValue::Aggregate(Aggregate { + kind: AggregateKind::Closure, + operands, + })) + } + fn rvalue_call_fat( &mut self, function: Node<'heap>, diff --git a/libs/@local/hashql/mir/src/reify/synthetic.rs b/libs/@local/hashql/mir/src/reify/synthetic.rs new file mode 100644 index 00000000000..c87149ac3a1 --- /dev/null +++ b/libs/@local/hashql/mir/src/reify/synthetic.rs @@ -0,0 +1,513 @@ +use core::alloc::Allocator; + +use hashql_core::{ + heap::Heap, + id::IdVec, + intern::Interned, + span::{SpanId, Spanned}, + symbol::{ConstantSymbol, sym}, + r#type::{TypeBuilder, TypeId}, +}; +use hashql_hir::{node::HirId, path::QualifiedPath}; + +use super::{ + ReifyContext, ReifyDiagnosticIssues, + error::{ + intrinsic_not_first_class, synthetic_binary_arity_mismatch, synthetic_unary_arity_mismatch, + }, +}; +use crate::{ + body::{ + Body, Source, + basic_block::{BasicBlock, BasicBlockVec}, + basic_blocks::BasicBlocks, + constant::Constant, + local::{LocalDecl, LocalVec}, + operand::Operand, + place::Place, + rvalue::{Aggregate, AggregateKind, BinOp, Binary, RValue, UnOp, Unary}, + statement::{Assign, Statement, StatementKind}, + terminator::{Return, Terminator, TerminatorKind}, + }, + def::{DefId, DefIdVec}, + reify::unwrap_closure_type, +}; + +/// Constructs a `&'static [ConstantSymbol]` path from `::` separated segments. +/// +/// Each segment maps to the corresponding `sym::` constant. +/// +/// ```ignore +/// T![::core::math::add] // expands to &[sym::core, sym::math, sym::add] +/// T![::core::cmp::eq] // expands to &[sym::core, sym::cmp, sym::eq] +/// ``` +#[expect(clippy::min_ident_chars)] +macro_rules! T { + [:: $($segment:ident)::+] => { + [ + $(::hashql_core::symbol::sym::$segment::CONST),+ + ] + }; +} + +const MAX_LENGTH: usize = 3; + +pub(crate) struct Synthetic { + pub span: SpanId, + pub r#type: TypeId, + + pub path: &'static [ConstantSymbol], + pub thunk: Option, + pub body: DefId, +} + +impl Synthetic { + fn create_thunk<'heap>(&self, id: DefId, heap: &'heap Heap) -> Body<'heap> { + let mut locals = LocalVec::with_capacity_in(1, heap); + let closure = locals.push(LocalDecl { + span: self.span, + r#type: self.r#type, + name: None, + }); + + let mut statements = Vec::with_capacity_in(1, heap); + + let mut operands = IdVec::with_capacity_in(2, heap); + operands.push(Operand::Constant(Constant::FnPtr(self.body))); + operands.push(Operand::Constant(Constant::Unit)); + + statements.push(Statement { + span: self.span, + kind: StatementKind::Assign(Assign { + lhs: Place::local(closure), + rhs: RValue::Aggregate(Aggregate { + kind: AggregateKind::Closure, + operands, + }), + }), + }); + + let mut blocks = BasicBlockVec::with_capacity_in(1, heap); + blocks.push(BasicBlock { + params: Interned::empty(), + statements, + terminator: Terminator { + span: self.span, + kind: TerminatorKind::Return(Return { + value: Operand::Place(Place::local(closure)), + }), + }, + }); + let blocks = BasicBlocks::new(blocks); + + // We must generate a new thunk for this body. That's easy enough, it's a simple body, + // that just returns a `closure` with no env. + Body { + id, + span: self.span, + return_type: self.r#type, + source: Source::Thunk(HirId::PLACEHOLDER, None), + local_decls: locals, + basic_blocks: blocks, + args: 0, + } + } + + pub(crate) fn thunk<'heap, A: Allocator>( + &mut self, + bodies: &mut DefIdVec, A>, + heap: &'heap Heap, + ) -> DefId { + if let Some(thunk) = self.thunk { + return thunk; + } + + let id = bodies.push_with(|id| self.create_thunk(id, heap)); + self.thunk = Some(id); + + id + } +} + +pub(crate) struct Synthetics { + inner: Vec, +} + +impl Synthetics { + pub(crate) const fn new_in(alloc: S) -> Self { + Self { + inner: Vec::new_in(alloc), + } + } + + #[expect(unsafe_code)] + pub(crate) fn find_or_insert( + &mut self, + context: &mut ReifyContext, + diagnostics: &mut ReifyDiagnosticIssues, + + hir_id: Spanned, + path: QualifiedPath<'_>, + ) -> Option<&mut Synthetic> { + let mut buffer = [sym::dummy::CONST; MAX_LENGTH]; + if path.0.len() > MAX_LENGTH { + // The path is longer than our maximum length, and therefore cannot be part of an + // intrinsic path + return None; + } + + for (index, segment) in path.0.iter().enumerate() { + let symbol = segment.value.as_constant()?; + + buffer[index] = symbol; + } + + // The actual path that we compare against + let path = &buffer[..path.0.len()]; + + // first check if we already have that path, in which case we can just return it + if let Some(index) = self + .inner + .iter_mut() + .position(|synthetic| synthetic.path == path) + { + // SAFETY: We know the position exists, so this is safe, workaround as we cannot use + // `find_mut` here because of NLL. + return Some(unsafe { self.inner.get_unchecked_mut(index) }); + } + + let builder = SyntheticBuilder { + context, + diagnostics, + hir_id, + inner: &mut self.inner, + }; + + macro_rules! binary { + (:: $($seg:ident)::+ => $op:expr) => { + Some(builder.binary( + ::hashql_core::symbol::sym::path::$($seg)::+::CONST, + &T![:: $($seg)::+], + $op, + )) + }; + } + + macro_rules! unary { + (:: $($seg:ident)::+ => $op:expr) => { + Some(builder.unary( + ::hashql_core::symbol::sym::path::$($seg)::+::CONST, + &T![:: $($seg)::+], + $op, + )) + }; + } + + #[rustfmt::skip] + #[expect(clippy::match_same_arms, reason = "readability")] + match path { + // comparison + &T![::core::cmp::gt] => binary!(::core::cmp::gt => BinOp::Gt), + &T![::core::cmp::lt] => binary!(::core::cmp::lt => BinOp::Lt), + &T![::core::cmp::gte] => binary!(::core::cmp::gte => BinOp::Gte), + &T![::core::cmp::lte] => binary!(::core::cmp::lte => BinOp::Lte), + &T![::core::cmp::eq] => binary!(::core::cmp::eq => BinOp::Eq), + &T![::core::cmp::ne] => binary!(::core::cmp::ne => BinOp::Ne), + + // boolean + &T![::core::bool::and] => binary!(::core::bool::and => BinOp::BitAnd), + &T![::core::bool::or] => binary!(::core::bool::or => BinOp::BitOr), + &T![::core::bool::not] => unary!(::core::bool::not => UnOp::BitNot), + + // arithmetic (only add/sub are currently constructible in MIR) + &T![::core::math::add] => binary!(::core::math::add => BinOp::Add), + &T![::core::math::sub] => binary!(::core::math::sub => BinOp::Sub), + + // bitwise (only and/or are currently constructible in MIR) + &T![::core::bits::and] => binary!(::core::bits::and => BinOp::BitAnd), + &T![::core::bits::or] => binary!(::core::bits::or => BinOp::BitOr), + &T![::core::bits::not] => unary!(::core::bits::not => UnOp::BitNot), + + // MIR ops exist but are unconstructible (BinOp variants carry `!`) + &T![::core::math::mul] + | &T![::core::math::div] + | &T![::core::math::rem] + | &T![::core::math::r#mod] + | &T![::core::math::pow] + | &T![::core::bits::xor] + | &T![::core::bits::shl] + | &T![::core::bits::shr] => None, + + // math functions without direct MIR ops + &T![::core::math::sqrt] | &T![::core::math::cbrt] | &T![::core::math::root] => None, + + // graph syntactic forms (not first-classable) + &T![::graph::head::entities] => { + diagnostics.push( + intrinsic_not_first_class(hir_id.span, sym::path::graph_head_entities) + .generalize(), + ); + None + } + &T![::graph::body::filter] => { + diagnostics.push( + intrinsic_not_first_class(hir_id.span, sym::path::graph_body_filter) + .generalize(), + ); + None + } + &T![::graph::tail::collect] => { + diagnostics.push( + intrinsic_not_first_class(hir_id.span, sym::path::graph_tail_collect) + .generalize(), + ); + None + } + + // not a known intrinsic path + _ => None, + } + } +} + +struct SyntheticBuilder<'syn, 'ctx, 'mir, 'hir, 'env, 'heap, A: Allocator, S: Allocator> { + context: &'ctx mut ReifyContext<'mir, 'hir, 'env, 'heap, A, S>, + diagnostics: &'ctx mut ReifyDiagnosticIssues, + + inner: &'syn mut Vec, + + hir_id: Spanned, +} + +impl<'syn, A: Allocator, S: Allocator> SyntheticBuilder<'syn, '_, '_, '_, '_, '_, A, S> { + fn binary( + mut self, + name: ConstantSymbol, + path: &'static [ConstantSymbol], + op: BinOp, + ) -> &'syn mut Synthetic { + let (r#type, body) = self.build_binary(name, op); + + self.inner.push_mut(Synthetic { + span: self.hir_id.span, + r#type, + path, + thunk: None, + body, + }) + } + + fn unary( + mut self, + name: ConstantSymbol, + path: &'static [ConstantSymbol], + op: UnOp, + ) -> &'syn mut Synthetic { + let (r#type, body) = self.build_unary(name, op); + + self.inner.push_mut(Synthetic { + span: self.hir_id.span, + r#type, + path, + thunk: None, + body, + }) + } + + fn build_binary(&mut self, name: ConstantSymbol, op: BinOp) -> (TypeId, DefId) { + let mut closure_type_id = self + .context + .hir + .map + .monomorphized_type_id(self.hir_id.value); + let closure_type = unwrap_closure_type(closure_type_id, self.context.mir.env); + + // Thunking wraps the qualified variable type as `() -> ClosureType`. + // If we see a no-arg closure, unwrap its return type to get the actual signature. + let closure_type = if closure_type.params.is_empty() { + closure_type_id = closure_type.returns; + unwrap_closure_type(closure_type.returns, self.context.mir.env) + } else { + closure_type + }; + + let [lhs_type, rhs_type] = + closure_type + .params + .as_array::<2>() + .copied() + .unwrap_or_else(|| { + self.diagnostics.push( + synthetic_binary_arity_mismatch( + self.hir_id.span, + name.into(), + closure_type.params.len(), + ) + .generalize(), + ); + + let builder = TypeBuilder::spanned(self.hir_id.span, self.context.mir.env); + + [builder.unknown(), builder.unknown()] + }); + + let env_type = + TypeBuilder::spanned(self.hir_id.span, self.context.mir.env).tuple([] as [TypeId; 0]); + + let mut locals = LocalVec::with_capacity_in(4, self.context.mir.heap); + // env is the first argument per fat closure ABI + let _env = locals.push(LocalDecl { + span: self.hir_id.span, + r#type: env_type, + name: None, + }); + let lhs_id = locals.push(LocalDecl { + span: self.hir_id.span, + r#type: lhs_type, + name: Some(sym::lhs), + }); + let rhs_id = locals.push(LocalDecl { + span: self.hir_id.span, + r#type: rhs_type, + name: Some(sym::rhs), + }); + let output = locals.push(LocalDecl { + span: self.hir_id.span, + r#type: closure_type.returns, + name: None, + }); + + let mut statements = Vec::with_capacity_in(1, self.context.mir.heap); + statements.push(Statement { + span: self.hir_id.span, + kind: StatementKind::Assign(Assign { + lhs: Place::local(output), + rhs: RValue::Binary(Binary { + op, + left: Operand::Place(Place::local(lhs_id)), + right: Operand::Place(Place::local(rhs_id)), + }), + }), + }); + + let mut blocks = BasicBlockVec::with_capacity_in(1, self.context.mir.heap); + blocks.push(BasicBlock { + params: Interned::empty(), + statements, + terminator: Terminator { + span: self.hir_id.span, + kind: TerminatorKind::Return(Return { + value: Operand::Place(Place::local(output)), + }), + }, + }); + let blocks = BasicBlocks::new(blocks); + + let body = self.context.bodies.push_with(|id| Body { + id, + span: self.hir_id.span, + return_type: closure_type.returns, + source: Source::Synthetic(name.into()), + local_decls: locals, + basic_blocks: blocks, + args: 3, + }); + + (closure_type_id, body) + } + + fn build_unary(&mut self, name: ConstantSymbol, op: UnOp) -> (TypeId, DefId) { + let mut closure_type_id = self + .context + .hir + .map + .monomorphized_type_id(self.hir_id.value); + let closure_type = unwrap_closure_type(closure_type_id, self.context.mir.env); + + // Thunking wraps the qualified variable type as `() -> ClosureType`. + // If we see a no-arg closure, unwrap its return type to get the actual signature. + let closure_type = if closure_type.params.is_empty() { + closure_type_id = closure_type.returns; + unwrap_closure_type(closure_type.returns, self.context.mir.env) + } else { + closure_type + }; + + let [operand_type] = closure_type + .params + .as_array::<1>() + .copied() + .unwrap_or_else(|| { + self.diagnostics.push( + synthetic_unary_arity_mismatch( + self.hir_id.span, + name.into(), + closure_type.params.len(), + ) + .generalize(), + ); + + let builder = TypeBuilder::spanned(self.hir_id.span, self.context.mir.env); + + [builder.unknown()] + }); + + let env_type = + TypeBuilder::spanned(self.hir_id.span, self.context.mir.env).tuple([] as [TypeId; 0]); + + let mut locals = LocalVec::with_capacity_in(3, self.context.mir.heap); + // env is the first argument per fat closure ABI + let _env = locals.push(LocalDecl { + span: self.hir_id.span, + r#type: env_type, + name: None, + }); + let operand_id = locals.push(LocalDecl { + span: self.hir_id.span, + r#type: operand_type, + name: Some(sym::lhs), + }); + let output = locals.push(LocalDecl { + span: self.hir_id.span, + r#type: closure_type.returns, + name: None, + }); + + let mut statements = Vec::with_capacity_in(1, self.context.mir.heap); + statements.push(Statement { + span: self.hir_id.span, + kind: StatementKind::Assign(Assign { + lhs: Place::local(output), + rhs: RValue::Unary(Unary { + op, + operand: Operand::Place(Place::local(operand_id)), + }), + }), + }); + + let mut blocks = BasicBlockVec::with_capacity_in(1, self.context.mir.heap); + blocks.push(BasicBlock { + params: Interned::empty(), + statements, + terminator: Terminator { + span: self.hir_id.span, + kind: TerminatorKind::Return(Return { + value: Operand::Place(Place::local(output)), + }), + }, + }); + let blocks = BasicBlocks::new(blocks); + + let body = self.context.bodies.push_with(|id| Body { + id, + span: self.hir_id.span, + return_type: closure_type.returns, + source: Source::Synthetic(name.into()), + local_decls: locals, + basic_blocks: blocks, + args: 2, + }); + + (closure_type_id, body) + } +} diff --git a/libs/@local/hashql/mir/tests/ui/interpret/synthetic-intrinsic-value.aux.mir b/libs/@local/hashql/mir/tests/ui/interpret/synthetic-intrinsic-value.aux.mir new file mode 100644 index 00000000000..d3189b55f0d --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/interpret/synthetic-intrinsic-value.aux.mir @@ -0,0 +1,161 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +fn {closure#6}(%0: (), %1: Integer, %2: Integer, %3: (Number, Number) -> Boolean) -> Boolean { + let %4: Boolean + + bb0(): { + %4 = apply %3.0 %3.1 %1 %2 + + return %4 + } +} + +thunk apply:0() -> (Integer, Integer, (Number, Number) -> Boolean) -> Boolean { + let %0: (Integer, Integer, (Number, Number) -> Boolean) -> Boolean + let %1: () + + bb0(): { + %1 = () + %0 = closure(({closure#6} as FnPtr), %1) + + return %0 + } +} + +fn {synthetic#::core::cmp::lte}(%0: (), %1: Number, %2: Number) -> Boolean { + let %3: Boolean + + bb0(): { + %3 = %1 <= %2 + + return %3 + } +} + +*thunk {thunk#5}() -> Boolean { + let %0: (Number, Number) -> Boolean + let %1: (Integer, Integer, (Number, Number) -> Boolean) -> Boolean + let %2: Boolean + + bb0(): { + %0 = closure(({synthetic#::core::cmp::lte} as FnPtr), ()) + %1 = apply (apply:0 as FnPtr) + %2 = apply %1.0 %1.1 2 3 %0 + + return %2 + } +} + +════ Pre-inlining MIR ══════════════════════════════════════════════════════════ + +fn {closure#6}(%0: (), %1: Integer, %2: Integer, %3: (Number, Number) -> Boolean) -> Boolean { + let %4: Boolean + + bb0(): { + %4 = apply %3.0 %3.1 %1 %2 + + return %4 + } +} + +thunk apply:0() -> (Integer, Integer, (Number, Number) -> Boolean) -> Boolean { + let %0: (Integer, Integer, (Number, Number) -> Boolean) -> Boolean + + bb0(): { + %0 = closure(({closure#6} as FnPtr), ()) + + return %0 + } +} + +fn {synthetic#::core::cmp::lte}(%0: (), %1: Number, %2: Number) -> Boolean { + let %3: Boolean + + bb0(): { + %3 = %1 <= %2 + + return %3 + } +} + +*thunk {thunk#5}() -> Boolean { + bb0(): { + return true + } +} + +════ Inlined MIR ═══════════════════════════════════════════════════════════════ + +fn {closure#6}(%0: (), %1: Integer, %2: Integer, %3: (Number, Number) -> Boolean) -> Boolean { + let %4: Boolean + + bb0(): { + %4 = apply %3.0 %3.1 %1 %2 + + return %4 + } +} + +thunk apply:0() -> (Integer, Integer, (Number, Number) -> Boolean) -> Boolean { + let %0: (Integer, Integer, (Number, Number) -> Boolean) -> Boolean + + bb0(): { + %0 = closure(({closure#6} as FnPtr), ()) + + return %0 + } +} + +fn {synthetic#::core::cmp::lte}(%0: (), %1: Number, %2: Number) -> Boolean { + let %3: Boolean + + bb0(): { + %3 = %1 <= %2 + + return %3 + } +} + +*thunk {thunk#5}() -> Boolean { + bb0(): { + return true + } +} + +════ Post Inline MIR ═══════════════════════════════════════════════════════════ + +fn {closure#6}(%0: (), %1: Integer, %2: Integer, %3: (Number, Number) -> Boolean) -> Boolean { + let %4: Boolean + + bb0(): { + %4 = apply %3.0 %3.1 %1 %2 + + return %4 + } +} + +thunk apply:0() -> (Integer, Integer, (Number, Number) -> Boolean) -> Boolean { + let %0: (Integer, Integer, (Number, Number) -> Boolean) -> Boolean + + bb0(): { + %0 = closure(({closure#6} as FnPtr), ()) + + return %0 + } +} + +fn {synthetic#::core::cmp::lte}(%0: (), %1: Number, %2: Number) -> Boolean { + let %3: Boolean + + bb0(): { + %3 = %1 <= %2 + + return %3 + } +} + +*thunk {thunk#5}() -> Boolean { + bb0(): { + return true + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/interpret/synthetic-intrinsic-value.jsonc b/libs/@local/hashql/mir/tests/ui/interpret/synthetic-intrinsic-value.jsonc new file mode 100644 index 00000000000..e1778d8a57c --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/interpret/synthetic-intrinsic-value.jsonc @@ -0,0 +1,14 @@ +//@ run: pass +//@ description: Intrinsic passed as a value to a higher-order function evaluates correctly +[ + "let", + "apply", + [ + "fn", + { "#tuple": [] }, + { "#struct": { "a": "Integer", "b": "Integer", "f": "_" } }, + "_", + ["f", "a", "b"] + ], + ["apply", { "#literal": 2 }, { "#literal": 3 }, "<="] +] diff --git a/libs/@local/hashql/mir/tests/ui/interpret/synthetic-intrinsic-value.stdout b/libs/@local/hashql/mir/tests/ui/interpret/synthetic-intrinsic-value.stdout new file mode 100644 index 00000000000..b77fc311152 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/interpret/synthetic-intrinsic-value.stdout @@ -0,0 +1,5 @@ +Integer( + Bool( + true, + ), +) \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/post_inline/synthetic-intrinsic-hof-dynamic.jsonc b/libs/@local/hashql/mir/tests/ui/pass/post_inline/synthetic-intrinsic-hof-dynamic.jsonc new file mode 100644 index 00000000000..7b642a53970 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/post_inline/synthetic-intrinsic-hof-dynamic.jsonc @@ -0,0 +1,16 @@ +//@ run: pass +//@ description: Higher-order function receiving an intrinsic as a value with dynamic arguments +// Same as synthetic-intrinsic-hof but uses inputs instead of literals so the +// comparison cannot be constant-folded and the call shape survives optimization. +[ + "let", + "apply", + [ + "fn", + { "#tuple": [] }, + { "#struct": { "a": "Integer", "b": "Integer", "f": "_" } }, + "_", + ["f", "a", "b"] + ], + ["apply", ["input", "x", "Integer"], ["input", "y", "Integer"], "<="] +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/post_inline/synthetic-intrinsic-hof-dynamic.stdout b/libs/@local/hashql/mir/tests/ui/pass/post_inline/synthetic-intrinsic-hof-dynamic.stdout new file mode 100644 index 00000000000..541204e71b3 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/post_inline/synthetic-intrinsic-hof-dynamic.stdout @@ -0,0 +1,269 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +fn {closure#8}(%0: (), %1: Integer, %2: Integer, %3: (Number, Number) -> Boolean) -> Boolean { + let %4: Boolean + + bb0(): { + %4 = apply %3.0 %3.1 %1 %2 + + return %4 + } +} + +thunk apply:0() -> (Integer, Integer, (Number, Number) -> Boolean) -> Boolean { + let %0: (Integer, Integer, (Number, Number) -> Boolean) -> Boolean + let %1: () + + bb0(): { + %1 = () + %0 = closure(({closure#8} as FnPtr), %1) + + return %0 + } +} + +thunk {thunk#5}() -> Integer { + let %0: Integer + + bb0(): { + %0 = input LOAD x + + return %0 + } +} + +thunk {thunk#6}() -> Integer { + let %0: Integer + + bb0(): { + %0 = input LOAD y + + return %0 + } +} + +fn {synthetic#::core::cmp::lte}(%0: (), %1: Number, %2: Number) -> Boolean { + let %3: Boolean + + bb0(): { + %3 = %1 <= %2 + + return %3 + } +} + +*thunk {thunk#7}() -> Boolean { + let %0: Integer + let %1: Integer + let %2: (Number, Number) -> Boolean + let %3: (Integer, Integer, (Number, Number) -> Boolean) -> Boolean + let %4: Boolean + + bb0(): { + %0 = apply ({thunk#5} as FnPtr) + %1 = apply ({thunk#6} as FnPtr) + %2 = closure(({synthetic#::core::cmp::lte} as FnPtr), ()) + %3 = apply (apply:0 as FnPtr) + %4 = apply %3.0 %3.1 %0 %1 %2 + + return %4 + } +} + +════ Pre-inlining MIR ══════════════════════════════════════════════════════════ + +fn {closure#8}(%0: (), %1: Integer, %2: Integer, %3: (Number, Number) -> Boolean) -> Boolean { + let %4: Boolean + + bb0(): { + %4 = apply %3.0 %3.1 %1 %2 + + return %4 + } +} + +thunk apply:0() -> (Integer, Integer, (Number, Number) -> Boolean) -> Boolean { + let %0: (Integer, Integer, (Number, Number) -> Boolean) -> Boolean + + bb0(): { + %0 = closure(({closure#8} as FnPtr), ()) + + return %0 + } +} + +thunk {thunk#5}() -> Integer { + let %0: Integer + + bb0(): { + %0 = input LOAD x + + return %0 + } +} + +thunk {thunk#6}() -> Integer { + let %0: Integer + + bb0(): { + %0 = input LOAD y + + return %0 + } +} + +fn {synthetic#::core::cmp::lte}(%0: (), %1: Number, %2: Number) -> Boolean { + let %3: Boolean + + bb0(): { + %3 = %1 <= %2 + + return %3 + } +} + +*thunk {thunk#7}() -> Boolean { + let %0: Integer + let %1: Integer + let %2: Boolean + + bb0(): { + %0 = input LOAD x + %1 = input LOAD y + %2 = %0 <= %1 + + return %2 + } +} + +════ Inlined MIR ═══════════════════════════════════════════════════════════════ + +fn {closure#8}(%0: (), %1: Integer, %2: Integer, %3: (Number, Number) -> Boolean) -> Boolean { + let %4: Boolean + + bb0(): { + %4 = apply %3.0 %3.1 %1 %2 + + return %4 + } +} + +thunk apply:0() -> (Integer, Integer, (Number, Number) -> Boolean) -> Boolean { + let %0: (Integer, Integer, (Number, Number) -> Boolean) -> Boolean + + bb0(): { + %0 = closure(({closure#8} as FnPtr), ()) + + return %0 + } +} + +thunk {thunk#5}() -> Integer { + let %0: Integer + + bb0(): { + %0 = input LOAD x + + return %0 + } +} + +thunk {thunk#6}() -> Integer { + let %0: Integer + + bb0(): { + %0 = input LOAD y + + return %0 + } +} + +fn {synthetic#::core::cmp::lte}(%0: (), %1: Number, %2: Number) -> Boolean { + let %3: Boolean + + bb0(): { + %3 = %1 <= %2 + + return %3 + } +} + +*thunk {thunk#7}() -> Boolean { + let %0: Integer + let %1: Integer + let %2: Boolean + + bb0(): { + %0 = input LOAD x + %1 = input LOAD y + %2 = %0 <= %1 + + return %2 + } +} + +════ Post Inline MIR ═══════════════════════════════════════════════════════════ + +fn {closure#8}(%0: (), %1: Integer, %2: Integer, %3: (Number, Number) -> Boolean) -> Boolean { + let %4: Boolean + + bb0(): { + %4 = apply %3.0 %3.1 %1 %2 + + return %4 + } +} + +thunk apply:0() -> (Integer, Integer, (Number, Number) -> Boolean) -> Boolean { + let %0: (Integer, Integer, (Number, Number) -> Boolean) -> Boolean + + bb0(): { + %0 = closure(({closure#8} as FnPtr), ()) + + return %0 + } +} + +thunk {thunk#5}() -> Integer { + let %0: Integer + + bb0(): { + %0 = input LOAD x + + return %0 + } +} + +thunk {thunk#6}() -> Integer { + let %0: Integer + + bb0(): { + %0 = input LOAD y + + return %0 + } +} + +fn {synthetic#::core::cmp::lte}(%0: (), %1: Number, %2: Number) -> Boolean { + let %3: Boolean + + bb0(): { + %3 = %1 <= %2 + + return %3 + } +} + +*thunk {thunk#7}() -> Boolean { + let %0: Integer + let %1: Integer + let %2: Boolean + + bb0(): { + %0 = input LOAD x + %1 = input LOAD y + %2 = %0 <= %1 + + return %2 + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/pass/post_inline/synthetic-intrinsic-hof.jsonc b/libs/@local/hashql/mir/tests/ui/pass/post_inline/synthetic-intrinsic-hof.jsonc new file mode 100644 index 00000000000..dbac8a3a046 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/post_inline/synthetic-intrinsic-hof.jsonc @@ -0,0 +1,16 @@ +//@ run: pass +//@ description: Higher-order function receiving an intrinsic as a value +// The intrinsic `<=` is passed as a value to `apply`. After all optimizations, +// the synthetic wrapper should be inlined and the comparison exposed directly. +[ + "let", + "apply", + [ + "fn", + { "#tuple": [] }, + { "#struct": { "a": "Integer", "b": "Integer", "f": "_" } }, + "_", + ["f", "a", "b"] + ], + ["apply", { "#literal": 2 }, { "#literal": 3 }, "<="] +] diff --git a/libs/@local/hashql/mir/tests/ui/pass/post_inline/synthetic-intrinsic-hof.stdout b/libs/@local/hashql/mir/tests/ui/pass/post_inline/synthetic-intrinsic-hof.stdout new file mode 100644 index 00000000000..d3189b55f0d --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/post_inline/synthetic-intrinsic-hof.stdout @@ -0,0 +1,161 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +fn {closure#6}(%0: (), %1: Integer, %2: Integer, %3: (Number, Number) -> Boolean) -> Boolean { + let %4: Boolean + + bb0(): { + %4 = apply %3.0 %3.1 %1 %2 + + return %4 + } +} + +thunk apply:0() -> (Integer, Integer, (Number, Number) -> Boolean) -> Boolean { + let %0: (Integer, Integer, (Number, Number) -> Boolean) -> Boolean + let %1: () + + bb0(): { + %1 = () + %0 = closure(({closure#6} as FnPtr), %1) + + return %0 + } +} + +fn {synthetic#::core::cmp::lte}(%0: (), %1: Number, %2: Number) -> Boolean { + let %3: Boolean + + bb0(): { + %3 = %1 <= %2 + + return %3 + } +} + +*thunk {thunk#5}() -> Boolean { + let %0: (Number, Number) -> Boolean + let %1: (Integer, Integer, (Number, Number) -> Boolean) -> Boolean + let %2: Boolean + + bb0(): { + %0 = closure(({synthetic#::core::cmp::lte} as FnPtr), ()) + %1 = apply (apply:0 as FnPtr) + %2 = apply %1.0 %1.1 2 3 %0 + + return %2 + } +} + +════ Pre-inlining MIR ══════════════════════════════════════════════════════════ + +fn {closure#6}(%0: (), %1: Integer, %2: Integer, %3: (Number, Number) -> Boolean) -> Boolean { + let %4: Boolean + + bb0(): { + %4 = apply %3.0 %3.1 %1 %2 + + return %4 + } +} + +thunk apply:0() -> (Integer, Integer, (Number, Number) -> Boolean) -> Boolean { + let %0: (Integer, Integer, (Number, Number) -> Boolean) -> Boolean + + bb0(): { + %0 = closure(({closure#6} as FnPtr), ()) + + return %0 + } +} + +fn {synthetic#::core::cmp::lte}(%0: (), %1: Number, %2: Number) -> Boolean { + let %3: Boolean + + bb0(): { + %3 = %1 <= %2 + + return %3 + } +} + +*thunk {thunk#5}() -> Boolean { + bb0(): { + return true + } +} + +════ Inlined MIR ═══════════════════════════════════════════════════════════════ + +fn {closure#6}(%0: (), %1: Integer, %2: Integer, %3: (Number, Number) -> Boolean) -> Boolean { + let %4: Boolean + + bb0(): { + %4 = apply %3.0 %3.1 %1 %2 + + return %4 + } +} + +thunk apply:0() -> (Integer, Integer, (Number, Number) -> Boolean) -> Boolean { + let %0: (Integer, Integer, (Number, Number) -> Boolean) -> Boolean + + bb0(): { + %0 = closure(({closure#6} as FnPtr), ()) + + return %0 + } +} + +fn {synthetic#::core::cmp::lte}(%0: (), %1: Number, %2: Number) -> Boolean { + let %3: Boolean + + bb0(): { + %3 = %1 <= %2 + + return %3 + } +} + +*thunk {thunk#5}() -> Boolean { + bb0(): { + return true + } +} + +════ Post Inline MIR ═══════════════════════════════════════════════════════════ + +fn {closure#6}(%0: (), %1: Integer, %2: Integer, %3: (Number, Number) -> Boolean) -> Boolean { + let %4: Boolean + + bb0(): { + %4 = apply %3.0 %3.1 %1 %2 + + return %4 + } +} + +thunk apply:0() -> (Integer, Integer, (Number, Number) -> Boolean) -> Boolean { + let %0: (Integer, Integer, (Number, Number) -> Boolean) -> Boolean + + bb0(): { + %0 = closure(({closure#6} as FnPtr), ()) + + return %0 + } +} + +fn {synthetic#::core::cmp::lte}(%0: (), %1: Number, %2: Number) -> Boolean { + let %3: Boolean + + bb0(): { + %3 = %1 <= %2 + + return %3 + } +} + +*thunk {thunk#5}() -> Boolean { + bb0(): { + return true + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/reify/synthetic-graph-not-first-class.jsonc b/libs/@local/hashql/mir/tests/ui/reify/synthetic-graph-not-first-class.jsonc new file mode 100644 index 00000000000..3e9a003b638 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/reify/synthetic-graph-not-first-class.jsonc @@ -0,0 +1,15 @@ +//@ run: fail +//@ description: Graph intrinsic cannot be used as a value +[ + "let", + "id", + [ + "fn", + { "#tuple": [] }, + { "#struct": { "f": "_" } }, + "_", + "f" + ], + ["id", "::graph::head::entities"] + //~^ ERROR `::graph::head::entities` cannot be used as a value +] diff --git a/libs/@local/hashql/mir/tests/ui/reify/synthetic-graph-not-first-class.stderr b/libs/@local/hashql/mir/tests/ui/reify/synthetic-graph-not-first-class.stderr new file mode 100644 index 00000000000..9040dcaf35f --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/reify/synthetic-graph-not-first-class.stderr @@ -0,0 +1,7 @@ +error[reify::unsupported-feature]: Unsupported Feature + ╭▸ +13 │ ["id", "::graph::head::entities"] + │ ━━━━━━━━━━━━━━━━━━━━━━━ `::graph::head::entities` cannot be used as a value + │ + ├ note: `::graph::head::entities` is a syntactic form that is only valid at a call site + ╰ help: call this intrinsic directly instead of passing it as an argument \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/reify/synthetic-intrinsic-bare.jsonc b/libs/@local/hashql/mir/tests/ui/reify/synthetic-intrinsic-bare.jsonc new file mode 100644 index 00000000000..be2a9c84d0d --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/reify/synthetic-intrinsic-bare.jsonc @@ -0,0 +1,6 @@ +//@ run: skip reason="bare intrinsic at module root is not yet supported" +//@ description: Bare intrinsic reference as the entire module body +// The thunking phase does not wrap the root body when it is already a variable, +// so a bare qualified path at module root never reaches the synthetic machinery. +// Tracked as part of the module system work (BE-67). +"<=" diff --git a/libs/@local/hashql/mir/tests/ui/reify/synthetic-intrinsic-multiple.jsonc b/libs/@local/hashql/mir/tests/ui/reify/synthetic-intrinsic-multiple.jsonc new file mode 100644 index 00000000000..ea142e5fb33 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/reify/synthetic-intrinsic-multiple.jsonc @@ -0,0 +1,24 @@ +//@ run: pass +//@ description: Different intrinsics in value position generate separate synthetic bodies +[ + "let", + "apply", + [ + "fn", + { "#tuple": [] }, + { "#struct": { "a": "Integer", "b": "Integer", "f": "_" } }, + "_", + ["f", "a", "b"] + ], + [ + "let", + "r1", + ["apply", { "#literal": 1 }, { "#literal": 2 }, "<="], + [ + "let", + "r2", + ["apply", { "#literal": 3 }, { "#literal": 4 }, ">="], + "r2" + ] + ] +] diff --git a/libs/@local/hashql/mir/tests/ui/reify/synthetic-intrinsic-multiple.stdout b/libs/@local/hashql/mir/tests/ui/reify/synthetic-intrinsic-multiple.stdout new file mode 100644 index 00000000000..de50b08fd55 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/reify/synthetic-intrinsic-multiple.stdout @@ -0,0 +1,69 @@ +fn {closure#7}(%0: (), %1: Integer, %2: Integer, %3: (Number, Number) -> Boolean) -> Boolean { + let %4: Boolean + + bb0(): { + %4 = apply %3.0 %3.1 %1 %2 + + return %4 + } +} + +thunk apply:0() -> (Integer, Integer, (Number, Number) -> Boolean) -> Boolean { + let %0: (Integer, Integer, (Number, Number) -> Boolean) -> Boolean + let %1: () + + bb0(): { + %1 = () + %0 = closure(({closure#7} as FnPtr), %1) + + return %0 + } +} + +fn {synthetic#::core::cmp::lte}(%0: (), %1: Number, %2: Number) -> Boolean { + let %3: Boolean + + bb0(): { + %3 = %1 <= %2 + + return %3 + } +} + +thunk r1:0() -> Boolean { + let %0: (Number, Number) -> Boolean + let %1: (Integer, Integer, (Number, Number) -> Boolean) -> Boolean + let %2: Boolean + + bb0(): { + %0 = closure(({synthetic#::core::cmp::lte} as FnPtr), ()) + %1 = apply (apply:0 as FnPtr) + %2 = apply %1.0 %1.1 1 2 %0 + + return %2 + } +} + +fn {synthetic#::core::cmp::gte}(%0: (), %1: Number, %2: Number) -> Boolean { + let %3: Boolean + + bb0(): { + %3 = %1 >= %2 + + return %3 + } +} + +*thunk r2:0() -> Boolean { + let %0: (Number, Number) -> Boolean + let %1: (Integer, Integer, (Number, Number) -> Boolean) -> Boolean + let %2: Boolean + + bb0(): { + %0 = closure(({synthetic#::core::cmp::gte} as FnPtr), ()) + %1 = apply (apply:0 as FnPtr) + %2 = apply %1.0 %1.1 3 4 %0 + + return %2 + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/reify/synthetic-intrinsic-reused.jsonc b/libs/@local/hashql/mir/tests/ui/reify/synthetic-intrinsic-reused.jsonc new file mode 100644 index 00000000000..89189bd98ff --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/reify/synthetic-intrinsic-reused.jsonc @@ -0,0 +1,24 @@ +//@ run: pass +//@ description: Same intrinsic used in value position twice shares a single synthetic body +[ + "let", + "apply", + [ + "fn", + { "#tuple": [] }, + { "#struct": { "a": "Integer", "b": "Integer", "f": "_" } }, + "_", + ["f", "a", "b"] + ], + [ + "let", + "r1", + ["apply", { "#literal": 1 }, { "#literal": 2 }, "<="], + [ + "let", + "r2", + ["apply", { "#literal": 3 }, { "#literal": 4 }, "<="], + "r2" + ] + ] +] diff --git a/libs/@local/hashql/mir/tests/ui/reify/synthetic-intrinsic-reused.stdout b/libs/@local/hashql/mir/tests/ui/reify/synthetic-intrinsic-reused.stdout new file mode 100644 index 00000000000..5f4eb78ef4c --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/reify/synthetic-intrinsic-reused.stdout @@ -0,0 +1,59 @@ +fn {closure#7}(%0: (), %1: Integer, %2: Integer, %3: (Number, Number) -> Boolean) -> Boolean { + let %4: Boolean + + bb0(): { + %4 = apply %3.0 %3.1 %1 %2 + + return %4 + } +} + +thunk apply:0() -> (Integer, Integer, (Number, Number) -> Boolean) -> Boolean { + let %0: (Integer, Integer, (Number, Number) -> Boolean) -> Boolean + let %1: () + + bb0(): { + %1 = () + %0 = closure(({closure#7} as FnPtr), %1) + + return %0 + } +} + +fn {synthetic#::core::cmp::lte}(%0: (), %1: Number, %2: Number) -> Boolean { + let %3: Boolean + + bb0(): { + %3 = %1 <= %2 + + return %3 + } +} + +thunk r1:0() -> Boolean { + let %0: (Number, Number) -> Boolean + let %1: (Integer, Integer, (Number, Number) -> Boolean) -> Boolean + let %2: Boolean + + bb0(): { + %0 = closure(({synthetic#::core::cmp::lte} as FnPtr), ()) + %1 = apply (apply:0 as FnPtr) + %2 = apply %1.0 %1.1 1 2 %0 + + return %2 + } +} + +*thunk r2:0() -> Boolean { + let %0: (Number, Number) -> Boolean + let %1: (Integer, Integer, (Number, Number) -> Boolean) -> Boolean + let %2: Boolean + + bb0(): { + %0 = closure(({synthetic#::core::cmp::lte} as FnPtr), ()) + %1 = apply (apply:0 as FnPtr) + %2 = apply %1.0 %1.1 3 4 %0 + + return %2 + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/reify/synthetic-intrinsic-value.jsonc b/libs/@local/hashql/mir/tests/ui/reify/synthetic-intrinsic-value.jsonc new file mode 100644 index 00000000000..324f797cc62 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/reify/synthetic-intrinsic-value.jsonc @@ -0,0 +1,14 @@ +//@ run: pass +//@ description: Intrinsic in value position generates a synthetic wrapper body +[ + "let", + "apply", + [ + "fn", + { "#tuple": [] }, + { "#struct": { "a": "Integer", "b": "Integer", "f": "_" } }, + "_", + ["f", "a", "b"] + ], + ["apply", { "#literal": 2 }, { "#literal": 3 }, "<="] +] diff --git a/libs/@local/hashql/mir/tests/ui/reify/synthetic-intrinsic-value.stdout b/libs/@local/hashql/mir/tests/ui/reify/synthetic-intrinsic-value.stdout new file mode 100644 index 00000000000..fb9d5c85f62 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/reify/synthetic-intrinsic-value.stdout @@ -0,0 +1,45 @@ +fn {closure#6}(%0: (), %1: Integer, %2: Integer, %3: (Number, Number) -> Boolean) -> Boolean { + let %4: Boolean + + bb0(): { + %4 = apply %3.0 %3.1 %1 %2 + + return %4 + } +} + +thunk apply:0() -> (Integer, Integer, (Number, Number) -> Boolean) -> Boolean { + let %0: (Integer, Integer, (Number, Number) -> Boolean) -> Boolean + let %1: () + + bb0(): { + %1 = () + %0 = closure(({closure#6} as FnPtr), %1) + + return %0 + } +} + +fn {synthetic#::core::cmp::lte}(%0: (), %1: Number, %2: Number) -> Boolean { + let %3: Boolean + + bb0(): { + %3 = %1 <= %2 + + return %3 + } +} + +*thunk {thunk#5}() -> Boolean { + let %0: (Number, Number) -> Boolean + let %1: (Integer, Integer, (Number, Number) -> Boolean) -> Boolean + let %2: Boolean + + bb0(): { + %0 = closure(({synthetic#::core::cmp::lte} as FnPtr), ()) + %1 = apply (apply:0 as FnPtr) + %2 = apply %1.0 %1.1 2 3 %0 + + return %2 + } +} \ No newline at end of file