diff --git a/library/std/src/Std/OpenQASM/Angle.qs b/library/std/src/Std/OpenQASM/Angle.qs index 95c67dacc8b..5b025a3620e 100644 --- a/library/std/src/Std/OpenQASM/Angle.qs +++ b/library/std/src/Std/OpenQASM/Angle.qs @@ -36,13 +36,29 @@ function AngleAsBoolArrayBE(angle : Angle) : Bool[] { function AngleAsDouble(angle : Angle) : Double { let F64_MANTISSA_DIGITS = 53; - let angle = if angle.Size > F64_MANTISSA_DIGITS { - AdjustAngleSize(angle, F64_MANTISSA_DIGITS, false) + // Keep the branch scalar-valued; partial evaluation cannot lower a dynamic + // branch that returns an Angle struct. + let value = if angle.Size > F64_MANTISSA_DIGITS { + let shift_amount = angle.Size - F64_MANTISSA_DIGITS; + let half = 1 <<< (shift_amount - 1); + let mask = (1 <<< shift_amount) - 1; + let lower_bits = angle.Value &&& mask; + let upper_bits = angle.Value >>> shift_amount; + if lower_bits > half or (lower_bits == half and (upper_bits &&& 1) == 1) { + upper_bits + 1 + } else { + upper_bits + } } else { - angle + angle.Value + }; + let size = if angle.Size > F64_MANTISSA_DIGITS { + F64_MANTISSA_DIGITS + } else { + angle.Size }; - let denom = Std.Convert.IntAsDouble(1 <<< angle.Size); - let value = Std.Convert.IntAsDouble(angle.Value); + let denom = Std.Convert.IntAsDouble(1 <<< size); + let value = Std.Convert.IntAsDouble(value); let factor = (2.0 * Std.Math.PI()) / denom; value * factor } diff --git a/source/compiler/qsc/src/codegen.rs b/source/compiler/qsc/src/codegen.rs index e06422bcd82..4ecc22f7a12 100644 --- a/source/compiler/qsc/src/codegen.rs +++ b/source/compiler/qsc/src/codegen.rs @@ -64,6 +64,16 @@ pub mod qir { }, } + /// Pre-computed type information for a reachable global callable: its formal + /// arrow type and the generic type parameters that type is quantified over. + /// Cached so a call site can infer the callable's concrete generic arguments + /// from the type expected at that site. + #[derive(Clone)] + struct CallableValueInfo { + ty: qsc_fir::ty::Ty, + generics: Vec, + } + /// Extracts the entry point expression from codegen FIR. /// /// Forms a `ProgramEntry` suitable for downstream codegen (QIR, RIR generation) @@ -470,23 +480,250 @@ pub mod qir { package.entry_exec_graph = Default::default(); } - /// Builds a pre-computed map of callable types for all Global/Closure values in `args`. + /// Builds a pre-computed map of normalized callable value types for all + /// `Global` and `Closure` values in `args`. /// /// This allows `lower_value_to_expr` to look up arrow types without holding an immutable /// reference to the package store while also mutating a package. fn build_callable_type_map( fir_store: &qsc_fir::fir::PackageStore, callables: &FxHashSet, - ) -> rustc_hash::FxHashMap { + ) -> rustc_hash::FxHashMap { + use qsc_fir::fir::{Global, PackageLookup}; + let mut map = rustc_hash::FxHashMap::with_capacity_and_hasher(callables.len(), Default::default()); for id in callables { + let package = fir_store.get(id.package); + let Some(Global::Callable(callable_decl)) = package.get_global(id.item) else { + panic!("callable should exist in lowered package"); + }; let (_, ty) = callable_expr_span_and_ty(fir_store, *id); - map.insert(*id, ty); + let normalized_ty = resolve_functor_params(&resolve_udt_ty(fir_store, &ty)); + map.insert( + *id, + CallableValueInfo { + ty: normalized_ty, + generics: callable_decl.generics.clone(), + }, + ); } map } + /// Normalizes concrete runtime callable type copies before synthetic-entry lowering. + /// + /// Interpreter-created callable values can retain inferred functor parameters + /// in their lowered body node types even when the callable itself is concrete. + /// Those stale parameters would violate post-monomorphization invariants once + /// the callable is made entry-reachable. Generic callable signatures are left + /// intact so monomorphization can still infer and create concrete + /// specializations from closure targets. + fn normalize_callable_signatures( + fir_store: &mut qsc_fir::fir::PackageStore, + callables: &FxHashSet, + ) { + use qsc_fir::fir::{CallableImpl, Global, PackageLookup}; + + let normalized: Vec<_> = callables + .iter() + .map(|id| { + let package = fir_store.get(id.package); + let Some(Global::Callable(callable_decl)) = package.get_global(id.item) else { + panic!("callable should exist in lowered package"); + }; + let normalized_signature = if callable_decl.generics.is_empty() { + let input_pat = package.get_pat(callable_decl.input); + Some(( + resolve_functor_params(&resolve_udt_ty(fir_store, &input_pat.ty)), + resolve_functor_params(&resolve_udt_ty(fir_store, &callable_decl.output)), + )) + } else { + None + }; + (*id, callable_decl.input, normalized_signature) + }) + .collect(); + + for (id, input_pat_id, normalized_signature) in normalized { + let package = fir_store.get_mut(id.package); + let qsc_fir::fir::ItemKind::Callable(callable_decl) = &mut package + .items + .get_mut(id.item) + .expect("callable item should exist") + .kind + else { + panic!("callable should exist in lowered package"); + }; + if let Some((input_ty, output_ty)) = normalized_signature { + package + .pats + .get_mut(input_pat_id) + .expect("callable input pattern should exist") + .ty = input_ty; + callable_decl.output = output_ty; + } + let CallableImpl::Spec(spec_impl) = &callable_decl.implementation else { + continue; + }; + let mut block_ids = vec![spec_impl.body.block]; + block_ids.extend( + spec_impl + .adj + .iter() + .chain(spec_impl.ctl.iter()) + .chain(spec_impl.ctl_adj.iter()) + .map(|spec| spec.block), + ); + + for block_id in block_ids { + normalize_block_node_types(package, block_id); + } + } + } + + fn normalize_block_node_types( + package: &mut qsc_fir::fir::Package, + block_id: qsc_fir::fir::BlockId, + ) { + let stmt_ids = package + .blocks + .get_mut(block_id) + .expect("callable block should exist") + .stmts + .clone(); + + for stmt_id in stmt_ids { + let stmt = package + .stmts + .get(stmt_id) + .expect("callable statement should exist"); + let (pat_id, expr_id) = match stmt.kind { + qsc_fir::fir::StmtKind::Expr(expr_id) | qsc_fir::fir::StmtKind::Semi(expr_id) => { + (None, Some(expr_id)) + } + qsc_fir::fir::StmtKind::Local(_, pat_id, expr_id) => (Some(pat_id), Some(expr_id)), + qsc_fir::fir::StmtKind::Item(_) => (None, None), + }; + + if let Some(pat_id) = pat_id { + normalize_pat_node_types(package, pat_id); + } + if let Some(expr_id) = expr_id { + normalize_expr_node_types(package, expr_id); + } + } + + let block = package + .blocks + .get_mut(block_id) + .expect("callable block should exist"); + block.ty = resolve_functor_params(&block.ty); + } + + fn normalize_pat_node_types(package: &mut qsc_fir::fir::Package, pat_id: qsc_fir::fir::PatId) { + let child_pats = { + let pat = package + .pats + .get_mut(pat_id) + .expect("callable pattern should exist"); + pat.ty = resolve_functor_params(&pat.ty); + match &pat.kind { + qsc_fir::fir::PatKind::Tuple(pats) => pats.clone(), + qsc_fir::fir::PatKind::Bind(_) | qsc_fir::fir::PatKind::Discard => Vec::new(), + } + }; + for child_pat in child_pats { + normalize_pat_node_types(package, child_pat); + } + } + + fn normalize_expr_node_types( + package: &mut qsc_fir::fir::Package, + expr_id: qsc_fir::fir::ExprId, + ) { + let (child_exprs, child_blocks) = { + let expr = package + .exprs + .get_mut(expr_id) + .expect("callable expression should exist"); + expr.ty = resolve_functor_params(&expr.ty); + child_nodes_for_expr_kind(&expr.kind) + }; + + for child_expr in child_exprs { + normalize_expr_node_types(package, child_expr); + } + for child_block in child_blocks { + normalize_block_node_types(package, child_block); + } + } + + fn child_nodes_for_expr_kind( + kind: &qsc_fir::fir::ExprKind, + ) -> (Vec, Vec) { + let mut exprs = Vec::new(); + let mut blocks = Vec::new(); + match kind { + qsc_fir::fir::ExprKind::Array(child_exprs) + | qsc_fir::fir::ExprKind::ArrayLit(child_exprs) + | qsc_fir::fir::ExprKind::Tuple(child_exprs) => exprs.extend(child_exprs.iter()), + qsc_fir::fir::ExprKind::ArrayRepeat(left, right) + | qsc_fir::fir::ExprKind::Assign(left, right) + | qsc_fir::fir::ExprKind::AssignOp(_, left, right) + | qsc_fir::fir::ExprKind::BinOp(_, left, right) + | qsc_fir::fir::ExprKind::Call(left, right) + | qsc_fir::fir::ExprKind::Index(left, right) + | qsc_fir::fir::ExprKind::AssignField(left, _, right) + | qsc_fir::fir::ExprKind::UpdateField(left, _, right) => { + exprs.push(*left); + exprs.push(*right); + } + qsc_fir::fir::ExprKind::AssignIndex(first, second, third) + | qsc_fir::fir::ExprKind::UpdateIndex(first, second, third) => { + exprs.push(*first); + exprs.push(*second); + exprs.push(*third); + } + qsc_fir::fir::ExprKind::Block(block_id) => blocks.push(*block_id), + qsc_fir::fir::ExprKind::Closure(_, _) + | qsc_fir::fir::ExprKind::Hole + | qsc_fir::fir::ExprKind::Lit(_) + | qsc_fir::fir::ExprKind::Var(_, _) => {} + qsc_fir::fir::ExprKind::Fail(expr_id) + | qsc_fir::fir::ExprKind::Field(expr_id, _) + | qsc_fir::fir::ExprKind::Return(expr_id) + | qsc_fir::fir::ExprKind::UnOp(_, expr_id) => exprs.push(*expr_id), + qsc_fir::fir::ExprKind::If(cond, body, otherwise) => { + exprs.push(*cond); + exprs.push(*body); + if let Some(otherwise) = otherwise { + exprs.push(*otherwise); + } + } + qsc_fir::fir::ExprKind::Range(start, step, end) => { + exprs.extend([start, step, end].into_iter().flatten().copied()); + } + qsc_fir::fir::ExprKind::Struct(_, copy, fields) => { + if let Some(copy) = copy { + exprs.push(*copy); + } + exprs.extend(fields.iter().map(|field| field.value)); + } + qsc_fir::fir::ExprKind::String(components) => { + exprs.extend(components.iter().filter_map(|component| match component { + qsc_fir::fir::StringComponent::Expr(expr_id) => Some(*expr_id), + qsc_fir::fir::StringComponent::Lit(_) => None, + })); + } + qsc_fir::fir::ExprKind::While(cond, block_id) => { + exprs.push(*cond); + blocks.push(*block_id); + } + } + (exprs, blocks) + } + /// Seeds the package entry with a synthetic `Call(target, args)` expression. /// /// Builds args matching the target callable's pure input type: callable-typed positions @@ -498,7 +735,7 @@ pub mod qir { fir_package_id: qsc_fir::fir::PackageId, target_callable: qsc_fir::fir::StoreItemId, args: &Value, - callable_types: &rustc_hash::FxHashMap, + callable_types: &rustc_hash::FxHashMap, ) { use qsc_fir::fir::{Global, PackageLookup}; @@ -509,18 +746,17 @@ pub mod qir { }; let span = callable_decl.span; let input_pat = package.get_pat(callable_decl.input); - let input_ty = resolve_functor_params(&resolve_udt_ty(fir_store, &input_pat.ty)); - let output_ty = resolve_functor_params(&resolve_udt_ty(fir_store, &callable_decl.output)); - let arrow_ty = qsc_fir::ty::Ty::Arrow(Box::new(qsc_fir::ty::Arrow { - kind: callable_decl.kind, - input: Box::new(input_ty.clone()), - output: Box::new(output_ty.clone()), - functors: qsc_fir::ty::FunctorSet::Value(callable_decl.functors), - })); - - // Build concrete generic args for the callee Var so monomorphization can - // resolve FunctorSet::Param in the specialized clone's body types. - let generic_args = build_concrete_generic_args(&callable_decl.generics); + let formal_input_ty = resolve_udt_ty(fir_store, &input_pat.ty); + let formal_output_ty = resolve_udt_ty(fir_store, &callable_decl.output); + let (generic_args, input_ty, output_ty, arrow_ty) = instantiate_synthetic_target_arrow( + callable_decl.generics.as_slice(), + callable_decl.kind, + callable_decl.functors, + &formal_input_ty, + &formal_output_ty, + args, + callable_types, + ); // Build assigner from the package's current ID counters. let mut assigner = qsc_fir::assigner::Assigner::from_package(fir_store.get(fir_package_id)); @@ -624,6 +860,50 @@ pub mod qir { package.entry_exec_graph = Default::default(); } + /// Infers concrete generic arguments for the synthetic target invocation and + /// returns the target's instantiated input, output, and arrow types. + /// + /// The synthetic entry is built before the normal monomorphization pass can + /// specialize the target for these runtime arguments. Instantiating the + /// arrow here keeps the synthetic call structurally concrete, so later FIR + /// passes do not see unresolved type or functor parameters. + fn instantiate_synthetic_target_arrow( + generics: &[qsc_fir::ty::TypeParameter], + kind: qsc_fir::fir::CallableKind, + functors: qsc_fir::ty::FunctorSetValue, + formal_input_ty: &qsc_fir::ty::Ty, + formal_output_ty: &qsc_fir::ty::Ty, + args: &Value, + callable_types: &rustc_hash::FxHashMap, + ) -> ( + Vec, + qsc_fir::ty::Ty, + qsc_fir::ty::Ty, + qsc_fir::ty::Ty, + ) { + let generic_args = + infer_target_generic_args(generics, formal_input_ty, args, callable_types); + let formal_arrow = qsc_fir::ty::Arrow { + kind, + input: Box::new(formal_input_ty.clone()), + output: Box::new(formal_output_ty.clone()), + functors: qsc_fir::ty::FunctorSet::Value(functors), + }; + let instantiated_arrow = + qsc_fir::ty::Scheme::new(generics.to_vec(), Box::new(formal_arrow.clone())) + .instantiate(&generic_args) + .unwrap_or(formal_arrow); + let input_ty = resolve_functor_params(&instantiated_arrow.input); + let output_ty = resolve_functor_params(&instantiated_arrow.output); + let arrow_ty = qsc_fir::ty::Ty::Arrow(Box::new(qsc_fir::ty::Arrow { + kind: instantiated_arrow.kind, + input: Box::new(input_ty.clone()), + output: Box::new(output_ty.clone()), + functors: instantiated_arrow.functors, + })); + (generic_args, input_ty, output_ty, arrow_ty) + } + /// Builds an args expression matching the target's input type. /// /// For callable-typed positions, uses the corresponding callable from `args`. @@ -635,7 +915,7 @@ pub mod qir { assigner: &mut qsc_fir::assigner::Assigner, input_ty: &qsc_fir::ty::Ty, args: &Value, - callable_types: &rustc_hash::FxHashMap, + callable_types: &rustc_hash::FxHashMap, pending_stmts: &mut Vec, ) -> qsc_fir::fir::ExprId { match input_ty { @@ -672,7 +952,7 @@ pub mod qir { package, assigner, args, - None, + Some(elem_ty), callable_types, pending_stmts, )); @@ -725,9 +1005,16 @@ pub mod qir { } qsc_fir::ty::Ty::Arrow(_) => { // Arrow-typed position — the args must be a callable value. - lower_value_to_expr(package, assigner, args, None, callable_types, pending_stmts) + lower_value_to_expr( + package, + assigner, + args, + Some(input_ty), + callable_types, + pending_stmts, + ) } - qsc_fir::ty::Ty::Array(elem_ty) => { + qsc_fir::ty::Ty::Array(_) => { // Array position — lower the value, threading the declared element // type so empty (and nested-empty) arrays carry their real element // type instead of `Ty::Err`. @@ -735,7 +1022,7 @@ pub mod qir { package, assigner, args, - Some(elem_ty.as_ref()), + Some(input_ty), callable_types, pending_stmts, ) @@ -750,7 +1037,7 @@ pub mod qir { package, assigner, args, - None, + Some(input_ty), callable_types, pending_stmts, ), @@ -862,27 +1149,153 @@ pub mod qir { } } - /// Builds concrete generic args from a callable's generic parameter list. + /// Builds concrete generic args from a target callable's input and the + /// runtime argument values supplied to the synthetic entry. /// - /// For each `TypeParameter::Functor`, produces `GenericArg::Functor(Value(Empty))`. - /// For each `TypeParameter::Ty`, produces `GenericArg::Ty(Tuple([]))` (unit). - /// These concrete args let monomorphization create a fully resolved specialization. - fn build_concrete_generic_args( + /// Any parameter that cannot be inferred from the argument value tree falls + /// back to the same concrete defaults used before synthetic-entry inference: + /// type parameters become `Unit`, and functor parameters become `Empty`. + fn infer_target_generic_args( generics: &[qsc_fir::ty::TypeParameter], + formal_input_ty: &qsc_fir::ty::Ty, + args: &Value, + callable_types: &rustc_hash::FxHashMap, ) -> Vec { + let mut arg_map = rustc_hash::FxHashMap::default(); + if let Some(actual_input_ty) = + value_ty_for_inference(args, Some(formal_input_ty), callable_types) + { + let _ = infer_generic_ty_args(formal_input_ty, &actual_input_ty, &mut arg_map); + } + generics .iter() - .map(|param| match param { - qsc_fir::ty::TypeParameter::Functor(_) => qsc_fir::ty::GenericArg::Functor( - qsc_fir::ty::FunctorSet::Value(qsc_fir::ty::FunctorSetValue::Empty), - ), - qsc_fir::ty::TypeParameter::Ty { .. } => { - qsc_fir::ty::GenericArg::Ty(qsc_fir::ty::Ty::Tuple(Vec::new())) + .enumerate() + .map(|(idx, param)| { + let inferred = arg_map.get(&qsc_fir::ty::ParamId::from(idx)); + match (param, inferred) { + ( + qsc_fir::ty::TypeParameter::Ty { .. }, + Some(qsc_fir::ty::GenericArg::Ty(ty)), + ) if !ty_contains_param(ty) => qsc_fir::ty::GenericArg::Ty(ty.clone()), + ( + qsc_fir::ty::TypeParameter::Functor(_), + Some(qsc_fir::ty::GenericArg::Functor(functors)), + ) if matches!(functors, qsc_fir::ty::FunctorSet::Value(_)) => { + qsc_fir::ty::GenericArg::Functor(*functors) + } + _ => default_generic_arg(param), } }) .collect() } + /// Produces the concrete fallback used when an individual generic parameter + /// cannot be inferred from the runtime argument value. + fn default_generic_arg(param: &qsc_fir::ty::TypeParameter) -> qsc_fir::ty::GenericArg { + match param { + qsc_fir::ty::TypeParameter::Functor(_) => qsc_fir::ty::GenericArg::Functor( + qsc_fir::ty::FunctorSet::Value(qsc_fir::ty::FunctorSetValue::Empty), + ), + qsc_fir::ty::TypeParameter::Ty { .. } => { + qsc_fir::ty::GenericArg::Ty(qsc_fir::ty::Ty::Tuple(Vec::new())) + } + } + } + + /// Returns true when a type still contains an unresolved type parameter or + /// parametric functor set. + fn ty_contains_param(ty: &qsc_fir::ty::Ty) -> bool { + match ty { + qsc_fir::ty::Ty::Param(_) => true, + qsc_fir::ty::Ty::Array(item) => ty_contains_param(item), + qsc_fir::ty::Ty::Arrow(arrow) => { + matches!(arrow.functors, qsc_fir::ty::FunctorSet::Param(_)) + || ty_contains_param(&arrow.input) + || ty_contains_param(&arrow.output) + } + qsc_fir::ty::Ty::Tuple(items) => items.iter().any(ty_contains_param), + qsc_fir::ty::Ty::Err + | qsc_fir::ty::Ty::Infer(_) + | qsc_fir::ty::Ty::Prim(_) + | qsc_fir::ty::Ty::Udt(_) => false, + } + } + + /// Reconstructs the best FIR type shape available from an interpreter value. + /// + /// This is used only for generic inference. Runtime identities that cannot + /// be lowered into synthetic FIR, such as qubits or dynamic variables, can + /// still expose enough type information to instantiate the target arrow. + /// Callable values prefer the expected type when it is compatible with the + /// callable's declared generic scheme, preserving caller-provided concrete + /// arrow types for generic globals. + fn value_ty_for_inference( + value: &Value, + expected_ty: Option<&qsc_fir::ty::Ty>, + callable_types: &rustc_hash::FxHashMap, + ) -> Option { + match value { + Value::Int(_) => Some(qsc_fir::ty::Ty::Prim(qsc_fir::ty::Prim::Int)), + Value::Double(_) => Some(qsc_fir::ty::Ty::Prim(qsc_fir::ty::Prim::Double)), + Value::Bool(_) => Some(qsc_fir::ty::Ty::Prim(qsc_fir::ty::Prim::Bool)), + Value::BigInt(_) => Some(qsc_fir::ty::Ty::Prim(qsc_fir::ty::Prim::BigInt)), + Value::Pauli(_) => Some(qsc_fir::ty::Ty::Prim(qsc_fir::ty::Prim::Pauli)), + Value::Qubit(_) => Some(qsc_fir::ty::Ty::Prim(qsc_fir::ty::Prim::Qubit)), + Value::Range(_) => Some(qsc_fir::ty::Ty::Prim(qsc_fir::ty::Prim::Range)), + Value::Result(_) => Some(qsc_fir::ty::Ty::Prim(qsc_fir::ty::Prim::Result)), + Value::String(_) => Some(qsc_fir::ty::Ty::Prim(qsc_fir::ty::Prim::String)), + Value::Tuple(values, _) => { + let expected_items = match expected_ty { + Some(qsc_fir::ty::Ty::Tuple(items)) if items.len() == values.len() => { + Some(items.as_slice()) + } + _ => None, + }; + values + .iter() + .enumerate() + .map(|(idx, value)| { + value_ty_for_inference( + value, + expected_items.map(|items| &items[idx]), + callable_types, + ) + }) + .collect::>>() + .map(qsc_fir::ty::Ty::Tuple) + } + Value::Array(values) => { + let expected_item = match expected_ty { + Some(qsc_fir::ty::Ty::Array(item)) => Some(item.as_ref()), + _ => None, + }; + let item_ty = values + .first() + .and_then(|value| value_ty_for_inference(value, expected_item, callable_types)) + .or_else(|| expected_item.cloned())?; + Some(qsc_fir::ty::Ty::Array(Box::new(item_ty))) + } + Value::Global(id, _) => { + let info = callable_types.get(id)?; + if let Some(expected_ty) = expected_ty + && infer_global_generic_args(&info.generics, &info.ty, expected_ty).is_some() + { + return Some(expected_ty.clone()); + } + Some(info.ty.clone()) + } + Value::Closure(closure) => { + let info = callable_types.get(&closure.id)?; + Some(partial_applied_closure_ty( + &info.ty, + closure.fixed_args.len(), + )) + } + Value::Var(_) => expected_ty.cloned(), + } + } + /// Lowers an interpreter `Value` into a FIR expression for the synthetic entry. /// /// Scalar values become literals, aggregate values are lowered recursively, and @@ -893,8 +1306,8 @@ pub mod qir { package: &mut qsc_fir::fir::Package, assigner: &mut qsc_fir::assigner::Assigner, value: &Value, - elem_ty_hint: Option<&qsc_fir::ty::Ty>, - callable_types: &rustc_hash::FxHashMap, + expected_ty: Option<&qsc_fir::ty::Ty>, + callable_types: &rustc_hash::FxHashMap, pending_stmts: &mut Vec, ) -> qsc_fir::fir::ExprId { let (kind, ty) = match value { @@ -931,14 +1344,20 @@ pub mod qir { qsc_fir::ty::Ty::Prim(qsc_fir::ty::Prim::String), ), Value::Tuple(vs, _) => { + let elem_ty_hints = match expected_ty { + Some(qsc_fir::ty::Ty::Tuple(elem_tys)) if elem_tys.len() == vs.len() => { + Some(elem_tys) + } + _ => None, + }; let mut lowered_ids = Vec::with_capacity(vs.len()); let mut lowered_tys = Vec::with_capacity(vs.len()); - for v in vs.iter() { + for (idx, v) in vs.iter().enumerate() { let id = lower_value_to_expr( package, assigner, v, - None, + elem_ty_hints.map(|elem_tys| &elem_tys[idx]), callable_types, pending_stmts, ); @@ -951,9 +1370,9 @@ pub mod qir { ) } Value::Array(vs) => { - // Decompose the declared element-type hint so empty (and nested-empty) + // Decompose the declared array type so empty (and nested-empty) // arrays can recover their real element type instead of `Ty::Err`. - let inner_hint: Option<&qsc_fir::ty::Ty> = match elem_ty_hint { + let inner_hint: Option<&qsc_fir::ty::Ty> = match expected_ty { Some(qsc_fir::ty::Ty::Array(inner)) => Some(inner.as_ref()), _ => None, }; @@ -970,9 +1389,9 @@ pub mod qir { } let elem_ty = match lowered_ids.first() { Some(id) => package.exprs.get(*id).expect("just inserted").ty.clone(), - // For an empty array the element type is the declared hint - // for this array, not the element's element type. - None => elem_ty_hint.cloned().unwrap_or(qsc_fir::ty::Ty::Err), + // For an empty array the element type is the declared array's + // element type, not the nested element hint. + None => inner_hint.cloned().unwrap_or(qsc_fir::ty::Ty::Err), }; ( qsc_fir::fir::ExprKind::Array(lowered_ids), @@ -1009,7 +1428,14 @@ pub mod qir { ) } Value::Global(id, functor) => { - return lower_global_to_expr(package, assigner, *id, *functor, callable_types); + return lower_global_to_expr( + package, + assigner, + *id, + *functor, + expected_ty, + callable_types, + ); } Value::Closure(c) => { return lower_closure_to_expr(package, assigner, c, callable_types, pending_stmts); @@ -1041,12 +1467,19 @@ pub mod qir { assigner: &mut qsc_fir::assigner::Assigner, id: qsc_fir::fir::StoreItemId, functor: FunctorApp, - callable_types: &rustc_hash::FxHashMap, + expected_ty: Option<&qsc_fir::ty::Ty>, + callable_types: &rustc_hash::FxHashMap, ) -> qsc_fir::fir::ExprId { - let ty = callable_types + let info = callable_types .get(&id) .expect("Global callable type must be pre-computed") .clone(); + let formal_ty = info.ty; + let inferred_generic_args = expected_ty.and_then(|actual_ty| { + infer_global_generic_args(&info.generics, &formal_ty, actual_ty) + .map(|generic_args| (actual_ty.clone(), generic_args)) + }); + let (ty, generic_args) = inferred_generic_args.unwrap_or_else(|| (formal_ty, Vec::new())); let expr_id = assigner.next_expr(); package.exprs.insert( expr_id, @@ -1059,7 +1492,7 @@ pub mod qir { package: id.package, item: id.item, }), - Vec::new(), + generic_args, ), exec_graph_range: qsc_fir::fir::ExecGraphIdx::ZERO ..qsc_fir::fir::ExecGraphIdx::ZERO, @@ -1068,6 +1501,115 @@ pub mod qir { wrap_expr_with_functor_app(package, assigner, expr_id, &ty, functor) } + /// Infers the concrete generic arguments for a reference to a generic global + /// callable by matching its formal type against the `actual_ty` expected at + /// the use site. + /// + /// Returns `None` when the callable is non-generic or the two types do not + /// unify; otherwise returns one `GenericArg` per declared parameter in + /// declaration order. + fn infer_global_generic_args( + generics: &[qsc_fir::ty::TypeParameter], + formal_ty: &qsc_fir::ty::Ty, + actual_ty: &qsc_fir::ty::Ty, + ) -> Option> { + if generics.is_empty() { + return None; + } + let mut arg_map = rustc_hash::FxHashMap::default(); + if !infer_generic_ty_args(formal_ty, actual_ty, &mut arg_map) { + return None; + } + generics + .iter() + .enumerate() + .map( + |(idx, param)| match (param, arg_map.get(&qsc_fir::ty::ParamId::from(idx))) { + ( + qsc_fir::ty::TypeParameter::Ty { .. }, + Some(qsc_fir::ty::GenericArg::Ty(ty)), + ) => Some(qsc_fir::ty::GenericArg::Ty(ty.clone())), + ( + qsc_fir::ty::TypeParameter::Functor(_), + Some(qsc_fir::ty::GenericArg::Functor(functors)), + ) => Some(qsc_fir::ty::GenericArg::Functor(*functors)), + _ => None, + }, + ) + .collect() + } + + /// Structurally unifies a formal type against an actual type, recording each + /// type/functor parameter binding in `arg_map`. + /// + /// Returns `false` on any structural mismatch or on conflicting bindings for + /// the same parameter (see [`record_inferred_arg`]). + fn infer_generic_ty_args( + formal: &qsc_fir::ty::Ty, + actual: &qsc_fir::ty::Ty, + arg_map: &mut rustc_hash::FxHashMap, + ) -> bool { + match (formal, actual) { + (qsc_fir::ty::Ty::Param(param), _) => { + record_inferred_arg(*param, qsc_fir::ty::GenericArg::Ty(actual.clone()), arg_map) + } + (qsc_fir::ty::Ty::Array(formal), qsc_fir::ty::Ty::Array(actual)) => { + infer_generic_ty_args(formal, actual, arg_map) + } + (qsc_fir::ty::Ty::Arrow(formal), qsc_fir::ty::Ty::Arrow(actual)) => { + formal.kind == actual.kind + && infer_generic_ty_args(&formal.input, &actual.input, arg_map) + && infer_generic_ty_args(&formal.output, &actual.output, arg_map) + && infer_generic_functor_args(formal.functors, actual.functors, arg_map) + } + (qsc_fir::ty::Ty::Tuple(formal), qsc_fir::ty::Ty::Tuple(actual)) + if formal.len() == actual.len() => + { + formal + .iter() + .zip(actual) + .all(|(formal, actual)| infer_generic_ty_args(formal, actual, arg_map)) + } + (qsc_fir::ty::Ty::Prim(formal), qsc_fir::ty::Ty::Prim(actual)) => formal == actual, + (qsc_fir::ty::Ty::Udt(formal), qsc_fir::ty::Ty::Udt(actual)) => formal == actual, + (qsc_fir::ty::Ty::Infer(formal), qsc_fir::ty::Ty::Infer(actual)) => formal == actual, + (qsc_fir::ty::Ty::Err, qsc_fir::ty::Ty::Err) => true, + _ => false, + } + } + + /// Unifies a formal functor set against an actual one, recording the binding + /// when the formal side is a functor parameter; otherwise requires the two + /// sets to be equal. + fn infer_generic_functor_args( + formal: qsc_fir::ty::FunctorSet, + actual: qsc_fir::ty::FunctorSet, + arg_map: &mut rustc_hash::FxHashMap, + ) -> bool { + match formal { + qsc_fir::ty::FunctorSet::Param(param) => { + record_inferred_arg(param, qsc_fir::ty::GenericArg::Functor(actual), arg_map) + } + _ => formal == actual, + } + } + + /// Records the inferred argument for a generic parameter, returning whether + /// it is consistent: a first binding is inserted and accepted, while a + /// repeated binding must equal the one already recorded. + fn record_inferred_arg( + param: qsc_fir::ty::ParamId, + arg: qsc_fir::ty::GenericArg, + arg_map: &mut rustc_hash::FxHashMap, + ) -> bool { + if let Some(existing) = arg_map.get(¶m) { + existing == &arg + } else { + arg_map.insert(param, arg); + true + } + } + /// Wraps a callable expression with the FIR functor operations in `functor`. /// /// Adjoint is applied before each controlled application to match the runtime @@ -1136,7 +1678,7 @@ pub mod qir { package: &mut qsc_fir::fir::Package, assigner: &mut qsc_fir::assigner::Assigner, closure: &qsc_eval::val::Closure, - callable_types: &rustc_hash::FxHashMap, + callable_types: &rustc_hash::FxHashMap, pending_stmts: &mut Vec, ) -> qsc_fir::fir::ExprId { // Full type of the underlying lifted callable, whose input is the tuple @@ -1144,6 +1686,7 @@ pub mod qir { let full_ty = callable_types .get(&closure.id) .expect("Closure callable type must be pre-computed") + .ty .clone(); if closure.fixed_args.is_empty() { @@ -1181,13 +1724,16 @@ pub mod qir { // build an `ExprKind::Closure` value referencing those locals. The capture // bindings are emitted in their original leading order so partial evaluation // reconstructs the closure's fixed arguments correctly. + let capture_ty_hints = closure_capture_ty_hints(&full_ty, closure.fixed_args.len()); let mut capture_locals = Vec::with_capacity(closure.fixed_args.len()); - for capture in closure.fixed_args.iter() { + for (idx, capture) in closure.fixed_args.iter().enumerate() { let value_expr_id = lower_value_to_expr( package, assigner, capture, - None, + capture_ty_hints + .as_ref() + .map(|capture_tys| &capture_tys[idx]), callable_types, pending_stmts, ); @@ -1221,6 +1767,25 @@ pub mod qir { wrap_expr_with_functor_app(package, assigner, expr_id, &closure_ty, closure.functor) } + /// Returns declared types for the captured prefix of a lifted closure callable. + /// + /// Capturing closures are lowered as callables whose input tuple starts with + /// the fixed capture values followed by the explicit argument. These hints let + /// captured values, including empty arrays, keep the types from the lowered + /// callable signature when they are reconstructed in the synthetic entry. + fn closure_capture_ty_hints( + full_ty: &qsc_fir::ty::Ty, + capture_count: usize, + ) -> Option> { + let qsc_fir::ty::Ty::Arrow(arrow) = full_ty else { + return None; + }; + let qsc_fir::ty::Ty::Tuple(elems) = arrow.input.as_ref() else { + return None; + }; + (elems.len() >= capture_count).then(|| elems[..capture_count].to_vec()) + } + /// Binds a lowered value expression to a fresh immutable local. /// /// Returns the `Local` statement and the new local variable id so the caller can @@ -1359,11 +1924,11 @@ pub mod qir { /// Prepares codegen FIR when a callable is invoked with concrete argument values. /// - /// Uses a synthetic `Call(Var(target), args)` entry expression when callable args - /// can be represented as FIR values, making the target and args entry-reachable for full - /// pipeline participation. Falls back to a pin-based approach when: - /// - Args contain closures with captures (partial applications require capture context - /// that can't be represented in the synthetic Call) + /// Uses a synthetic `Call(Var(target), args)` entry expression when callable + /// args can be represented as FIR values, making the target and args + /// entry-reachable for full pipeline participation. Falls back to a + /// pin-based approach when args contain runtime identities that cannot be + /// represented as FIR values. /// /// The original target is pinned for DCE survival so that `fir_to_qir_from_callable` /// can still use the original ID for partial evaluation. @@ -1393,12 +1958,12 @@ pub mod qir { )); } - // Closures whose captures are runtime identities (allocated qubits or - // dynamic values) cannot be reconstructed as FIR literals, so they keep the - // pin-based approach where partial evaluation supplies the captures from the - // runtime closure value at QIR generation time. Closures whose captures are - // classical values flow into the self-contained synthetic entry below. - if has_closure_with_captures(args) && !captures_are_fir_lowerable(args) { + // Runtime identities (allocated qubits, dynamic values, and closures + // that capture them) cannot be reconstructed as FIR literals, so they + // keep the pin-based approach where partial evaluation supplies the + // original values at QIR generation time. Fully lowerable values flow + // into the self-contained synthetic entry below. + if !value_is_fir_lowerable(args) { let codegen_fir = prepare_codegen_fir_from_callable_args_pinned( package_store, callable, @@ -1417,8 +1982,11 @@ pub mod qir { let (mut fir_store, fir_package_id, _assigner) = lower_to_fir(package_store, callable.package, None); - // Pre-compute callable type map (immutable store access) before mutating. + // Pre-compute callable value types before normalizing concrete callable + // bodies, so closure values still expose the original generic target + // signatures needed by monomorphization. let callable_types = build_callable_type_map(&fir_store, &concrete_callables); + normalize_callable_signatures(&mut fir_store, &concrete_callables); // Build synthetic Call(Var(target), args) as the entry expression. // This makes the target and all callable args entry-reachable for pipeline transforms. @@ -1430,8 +1998,8 @@ pub mod qir { &callable_types, ); - // Captureless callable values — whether passed directly or wrapped inside - // a UDT with one or more callable fields — lower into a self-contained + // FIR-lowerable callable values — whether passed directly, captured by a + // closure, or wrapped inside a UDT field — lower into a self-contained // synthetic entry that is evaluated directly. Field-typed callables hidden // inside a UDT collapse during defunctionalization and UDT erasure so the // entry's argument shape stays aligned with the specialized body. @@ -1467,7 +2035,7 @@ pub mod qir { )) } - /// Pin-based fallback for callable args containing closures with captures. + /// Pin-based fallback for callable args containing non-lowerable closure captures. /// /// Seeds concrete (non-arrow-input) callables into the entry for reachability, /// pins arrow-input callables and the target for DCE survival, and lets @@ -1541,33 +2109,8 @@ pub mod qir { }) } - /// Returns `true` if the value tree contains any closures with captures. - fn has_closure_with_captures(value: &Value) -> bool { - match value { - Value::Closure(c) => !c.fixed_args.is_empty(), - Value::Tuple(vs, _) => vs.iter().any(has_closure_with_captures), - Value::Array(vs) => vs.iter().any(has_closure_with_captures), - _ => false, - } - } - - /// Returns `true` if every closure capture in the value tree can be lowered into - /// a FIR literal for the synthetic entry. - /// - /// Only the captures themselves are inspected. Top-level runtime arguments (for - /// example, freshly allocated qubit registers) are always acceptable because the - /// synthetic entry allocates them; the restriction applies to values a closure - /// has already fixed, which must be reconstructable as classical literals. - fn captures_are_fir_lowerable(value: &Value) -> bool { - match value { - Value::Closure(c) => c.fixed_args.iter().all(value_is_fir_lowerable), - Value::Tuple(vs, _) => vs.iter().all(captures_are_fir_lowerable), - Value::Array(vs) => vs.iter().all(captures_are_fir_lowerable), - _ => true, - } - } - - /// Returns `true` if a captured value can be reconstructed as a FIR literal. + /// Returns `true` if a value can be reconstructed inside the synthetic entry + /// as FIR literals and callable references. /// /// Runtime identities such as allocated qubits and dynamic measurement results /// have no classical literal form and therefore cannot be lowered. diff --git a/source/compiler/qsc/src/codegen/tests.rs b/source/compiler/qsc/src/codegen/tests.rs index 41d0c16f708..61ef84fef06 100644 --- a/source/compiler/qsc/src/codegen/tests.rs +++ b/source/compiler/qsc/src/codegen/tests.rs @@ -8,9 +8,7 @@ mod adaptive_ri_profile; mod adaptive_rif_profile; mod base_profile; -use std::sync::Arc; - -use std::rc::Rc; +use std::{io::Cursor, rc::Rc, sync::Arc}; use expect_test::expect; use miette::Report; @@ -20,9 +18,11 @@ use qsc_data_structures::{ source::SourceMap, target::{Profile, TargetCapabilityFlags}, }; +use qsc_eval::output::CursorReceiver; use qsc_eval::val::Value; use qsc_frontend::compile::parse_all; use qsc_hir::hir::{ItemKind, PackageId}; +use qsc_passes::PackageType; use rustc_hash::FxHashMap; use crate::codegen::qir::{ @@ -2204,6 +2204,32 @@ fn callable_args_to_qir( } } +fn eval_fragments(interpreter: &mut crate::interpret::Interpreter, source: &str) -> Value { + let mut cursor = Cursor::new(Vec::::new()); + let mut receiver = CursorReceiver::new(&mut cursor); + interpreter + .eval_fragments(&mut receiver, source) + .unwrap_or_else(|errors| { + panic!("eval_fragments failed: {}", format_interpret_errors(errors)) + }) +} + +fn interpreter_with_capabilities( + capabilities: TargetCapabilityFlags, +) -> crate::interpret::Interpreter { + let (std_id, store) = crate::compile::package_store_with_stdlib(capabilities); + let dependencies = &[(std_id, None)]; + crate::interpret::Interpreter::new( + SourceMap::default(), + PackageType::Lib, + capabilities, + LanguageFeatures::default(), + store, + dependencies, + ) + .expect("interpreter should be created") +} + // ---- Synthetic path: arrow + non-callable params (tuple input) ---- #[test] @@ -2282,6 +2308,87 @@ fn synthetic_path_two_arrow_args_generates_qir() { ); } +#[test] +fn synthetic_path_generic_target_infers_type_from_callable_arg() { + let source = indoc::indoc! {r#" + namespace Test { + operation ApplyGeneric<'T>(op : 'T => Unit, value : 'T) : Result { + op(value); + use q = Qubit(); + return MResetZ(q); + } + operation DoHIfOne(n : Int) : Unit { + if n == 1 { + use q = Qubit(); + H(q); + Reset(q); + } + } + } + "#}; + let caps = Profile::AdaptiveRIF.into(); + let (store, pkg, items) = + compile_and_locate_items(source, &[("ApplyGeneric", true), ("DoHIfOne", true)], caps); + + let do_h_if_one = Value::Global(fir_id_for(pkg, items["DoHIfOne"]), FunctorApp::default()); + let args = Value::Tuple(vec![do_h_if_one, Value::Int(1)].into(), None); + + let target_hir = hir_id_for(pkg, items["ApplyGeneric"]); + let (_codegen_fir, backend) = prepare_codegen_fir_from_callable_args( + &store, target_hir, &args, caps, + ) + .unwrap_or_else(|errors| { + panic!( + "generic target should produce CodegenFir, got: {}", + format_interpret_errors(errors) + ) + }); + assert!( + matches!(backend, CallableArgsBackend::SyntheticEntry), + "lowerable generic target args should use the synthetic entry" + ); + + let qir = callable_args_to_qir(&store, pkg, items["ApplyGeneric"], &args, caps); + assert!( + qir.contains("__quantum__qis__h__body"), + "expected generic target to infer T = Int and emit H:\n{qir}" + ); +} + +#[test] +fn callable_args_with_top_level_qubit_value_use_reinvoke_original() { + let source = indoc::indoc! {r#" + namespace Test { + operation ApplyGeneric<'T>(op : 'T => Unit, value : 'T) : Unit { + op(value); + } + operation DoH(q : Qubit) : Unit { H(q); } + } + "#}; + let caps = Profile::AdaptiveRIF.into(); + let (store, pkg, items) = + compile_and_locate_items(source, &[("ApplyGeneric", true), ("DoH", true)], caps); + + let do_h = Value::Global(fir_id_for(pkg, items["DoH"]), FunctorApp::default()); + let qubit = Rc::new(qsc_eval::val::Qubit(0)); + let args = Value::Tuple(vec![do_h, Value::Qubit((&qubit).into())].into(), None); + + let target_hir = hir_id_for(pkg, items["ApplyGeneric"]); + let (_codegen_fir, backend) = prepare_codegen_fir_from_callable_args( + &store, target_hir, &args, caps, + ) + .unwrap_or_else(|errors| { + panic!( + "top-level qubit callable args should produce CodegenFir, got: {}", + format_interpret_errors(errors) + ) + }); + assert!( + matches!(backend, CallableArgsBackend::ReinvokeOriginal { .. }), + "top-level runtime qubit values must not be lowered into the synthetic entry" + ); +} + // ---- Synthetic path: arrow sandwiched between non-callable params ---- #[test] @@ -2566,299 +2673,625 @@ fn synthetic_path_classical_capture_closure_generates_qir() { } #[test] -fn classical_capture_closure_routes_to_synthetic_entry_qubit_capture_does_not() { - // A closure capturing a runtime qubit identity cannot be lowered to a FIR - // literal, so it must keep the pin-based `ReinvokeOriginal` route. +fn synthetic_path_returned_for_each_with_generic_global_capture_generates_qir() { + let caps = Profile::AdaptiveRI.into(); + let mut interpreter = interpreter_with_capabilities(caps); + eval_fragments( + &mut interpreter, + indoc::indoc! {r#" + import Std.Arrays.ForEach; + + function Make() : Int[][] => Int[] { + let f = Length; + ForEach(arr => f(arr), _) + } + + operation Apply(op : Int[][] => Int[], arrs : Int[][]) : Int[] { + return op(arrs); + } + "#}, + ); + + let op = eval_fragments(&mut interpreter, "Make()"); + let apply = eval_fragments(&mut interpreter, "Apply"); + let args = Value::Tuple( + vec![ + op, + Value::Array( + vec![ + Value::Array(vec![Value::Int(1), Value::Int(2)].into()), + Value::Array(vec![Value::Int(1)].into()), + ] + .into(), + ), + ] + .into(), + None, + ); + + let qir = interpreter + .qirgen_from_callable(&apply, args) + .unwrap_or_else(|errors| { + panic!( + "returned ForEach callable should compile, got: {}", + format_interpret_errors(errors) + ) + }); + assert!( + qir.contains("i64 2") && qir.contains("i64 1"), + "expected QIR to contain the computed lengths [2, 1]:\n{qir}" + ); +} + +#[test] +fn synthetic_path_returned_for_each_direct_length_closure_generates_qir() { + let caps = Profile::AdaptiveRI.into(); + let mut interpreter = interpreter_with_capabilities(caps); + eval_fragments( + &mut interpreter, + indoc::indoc! {r#" + import Std.Arrays.ForEach; + + function Make() : Int[][] => Int[] { + ForEach(arr => Length(arr), _) + } + + operation Apply(op : Int[][] => Int[], arrs : Int[][]) : Int[] { + return op(arrs); + } + "#}, + ); + + let op = eval_fragments(&mut interpreter, "Make()"); + let apply = eval_fragments(&mut interpreter, "Apply"); + let args = Value::Tuple( + vec![ + op, + Value::Array( + vec![ + Value::Array(vec![Value::Int(1), Value::Int(2)].into()), + Value::Array(vec![Value::Int(1)].into()), + ] + .into(), + ), + ] + .into(), + None, + ); + + let qir = interpreter + .qirgen_from_callable(&apply, args) + .unwrap_or_else(|errors| { + panic!( + "returned direct ForEach callable should compile, got: {}", + format_interpret_errors(errors) + ) + }); + assert!( + qir.contains("i64 2") && qir.contains("i64 1"), + "expected QIR to contain the computed lengths [2, 1]:\n{qir}" + ); +} + +#[test] +fn synthetic_path_controlled_hof_forwarded_capturing_closure_generates_qir() { + // A capturing closure (`MakeSelectOp` captures `coeffs`) forwarded through a + // CONTROLLED two-callable HOF (`PrepSelPrep`) and invoked under `Controlled` + // must nest its capture INSIDE the control-level input tuple, producing + // `([control], (systems, ancilla, coeffs))`. Appending the capture as a + // top-level sibling of `[control]` would yield a malformed 3-tuple + // `([control], (systems, ancilla), coeffs)` whose control-level element is no + // longer a 2-tuple, which `split_controls_and_input` in qsc_rca rejects. let source = indoc::indoc! {r#" namespace Test { import Std.Measurement.*; - operation RunOp(op : (Qubit => Unit)) : Result { - use q = Qubit(); - op(q); - return MResetZ(q); + operation SelectWithCoeffs(coeffs : Int[], systems : Qubit[], ancilla : Qubit[]) : Unit is Adj + Ctl { + for _ in coeffs { + X(systems[0]); + } } - operation Entangle(control : Qubit, target : Qubit) : Unit is Adj + Ctl { - CNOT(control, target); + operation MakeSelectOp(coeffs : Int[]) : ((Qubit[], Qubit[]) => Unit is Adj + Ctl) { + return (systems, ancilla) => SelectWithCoeffs(coeffs, systems, ancilla); } - operation MakeEntangler(control : Qubit) : (Qubit => Unit) { - return target => Entangle(control, target); + operation PrepareIdentity(register : Qubit[]) : Unit is Adj + Ctl { + body ... {} + controlled (ctls, ...) {} + } + + operation PrepSelPrep( + prepareOp : (Qubit[] => Unit is Adj + Ctl), + selectOp : ((Qubit[], Qubit[]) => Unit is Adj + Ctl), + systems : Qubit[], + ancilla : Qubit[] + ) : Unit is Adj + Ctl { + body ... { + prepareOp(systems); + selectOp(systems, ancilla); + Adjoint prepareOp(systems); + } + controlled (ctls, ...) { + prepareOp(systems); + Controlled selectOp(ctls, (ancilla, systems)); + Adjoint prepareOp(systems); + } + } + + operation RunCircuit( + prepareOp : (Qubit[] => Unit is Adj + Ctl), + selectOp : ((Qubit[], Qubit[]) => Unit is Adj + Ctl), + power : Int + ) : Result[] { + use systems = Qubit[1]; + use ancilla = Qubit[1]; + use control = Qubit(); + for _ in 1..power { + Controlled PrepSelPrep([control], (prepareOp, selectOp, systems, ancilla)); + } + return MResetEachZ(systems + ancilla); } } "#}; let caps = Profile::AdaptiveRIF.into(); let (store, pkg, items) = compile_and_locate_items( source, - &[("RunOp", true), ("Entangle", true), (".lambda", true)], + &[ + ("RunCircuit", true), + ("PrepareIdentity", true), + ("SelectWithCoeffs", true), + (".lambda", true), + ], caps, ); - // Keep the captured qubit alive for the duration of the closure value. - let captured_qubit = Rc::new(qsc_eval::val::Qubit(0)); - let qubit_closure = Value::Closure(Box::new(qsc_eval::val::Closure { - fixed_args: vec![Value::Qubit((&captured_qubit).into())].into(), + // prepare = Global(PrepareIdentity); select = Closure capturing coeffs = [1]. + let prepare = Value::Global( + fir_id_for(pkg, items["PrepareIdentity"]), + FunctorApp::default(), + ); + let select = Value::Closure(Box::new(qsc_eval::val::Closure { + fixed_args: vec![Value::Array(Rc::new(vec![Value::Int(1)]))].into(), id: fir_id_for(pkg, items[".lambda"]), functor: FunctorApp::default(), })); + let args = Value::Tuple(vec![prepare, select, Value::Int(1)].into(), None); - let target_hir = hir_id_for(pkg, items["RunOp"]); - let (_codegen_fir, backend) = - prepare_codegen_fir_from_callable_args(&store, target_hir, &qubit_closure, caps) - .unwrap_or_else(|errors| { - panic!( - "qubit-capture closure should still produce CodegenFir, got: {}", - format_interpret_errors(errors) - ) - }); + // The captured `coeffs = [1]` (length 1) drives exactly one `X(systems[0])` + // inside `SelectWithCoeffs`; applied under `Controlled` it lowers to a single + // `cx` (controlled-X), proving the capture was genuinely threaded into the + // controlled call rather than dropped or panic-silenced. + let qir = callable_args_to_qir(&store, pkg, items["RunCircuit"], &args, caps); assert!( - matches!(backend, CallableArgsBackend::ReinvokeOriginal { .. }), - "qubit-capture closure should keep the pin-based ReinvokeOriginal route" + qir.contains("__quantum__qis__cx__body"), + "expected controlled X (cx) gate from the forwarded select closure in QIR:\n{qir}" ); } #[test] -fn synthetic_path_array_arg_preserves_element_values() { - // A `Value::Array` argument must survive materialization on the synthetic - // path with its element VALUES intact (not just its length). Each nonzero - // element drives one `op(q)` call, so the gate count proves the concrete - // contents `[1, 0, 1]` were threaded through `build_synthetic_args`. +fn synthetic_path_doubly_controlled_hof_forwarded_capturing_closure_generates_qir() { + // The controlled==1 forwarding case above threads a capture beneath a + // single control layer. This case forwards the same capturing closure + // through the same two-callable HOF (`PrepSelPrep`) but invokes it under + // `Controlled Controlled`, so the capture must nest beneath BOTH control + // layers, producing the strict per-layer 2-tuple shape + // `([c2], ([c1], (systems, ancilla, coeffs)))`. Appending the capture as a + // top-level sibling of the outer control register would break the outermost + // control layer's 2-tuple shape and `split_controls_and_input` in qsc_rca + // would reject it on the first control-layer peel. let source = indoc::indoc! {r#" namespace Test { - operation RunWith(op : Qubit => Unit, data : Int[]) : Result { - use q = Qubit(); - for x in data { - if x != 0 { - op(q); - } + import Std.Measurement.*; + + operation SelectWithCoeffs(coeffs : Int[], systems : Qubit[], ancilla : Qubit[]) : Unit is Adj + Ctl { + for _ in coeffs { + X(systems[0]); } - MResetZ(q) } - operation DoX(q : Qubit) : Unit { X(q); } + + operation MakeSelectOp(coeffs : Int[]) : ((Qubit[], Qubit[]) => Unit is Adj + Ctl) { + return (systems, ancilla) => SelectWithCoeffs(coeffs, systems, ancilla); + } + + operation PrepareIdentity(register : Qubit[]) : Unit is Adj + Ctl { + body ... {} + controlled (ctls, ...) {} + } + + operation PrepSelPrep( + prepareOp : (Qubit[] => Unit is Adj + Ctl), + selectOp : ((Qubit[], Qubit[]) => Unit is Adj + Ctl), + systems : Qubit[], + ancilla : Qubit[] + ) : Unit is Adj + Ctl { + body ... { + prepareOp(systems); + selectOp(systems, ancilla); + Adjoint prepareOp(systems); + } + controlled (ctls, ...) { + prepareOp(systems); + Controlled selectOp(ctls, (ancilla, systems)); + Adjoint prepareOp(systems); + } + } + + operation RunCircuit( + prepareOp : (Qubit[] => Unit is Adj + Ctl), + selectOp : ((Qubit[], Qubit[]) => Unit is Adj + Ctl), + power : Int + ) : Result[] { + use systems = Qubit[1]; + use ancilla = Qubit[1]; + use c1 = Qubit(); + use c2 = Qubit(); + for _ in 1..power { + Controlled Controlled PrepSelPrep([c2], ([c1], (prepareOp, selectOp, systems, ancilla))); + } + return MResetEachZ(systems + ancilla); + } } "#}; let caps = Profile::AdaptiveRIF.into(); - let (store, pkg, items) = - compile_and_locate_items(source, &[("RunWith", true), ("DoX", true)], caps); + let (store, pkg, items) = compile_and_locate_items( + source, + &[ + ("RunCircuit", true), + ("PrepareIdentity", true), + ("SelectWithCoeffs", true), + (".lambda", true), + ], + caps, + ); - let do_x = Value::Global(fir_id_for(pkg, items["DoX"]), FunctorApp::default()); - let args = Value::Tuple( - vec![ - do_x, - Value::Array(vec![Value::Int(1), Value::Int(0), Value::Int(1)].into()), - ] - .into(), - None, + let prepare = Value::Global( + fir_id_for(pkg, items["PrepareIdentity"]), + FunctorApp::default(), ); + let select = Value::Closure(Box::new(qsc_eval::val::Closure { + fixed_args: vec![Value::Array(Rc::new(vec![Value::Int(1)]))].into(), + id: fir_id_for(pkg, items[".lambda"]), + functor: FunctorApp::default(), + })); + let args = Value::Tuple(vec![prepare, select, Value::Int(1)].into(), None); - let qir = callable_args_to_qir(&store, pkg, items["RunWith"], &args, caps); - // Count call sites only (the bare symbol also appears in the `declare` line). - assert_eq!( - qir.matches("call void @__quantum__qis__x__body").count(), - 2, - "expected exactly 2 X gates (one per nonzero element) in QIR:\n{qir}" + // The captured `coeffs = [1]` drives one `X(systems[0])` inside + // `SelectWithCoeffs`; applied under two control layers it lowers to a + // doubly-controlled X (`ccx`), proving the capture was threaded beneath + // both control layers rather than dropped or panic-silenced. + let qir = callable_args_to_qir(&store, pkg, items["RunCircuit"], &args, caps); + assert!( + qir.contains("__quantum__qis__ccx__body"), + "expected doubly-controlled X (ccx) gate from the forwarded select closure in QIR:\n{qir}" ); } #[test] -fn synthetic_path_empty_array_arg_does_not_panic() { - // Regression: an empty `Value::Array` argument previously lowered to - // `Ty::Array(Ty::Err)`, which panicked at `PostAll` in release builds. - // The element-type hint fix lets the empty array carry its real element - // type, so codegen succeeds and emits no `op(q)` calls. +fn synthetic_path_single_callable_doubly_controlled_hof_forwarded_capturing_closure_generates_qir() +{ + // Single-callable variant of the doubly-controlled forwarding case: the + // capture is threaded through a one-callable HOF (`ApplySelect`, whose + // controlled specialization is auto-generated) invoked under + // `Controlled Controlled`. The number of callable parameters does not + // change the control-layer capture placement, so this must nest the + // capture beneath both control layers exactly like the two-callable case. let source = indoc::indoc! {r#" namespace Test { - operation RunWith(op : Qubit => Unit, data : Int[]) : Result { - use q = Qubit(); - for x in data { - if x != 0 { - op(q); - } + import Std.Measurement.*; + + operation SelectWithCoeffs(coeffs : Int[], systems : Qubit[], ancilla : Qubit[]) : Unit is Adj + Ctl { + for _ in coeffs { + X(systems[0]); } - MResetZ(q) } - operation DoX(q : Qubit) : Unit { X(q); } + + operation MakeSelectOp(coeffs : Int[]) : ((Qubit[], Qubit[]) => Unit is Adj + Ctl) { + return (systems, ancilla) => SelectWithCoeffs(coeffs, systems, ancilla); + } + + operation ApplySelect( + selectOp : ((Qubit[], Qubit[]) => Unit is Adj + Ctl), + systems : Qubit[], + ancilla : Qubit[] + ) : Unit is Adj + Ctl { + selectOp(systems, ancilla); + } + + operation RunCircuit( + selectOp : ((Qubit[], Qubit[]) => Unit is Adj + Ctl), + power : Int + ) : Result[] { + use systems = Qubit[1]; + use ancilla = Qubit[1]; + use c1 = Qubit(); + use c2 = Qubit(); + for _ in 1..power { + Controlled Controlled ApplySelect([c2], ([c1], (selectOp, systems, ancilla))); + } + return MResetEachZ(systems + ancilla); + } } "#}; let caps = Profile::AdaptiveRIF.into(); - let (store, pkg, items) = - compile_and_locate_items(source, &[("RunWith", true), ("DoX", true)], caps); + let (store, pkg, items) = compile_and_locate_items( + source, + &[ + ("RunCircuit", true), + ("SelectWithCoeffs", true), + (".lambda", true), + ], + caps, + ); - let do_x = Value::Global(fir_id_for(pkg, items["DoX"]), FunctorApp::default()); - let args = Value::Tuple(vec![do_x, Value::Array(vec![].into())].into(), None); + let select = Value::Closure(Box::new(qsc_eval::val::Closure { + fixed_args: vec![Value::Array(Rc::new(vec![Value::Int(1)]))].into(), + id: fir_id_for(pkg, items[".lambda"]), + functor: FunctorApp::default(), + })); + let args = Value::Tuple(vec![select, Value::Int(1)].into(), None); - let qir = callable_args_to_qir(&store, pkg, items["RunWith"], &args, caps); - // Count call sites only (the bare symbol also appears in the `declare` line). - assert_eq!( - qir.matches("call void @__quantum__qis__x__body").count(), - 0, - "expected no X gates for an empty array argument in QIR:\n{qir}" + let qir = callable_args_to_qir(&store, pkg, items["RunCircuit"], &args, caps); + assert!( + qir.contains("__quantum__qis__ccx__body"), + "expected doubly-controlled X (ccx) gate from the forwarded select closure in QIR:\n{qir}" ); } #[test] -fn synthetic_path_nested_empty_array_arg_does_not_panic() { - // Regression: a nested array `[[]] : Int[][]` whose inner array is empty - // previously poisoned the OUTER element type with `Ty::Err`, panicking at - // `PostAll`. The element-type hint must recurse so the outer array keeps - // its `Int[]` element type even when an inner array is empty. +fn deep_var_local_bound_nested_udt_callable_generates_qir() { + // A HOF (`RunConfig`) whose single parameter is a doubly-nested UDT + // (`Config` wrapping an `OpBox` wrapping a callable) is called with a + // `let`-bound local built from UDT constructors. Defunctionalization must + // deep-strip the callable field from the local's initializer at the call + // site so the local no longer retains an arrow-typed field, otherwise the + // `PostDefunc` invariant that forbids a tuple-bound local from keeping an + // arrow-typed field would fire. let source = indoc::indoc! {r#" namespace Test { - operation RunNested(op : Qubit => Unit, data : Int[][]) : Result { + import Std.Measurement.*; + + newtype OpBox = (Op : Qubit => Unit, Id : Int); + newtype Config = (Inner : OpBox, N : Int); + + operation RunConfig(cfg : Config) : Unit { use q = Qubit(); - for inner in data { - for x in inner { - if x != 0 { - op(q); - } - } - } + cfg::Inner::Op(q); + } + + operation DoX(q : Qubit) : Unit { + X(q); + } + + @EntryPoint() + operation Main() : Result { + use q = Qubit(); + let cfg = Config(OpBox(DoX, 1), 5); + RunConfig(cfg); MResetZ(q) } - operation DoX(q : Qubit) : Unit { X(q); } } "#}; - let caps = Profile::AdaptiveRIF.into(); - let (store, pkg, items) = - compile_and_locate_items(source, &[("RunNested", true), ("DoX", true)], caps); - - let do_x = Value::Global(fir_id_for(pkg, items["DoX"]), FunctorApp::default()); - let args = Value::Tuple( - vec![do_x, Value::Array(vec![Value::Array(vec![].into())].into())].into(), - None, + let qir = compile_source_to_qir(source, Profile::Base.into()); + assert!( + qir.contains("__quantum__qis__x__body"), + "expected X gate from the deep-nested UDT callable in QIR:\n{qir}" ); +} - let qir = callable_args_to_qir(&store, pkg, items["RunNested"], &args, caps); - // Count call sites only (the bare symbol also appears in the `declare` line). - assert_eq!( - qir.matches("call void @__quantum__qis__x__body").count(), - 0, - "expected no X gates for a nested empty array argument in QIR:\n{qir}" +#[test] +fn capture_carrying_deep_var_local_bound_nested_udt_callable_generates_qir() { + // Like the deep-nested UDT case above, but the innermost callable is a + // capturing closure (`qq => RotateBy(angle, qq)` capturing `angle`) bound + // through the same doubly-nested UDT local. After deep-stripping the + // callable field, the captured `angle` must be threaded to the specialized + // callable so the `Rz` receives the captured rotation, rather than dropping + // the capture or building a self-referential argument expression. + let source = indoc::indoc! {r#" + namespace Test { + import Std.Measurement.*; + + newtype OpBox = (Op : Qubit => Unit, Id : Int); + newtype Config = (Inner : OpBox, N : Int); + + operation RunConfig(cfg : Config) : Unit { + use q = Qubit(); + cfg::Inner::Op(q); + } + + operation RotateBy(theta : Double, q : Qubit) : Unit { + Rz(theta, q); + } + + @EntryPoint() + operation Main() : Result { + use q = Qubit(); + let angle = 1.5; + let cfg = Config(OpBox(qq => RotateBy(angle, qq), 1), 5); + RunConfig(cfg); + MResetZ(q) + } + } + "#}; + let capabilities = + TargetCapabilityFlags::Adaptive | TargetCapabilityFlags::FloatingPointComputations; + let qir = compile_source_to_qir(source, capabilities); + assert!( + qir.contains("__quantum__qis__rz__body(double 1.5"), + "expected Rz with the captured angle 1.5 from the deep-nested capturing closure in QIR:\n{qir}" ); } -// ---- SyntheticEntry vs ReinvokeOriginal early-return-in-dynamic-branch parity ---- +#[test] +fn same_target_multi_closure_args_route_to_synthetic_entry_and_generate_qir() { + let source = indoc::indoc! {r#" + namespace Test { + operation InvokeThree( + first : Qubit => Unit, + second : Qubit => Unit, + third : Qubit => Unit + ) : Unit { + use q = Qubit(); + first(q); + second(q); + third(q); + } -/// Target body shared by the early-return parity tests below. Its early `return` sits -/// inside a measurement-dependent (`MResetZ`) branch with statements after the branch, -/// which is exactly the `ReturnWithinDynamicScope` shape that the entry-reachable -/// `return_unify` pass rewrites. -const EARLY_RETURN_TARGET_BODY: &str = r#" - operation RunOp(op : (Qubit => Unit)) : Int { - let r = { - use q = Qubit(); - op(q); - MResetZ(q) - }; - if r == One { - return 1; + operation ApplyRz(theta : Double, q : Qubit) : Unit { + Rz(theta, q); + } } - return 2; - } -"#; + "#}; + let caps = Profile::Base.into(); + let (store, pkg, items) = + compile_and_locate_items(source, &[("InvokeThree", true), ("ApplyRz", true)], caps); -/// Source whose closure arg captures a classical value, so the arg is FIR-lowerable -/// and routes through the self-contained `SyntheticEntry` backend. -fn early_return_synthetic_entry_source() -> String { - format!( - r#" -namespace Test {{ - import Std.Measurement.*; -{EARLY_RETURN_TARGET_BODY} - operation Rotate(reps : Int, target : Qubit) : Unit {{ - for _ in 1..reps {{ - X(target); - }} - }} + let apply_rz = fir_id_for(pkg, items["ApplyRz"]); + let first = Value::Closure(Box::new(qsc_eval::val::Closure { + fixed_args: vec![Value::Double(1.0)].into(), + id: apply_rz, + functor: FunctorApp::default(), + })); + let second = Value::Closure(Box::new(qsc_eval::val::Closure { + fixed_args: vec![Value::Double(2.0)].into(), + id: apply_rz, + functor: FunctorApp::default(), + })); + let third = Value::Closure(Box::new(qsc_eval::val::Closure { + fixed_args: vec![Value::Double(3.0)].into(), + id: apply_rz, + functor: FunctorApp::default(), + })); + let args = Value::Tuple(vec![first, second, third].into(), None); - operation MakeRotation(reps : Int) : (Qubit => Unit) {{ - return target => Rotate(reps, target); - }} -}} -"# + let target_hir = hir_id_for(pkg, items["InvokeThree"]); + let (_codegen_fir, backend) = prepare_codegen_fir_from_callable_args( + &store, target_hir, &args, caps, ) -} - -/// Source whose closure arg captures an allocated qubit (a runtime identity that is -/// NOT FIR-lowerable), forcing the pin-based `ReinvokeOriginal` backend. -fn early_return_reinvoke_original_source() -> String { - format!( - r#" -namespace Test {{ - import Std.Measurement.*; -{EARLY_RETURN_TARGET_BODY} - operation Entangle(control : Qubit, target : Qubit) : Unit is Adj + Ctl {{ - CNOT(control, target); - }} + .unwrap_or_else(|errors| { + panic!( + "same-target classical closures should produce CodegenFir, got: {}", + format_interpret_errors(errors) + ) + }); + assert!( + matches!(backend, CallableArgsBackend::SyntheticEntry), + "same-target classical closures should route to the synthetic entry" + ); - operation MakeEntangler(control : Qubit) : (Qubit => Unit) {{ - return target => Entangle(control, target); - }} -}} -"# - ) + let qir = callable_args_to_qir(&store, pkg, items["InvokeThree"], &args, caps); + let expected_calls = [ + "call void @__quantum__qis__rz__body(double 1.0,", + "call void @__quantum__qis__rz__body(double 2.0,", + "call void @__quantum__qis__rz__body(double 3.0,", + ]; + let counts: Vec<_> = expected_calls + .iter() + .map(|call| qir.matches(call).count()) + .collect(); + assert_eq!( + counts, + vec![1, 1, 1], + "expected each captured rotation exactly once in QIR:\n{qir}" + ); + let positions: Vec<_> = expected_calls + .iter() + .map(|call| qir.find(call).expect("expected Rz call in QIR")) + .collect(); + let mut sorted_positions = positions.clone(); + sorted_positions.sort_unstable(); + assert_eq!( + positions, sorted_positions, + "expected captured rotations to be emitted in argument order:\n{qir}" + ); } -/// Parity regression for the `SyntheticEntry`-vs-`ReinvokeOriginal` capability behavior -/// on early-return-in-dynamic-branch closures. -/// -/// Both variants pass the SAME target operation (`RunOp`), whose body early-returns -/// inside a measurement-dependent branch. They differ ONLY in the closure capture: -/// -/// * Classical capture -> FIR-lowerable -> `SyntheticEntry`. The target body becomes -/// entry-reachable through the synthetic `Call`, so `return_unify` rewrites the early -/// return and the program compiles to QIR under an Adaptive profile. -/// * Qubit capture -> NOT FIR-lowerable -> `ReinvokeOriginal`. The target body is pinned -/// (not entry-reachable), so the main pipeline never return-unifies it. The body-only -/// signature-preserving sub-pipeline runs on the pinned body, so the early return is -/// rewritten into flag-guarded forward control flow and the program compiles to QIR -/// under an Adaptive profile with parity to the `SyntheticEntry` variant. -/// -/// This test asserts both routes compile. #[test] -fn early_return_in_dynamic_branch_synthetic_and_reinvoke_both_compile_parity() { - let caps = Profile::AdaptiveRIF.into(); +fn nested_closure_arg_routes_to_synthetic_entry_and_generates_inner_effect() { + let source = indoc::indoc! {r#" + namespace Test { + operation InvokeOne(op : Qubit => Unit) : Unit { + use q = Qubit(); + op(q); + } - // --- SyntheticEntry variant: classical capture compiles to QIR. --- - let synthetic_source = early_return_synthetic_entry_source(); + operation ApplyInner(inner : Qubit => Unit, q : Qubit) : Unit { + inner(q); + } + + function MakeRz(theta : Double) : Qubit => Unit { + Rz(theta, _) + } + } + "#}; + let caps = Profile::Base.into(); let (store, pkg, items) = compile_and_locate_items( - &synthetic_source, - &[("RunOp", true), ("Rotate", true), (".lambda", true)], + source, + &[("InvokeOne", true), ("ApplyInner", true), (".lambda", true)], caps, ); - // The lifted lambda's input is `(reps, target)`; capturing `reps = 1` leaves the - // explicit `target` slot for the closure invocation to supply. - let rotation_closure = Value::Closure(Box::new(qsc_eval::val::Closure { - fixed_args: vec![Value::Int(1)].into(), + let inner = Value::Closure(Box::new(qsc_eval::val::Closure { + fixed_args: vec![Value::Double(4.0)].into(), id: fir_id_for(pkg, items[".lambda"]), functor: FunctorApp::default(), })); + let outer = Value::Closure(Box::new(qsc_eval::val::Closure { + fixed_args: vec![inner].into(), + id: fir_id_for(pkg, items["ApplyInner"]), + functor: FunctorApp::default(), + })); - let target_hir = hir_id_for(pkg, items["RunOp"]); - let (_codegen_fir, backend) = - prepare_codegen_fir_from_callable_args(&store, target_hir, &rotation_closure, caps) - .unwrap_or_else(|errors| { - panic!( - "classical-capture early-return closure should produce CodegenFir, got: {}", - format_interpret_errors(errors) - ) - }); + let target_hir = hir_id_for(pkg, items["InvokeOne"]); + let (_codegen_fir, backend) = prepare_codegen_fir_from_callable_args( + &store, target_hir, &outer, caps, + ) + .unwrap_or_else(|errors| { + panic!( + "nested classical closure should produce CodegenFir, got: {}", + format_interpret_errors(errors) + ) + }); assert!( matches!(backend, CallableArgsBackend::SyntheticEntry), - "classical-capture closure should route to the synthetic entry" + "nested classical closure should route to the synthetic entry" ); - let qir = callable_args_to_qir(&store, pkg, items["RunOp"], &rotation_closure, caps); - assert!( - qir.contains("__quantum__qis__x__body"), - "expected X gate from the SyntheticEntry early-return body in QIR:\n{qir}" + + let qir = callable_args_to_qir(&store, pkg, items["InvokeOne"], &outer, caps); + assert_eq!( + qir.matches("call void @__quantum__qis__rz__body(double 4.0,") + .count(), + 1, + "expected one inner captured rotation in QIR:\n{qir}" ); +} - // --- ReinvokeOriginal variant: qubit capture compiles to QIR. --- - let reinvoke_source = early_return_reinvoke_original_source(); +#[test] +fn classical_capture_closure_routes_to_synthetic_entry_qubit_capture_does_not() { + // A closure capturing a runtime qubit identity cannot be lowered to a FIR + // literal, so it must keep the pin-based `ReinvokeOriginal` route. + let source = indoc::indoc! {r#" + namespace Test { + import Std.Measurement.*; + + operation RunOp(op : (Qubit => Unit)) : Result { + use q = Qubit(); + op(q); + return MResetZ(q); + } + + operation Entangle(control : Qubit, target : Qubit) : Unit is Adj + Ctl { + CNOT(control, target); + } + + operation MakeEntangler(control : Qubit) : (Qubit => Unit) { + return target => Entangle(control, target); + } + } + "#}; + let caps = Profile::AdaptiveRIF.into(); let (store, pkg, items) = compile_and_locate_items( - &reinvoke_source, + source, &[("RunOp", true), ("Entangle", true), (".lambda", true)], caps, ); @@ -2876,8 +3309,7 @@ fn early_return_in_dynamic_branch_synthetic_and_reinvoke_both_compile_parity() { prepare_codegen_fir_from_callable_args(&store, target_hir, &qubit_closure, caps) .unwrap_or_else(|errors| { panic!( - "qubit-capture early-return closure should compile \ - (ReinvokeOriginal sub-pipeline return-unifies the pinned body), got: {}", + "qubit-capture closure should still produce CodegenFir, got: {}", format_interpret_errors(errors) ) }); @@ -2885,37 +3317,259 @@ fn early_return_in_dynamic_branch_synthetic_and_reinvoke_both_compile_parity() { matches!(backend, CallableArgsBackend::ReinvokeOriginal { .. }), "qubit-capture closure should keep the pin-based ReinvokeOriginal route" ); - let qir = callable_args_to_qir(&store, pkg, items["RunOp"], &qubit_closure, caps); - assert!( - qir.contains("__quantum__qis__cnot__body") || qir.contains("__quantum__qis__cx__body"), - "expected the entangler CNOT from the ReinvokeOriginal early-return body in QIR:\n{qir}" - ); } -/// Parity check: the `ReinvokeOriginal` early-return-in-dynamic-branch body compiles to -/// QIR with parity to the `SyntheticEntry` variant now that the body-only -/// signature-preserving sub-pipeline runs `return_unify` on the pinned target body. -/// -/// This is the focused twin of -/// `early_return_in_dynamic_branch_synthetic_and_reinvoke_both_compile_parity`: it isolates -/// the `ReinvokeOriginal` route and asserts the entangler CNOT appears in the QIR. #[test] -fn early_return_in_dynamic_branch_reinvoke_original_compiles_parity_target() { - let caps = Profile::AdaptiveRIF.into(); - - let reinvoke_source = early_return_reinvoke_original_source(); - let (store, pkg, items) = compile_and_locate_items( - &reinvoke_source, - &[("RunOp", true), ("Entangle", true), (".lambda", true)], - caps, - ); - - // Keep the captured qubit alive for the duration of the closure value. - let captured_qubit = Rc::new(qsc_eval::val::Qubit(0)); - let qubit_closure = Value::Closure(Box::new(qsc_eval::val::Closure { - fixed_args: vec![Value::Qubit((&captured_qubit).into())].into(), - id: fir_id_for(pkg, items[".lambda"]), - functor: FunctorApp::default(), +fn synthetic_path_array_arg_preserves_element_values() { + // A `Value::Array` argument must survive materialization on the synthetic + // path with its element VALUES intact (not just its length). Each nonzero + // element drives one `op(q)` call, so the gate count proves the concrete + // contents `[1, 0, 1]` were threaded through `build_synthetic_args`. + let source = indoc::indoc! {r#" + namespace Test { + operation RunWith(op : Qubit => Unit, data : Int[]) : Result { + use q = Qubit(); + for x in data { + if x != 0 { + op(q); + } + } + MResetZ(q) + } + operation DoX(q : Qubit) : Unit { X(q); } + } + "#}; + let caps = Profile::AdaptiveRIF.into(); + let (store, pkg, items) = + compile_and_locate_items(source, &[("RunWith", true), ("DoX", true)], caps); + + let do_x = Value::Global(fir_id_for(pkg, items["DoX"]), FunctorApp::default()); + let args = Value::Tuple( + vec![ + do_x, + Value::Array(vec![Value::Int(1), Value::Int(0), Value::Int(1)].into()), + ] + .into(), + None, + ); + + let qir = callable_args_to_qir(&store, pkg, items["RunWith"], &args, caps); + // Count call sites only (the bare symbol also appears in the `declare` line). + assert_eq!( + qir.matches("call void @__quantum__qis__x__body").count(), + 2, + "expected exactly 2 X gates (one per nonzero element) in QIR:\n{qir}" + ); +} + +#[test] +fn synthetic_path_empty_array_arg_does_not_panic() { + // Regression: an empty `Value::Array` argument previously lowered to + // `Ty::Array(Ty::Err)`, which panicked at `PostAll` in release builds. + // The element-type hint fix lets the empty array carry its real element + // type, so codegen succeeds and emits no `op(q)` calls. + let source = indoc::indoc! {r#" + namespace Test { + operation RunWith(op : Qubit => Unit, data : Int[]) : Result { + use q = Qubit(); + for x in data { + if x != 0 { + op(q); + } + } + MResetZ(q) + } + operation DoX(q : Qubit) : Unit { X(q); } + } + "#}; + let caps = Profile::AdaptiveRIF.into(); + let (store, pkg, items) = + compile_and_locate_items(source, &[("RunWith", true), ("DoX", true)], caps); + + let do_x = Value::Global(fir_id_for(pkg, items["DoX"]), FunctorApp::default()); + let args = Value::Tuple(vec![do_x, Value::Array(vec![].into())].into(), None); + + let qir = callable_args_to_qir(&store, pkg, items["RunWith"], &args, caps); + // Count call sites only (the bare symbol also appears in the `declare` line). + assert_eq!( + qir.matches("call void @__quantum__qis__x__body").count(), + 0, + "expected no X gates for an empty array argument in QIR:\n{qir}" + ); +} + +#[test] +fn synthetic_path_nested_empty_array_arg_does_not_panic() { + // Regression: a nested array `[[]] : Int[][]` whose inner array is empty + // previously poisoned the OUTER element type with `Ty::Err`, panicking at + // `PostAll`. The element-type hint must recurse so the outer array keeps + // its `Int[]` element type even when an inner array is empty. + let source = indoc::indoc! {r#" + namespace Test { + operation RunNested(op : Qubit => Unit, data : Int[][]) : Result { + use q = Qubit(); + for inner in data { + for x in inner { + if x != 0 { + op(q); + } + } + } + MResetZ(q) + } + operation DoX(q : Qubit) : Unit { X(q); } + } + "#}; + let caps = Profile::AdaptiveRIF.into(); + let (store, pkg, items) = + compile_and_locate_items(source, &[("RunNested", true), ("DoX", true)], caps); + + let do_x = Value::Global(fir_id_for(pkg, items["DoX"]), FunctorApp::default()); + let args = Value::Tuple( + vec![do_x, Value::Array(vec![Value::Array(vec![].into())].into())].into(), + None, + ); + + let qir = callable_args_to_qir(&store, pkg, items["RunNested"], &args, caps); + // Count call sites only (the bare symbol also appears in the `declare` line). + assert_eq!( + qir.matches("call void @__quantum__qis__x__body").count(), + 0, + "expected no X gates for a nested empty array argument in QIR:\n{qir}" + ); +} + +// ---- SyntheticEntry vs ReinvokeOriginal early-return-in-dynamic-branch parity ---- + +/// Target body shared by the early-return parity tests below. Its early `return` sits +/// inside a measurement-dependent (`MResetZ`) branch with statements after the branch, +/// which is exactly the `ReturnWithinDynamicScope` shape that the entry-reachable +/// `return_unify` pass rewrites. +const EARLY_RETURN_TARGET_BODY: &str = r#" + operation RunOp(op : (Qubit => Unit)) : Int { + let r = { + use q = Qubit(); + op(q); + MResetZ(q) + }; + if r == One { + return 1; + } + return 2; + } +"#; + +/// Source whose closure arg captures a classical value, so the arg is FIR-lowerable +/// and routes through the self-contained `SyntheticEntry` backend. +fn early_return_synthetic_entry_source() -> String { + format!( + r#" +namespace Test {{ + import Std.Measurement.*; +{EARLY_RETURN_TARGET_BODY} + operation Rotate(reps : Int, target : Qubit) : Unit {{ + for _ in 1..reps {{ + X(target); + }} + }} + + operation MakeRotation(reps : Int) : (Qubit => Unit) {{ + return target => Rotate(reps, target); + }} +}} +"# + ) +} + +/// Source whose closure arg captures an allocated qubit (a runtime identity that is +/// NOT FIR-lowerable), forcing the pin-based `ReinvokeOriginal` backend. +fn early_return_reinvoke_original_source() -> String { + format!( + r#" +namespace Test {{ + import Std.Measurement.*; +{EARLY_RETURN_TARGET_BODY} + operation Entangle(control : Qubit, target : Qubit) : Unit is Adj + Ctl {{ + CNOT(control, target); + }} + + operation MakeEntangler(control : Qubit) : (Qubit => Unit) {{ + return target => Entangle(control, target); + }} +}} +"# + ) +} + +/// Parity regression for the `SyntheticEntry`-vs-`ReinvokeOriginal` capability behavior +/// on early-return-in-dynamic-branch closures. +/// +/// Both variants pass the SAME target operation (`RunOp`), whose body early-returns +/// inside a measurement-dependent branch. They differ ONLY in the closure capture: +/// +/// * Classical capture -> FIR-lowerable -> `SyntheticEntry`. The target body becomes +/// entry-reachable through the synthetic `Call`, so `return_unify` rewrites the early +/// return and the program compiles to QIR under an Adaptive profile. +/// * Qubit capture -> NOT FIR-lowerable -> `ReinvokeOriginal`. The target body is pinned +/// (not entry-reachable), so the main pipeline never return-unifies it. The body-only +/// signature-preserving sub-pipeline runs on the pinned body, so the early return is +/// rewritten into flag-guarded forward control flow and the program compiles to QIR +/// under an Adaptive profile with parity to the `SyntheticEntry` variant. +/// +/// This test asserts both routes compile. +#[test] +fn early_return_in_dynamic_branch_synthetic_and_reinvoke_both_compile_parity() { + let caps = Profile::AdaptiveRIF.into(); + + // --- SyntheticEntry variant: classical capture compiles to QIR. --- + let synthetic_source = early_return_synthetic_entry_source(); + let (store, pkg, items) = compile_and_locate_items( + &synthetic_source, + &[("RunOp", true), ("Rotate", true), (".lambda", true)], + caps, + ); + + // The lifted lambda's input is `(reps, target)`; capturing `reps = 1` leaves the + // explicit `target` slot for the closure invocation to supply. + let rotation_closure = Value::Closure(Box::new(qsc_eval::val::Closure { + fixed_args: vec![Value::Int(1)].into(), + id: fir_id_for(pkg, items[".lambda"]), + functor: FunctorApp::default(), + })); + + let target_hir = hir_id_for(pkg, items["RunOp"]); + let (_codegen_fir, backend) = + prepare_codegen_fir_from_callable_args(&store, target_hir, &rotation_closure, caps) + .unwrap_or_else(|errors| { + panic!( + "classical-capture early-return closure should produce CodegenFir, got: {}", + format_interpret_errors(errors) + ) + }); + assert!( + matches!(backend, CallableArgsBackend::SyntheticEntry), + "classical-capture closure should route to the synthetic entry" + ); + let qir = callable_args_to_qir(&store, pkg, items["RunOp"], &rotation_closure, caps); + assert!( + qir.contains("__quantum__qis__x__body"), + "expected X gate from the SyntheticEntry early-return body in QIR:\n{qir}" + ); + + // --- ReinvokeOriginal variant: qubit capture compiles to QIR. --- + let reinvoke_source = early_return_reinvoke_original_source(); + let (store, pkg, items) = compile_and_locate_items( + &reinvoke_source, + &[("RunOp", true), ("Entangle", true), (".lambda", true)], + caps, + ); + + // Keep the captured qubit alive for the duration of the closure value. + let captured_qubit = Rc::new(qsc_eval::val::Qubit(0)); + let qubit_closure = Value::Closure(Box::new(qsc_eval::val::Closure { + fixed_args: vec![Value::Qubit((&captured_qubit).into())].into(), + id: fir_id_for(pkg, items[".lambda"]), + functor: FunctorApp::default(), })); let target_hir = hir_id_for(pkg, items["RunOp"]); @@ -2923,7 +3577,8 @@ fn early_return_in_dynamic_branch_reinvoke_original_compiles_parity_target() { prepare_codegen_fir_from_callable_args(&store, target_hir, &qubit_closure, caps) .unwrap_or_else(|errors| { panic!( - "qubit-capture early-return closure should compile, got: {}", + "qubit-capture early-return closure should compile \ + (ReinvokeOriginal sub-pipeline return-unifies the pinned body), got: {}", format_interpret_errors(errors) ) }); @@ -2938,9 +3593,55 @@ fn early_return_in_dynamic_branch_reinvoke_original_compiles_parity_target() { ); } -#[test] -fn synthetic_path_udt_wrapped_controlled_callable_preserves_functor() { - let source = indoc::indoc! {r#" +/// Parity check: the `ReinvokeOriginal` early-return-in-dynamic-branch body compiles to +/// QIR with parity to the `SyntheticEntry` variant now that the body-only +/// signature-preserving sub-pipeline runs `return_unify` on the pinned target body. +/// +/// This is the focused twin of +/// `early_return_in_dynamic_branch_synthetic_and_reinvoke_both_compile_parity`: it isolates +/// the `ReinvokeOriginal` route and asserts the entangler CNOT appears in the QIR. +#[test] +fn early_return_in_dynamic_branch_reinvoke_original_compiles_parity_target() { + let caps = Profile::AdaptiveRIF.into(); + + let reinvoke_source = early_return_reinvoke_original_source(); + let (store, pkg, items) = compile_and_locate_items( + &reinvoke_source, + &[("RunOp", true), ("Entangle", true), (".lambda", true)], + caps, + ); + + // Keep the captured qubit alive for the duration of the closure value. + let captured_qubit = Rc::new(qsc_eval::val::Qubit(0)); + let qubit_closure = Value::Closure(Box::new(qsc_eval::val::Closure { + fixed_args: vec![Value::Qubit((&captured_qubit).into())].into(), + id: fir_id_for(pkg, items[".lambda"]), + functor: FunctorApp::default(), + })); + + let target_hir = hir_id_for(pkg, items["RunOp"]); + let (_codegen_fir, backend) = + prepare_codegen_fir_from_callable_args(&store, target_hir, &qubit_closure, caps) + .unwrap_or_else(|errors| { + panic!( + "qubit-capture early-return closure should compile, got: {}", + format_interpret_errors(errors) + ) + }); + assert!( + matches!(backend, CallableArgsBackend::ReinvokeOriginal { .. }), + "qubit-capture closure should keep the pin-based ReinvokeOriginal route" + ); + let qir = callable_args_to_qir(&store, pkg, items["RunOp"], &qubit_closure, caps); + assert!( + qir.contains("__quantum__qis__cnot__body") || qir.contains("__quantum__qis__cx__body"), + "expected the entangler CNOT from the ReinvokeOriginal early-return body in QIR:\n{qir}" + ); +} + +#[test] +fn synthetic_path_udt_wrapped_controlled_callable_preserves_functor() { + let source = indoc::indoc! {r#" namespace Test { newtype CtlBox = (Op: ((Qubit[], Qubit) => Unit), Tag: Int); operation Invoke(b : CtlBox) : Result { @@ -2949,35 +3650,307 @@ fn synthetic_path_udt_wrapped_controlled_callable_preserves_functor() { Reset(control); MResetZ(target) } - operation DoX(q : Qubit) : Unit is Ctl { X(q); } + operation DoX(q : Qubit) : Unit is Ctl { X(q); } + } + "#}; + let caps = Profile::AdaptiveRIF.into(); + let (store, pkg, items) = compile_and_locate_items( + source, + &[("Invoke", true), ("DoX", true), ("CtlBox", false)], + caps, + ); + + let controlled_do_x = Value::Global( + fir_id_for(pkg, items["DoX"]), + FunctorApp { + adjoint: false, + controlled: 1, + }, + ); + let boxed = Value::Tuple( + vec![controlled_do_x, Value::Int(0)].into(), + Some(Rc::new(fir_id_for(pkg, items["CtlBox"]))), + ); + + let target_hir = hir_id_for(pkg, items["Invoke"]); + let (codegen_fir, _backend) = prepare_codegen_fir_from_callable_args( + &store, target_hir, &boxed, caps, + ) + .unwrap_or_else(|errors| { + panic!( + "controlled UDT-wrapped callable should produce CodegenFir, got: {}", + format_interpret_errors(errors) + ) + }); + let entry = crate::codegen::qir::entry_from_codegen_fir(&codegen_fir); + let qir = qsc_codegen::qir::fir_to_qir( + &codegen_fir.fir_store, + caps, + &codegen_fir.compute_properties, + &entry, + ) + .unwrap_or_else(|e| panic!("synthetic entry QIR generation should succeed: {e:?}")); + assert!( + qir.contains("__quantum__qis__cx__body"), + "expected controlled X gate in QIR:\n{qir}" + ); +} + +// ---- Synthetic path: struct wrapping a callable field ---- + +#[test] +fn synthetic_path_single_field_struct_wrapping_callable_generates_qir() { + // Single-field UDT constructors are transparent in Value form: OpBox(DoH) + // is represented as the bare Global callable value. + let source = indoc::indoc! {r#" + namespace Test { + newtype OpBox = (Op: Qubit => Unit); + operation RunBoxed(b: OpBox) : Result { + use q = Qubit(); + b::Op(q); + MResetZ(q) + } + operation DoH(q: Qubit) : Unit { H(q); } + } + "#}; + let caps = Profile::AdaptiveRIF.into(); + let (store, pkg, items) = + compile_and_locate_items(source, &[("RunBoxed", true), ("DoH", true)], caps); + + let do_h = Value::Global(fir_id_for(pkg, items["DoH"]), FunctorApp::default()); + + let qir = callable_args_to_qir(&store, pkg, items["RunBoxed"], &do_h, caps); + assert!( + qir.contains("__quantum__qis__h__body"), + "expected H gate in QIR:\n{qir}" + ); +} + +#[test] +fn synthetic_path_struct_wrapping_callable_and_tag_generates_qir() { + // A newtype that wraps a callable and a non-callable field. + // This keeps tuple structure in the runtime Value while still exercising + // UDT pure-type discovery. + let source = indoc::indoc! {r#" + namespace Test { + newtype OpBox = (Op: Qubit => Unit, Tag: Int); + operation RunBoxed(b: OpBox) : Result { + use q = Qubit(); + b::Op(q); + MResetZ(q) + } + operation DoH(q: Qubit) : Unit { H(q); } + } + "#}; + let caps = Profile::AdaptiveRIF.into(); + let (store, pkg, items) = compile_and_locate_items( + source, + &[("RunBoxed", true), ("DoH", true), ("OpBox", false)], + caps, + ); + + let do_h = Value::Global(fir_id_for(pkg, items["DoH"]), FunctorApp::default()); + let boxed = Value::Tuple( + vec![do_h, Value::Int(0)].into(), + Some(Rc::new(fir_id_for(pkg, items["OpBox"]))), + ); + + let qir = callable_args_to_qir(&store, pkg, items["RunBoxed"], &boxed, caps); + assert!( + qir.contains("__quantum__qis__h__body"), + "expected H gate in QIR:\n{qir}" + ); +} + +#[test] +fn synthetic_path_udt_wrapped_adjoint_callable_preserves_functor() { + let source = indoc::indoc! {r#" + namespace Test { + newtype OpBox = (Op: Qubit => Unit is Adj, Tag: Int); + operation RunBoxed(b: OpBox) : Result { + use q = Qubit(); + b::Op(q); + MResetZ(q) + } + operation DoS(q: Qubit) : Unit is Adj { S(q); } + } + "#}; + let caps = Profile::AdaptiveRIF.into(); + let (store, pkg, items) = compile_and_locate_items( + source, + &[("RunBoxed", true), ("DoS", true), ("OpBox", false)], + caps, + ); + + let adjoint_do_s = Value::Global( + fir_id_for(pkg, items["DoS"]), + FunctorApp { + adjoint: true, + controlled: 0, + }, + ); + let boxed = Value::Tuple( + vec![adjoint_do_s, Value::Int(0)].into(), + Some(Rc::new(fir_id_for(pkg, items["OpBox"]))), + ); + + let target_hir = hir_id_for(pkg, items["RunBoxed"]); + let (codegen_fir, _backend) = prepare_codegen_fir_from_callable_args( + &store, target_hir, &boxed, caps, + ) + .unwrap_or_else(|errors| { + panic!( + "adjoint UDT-wrapped callable should produce CodegenFir, got: {}", + format_interpret_errors(errors) + ) + }); + let entry = crate::codegen::qir::entry_from_codegen_fir(&codegen_fir); + let qir = qsc_codegen::qir::fir_to_qir( + &codegen_fir.fir_store, + caps, + &codegen_fir.compute_properties, + &entry, + ) + .unwrap_or_else(|e| panic!("synthetic entry QIR generation should succeed: {e:?}")); + assert!( + qir.contains("__quantum__qis__s__adj"), + "expected adjoint S gate in QIR:\n{qir}" + ); +} + +// ---- Synthetic path: callable arg with additional non-callable tuple values ---- + +#[test] +fn synthetic_path_callable_with_double_and_string_generates_qir() { + // Target takes (factor: Double, op: Qubit => Unit, label: String). + // All three value types exercise different branches in `lower_value_to_expr`. + let source = indoc::indoc! {r#" + namespace Test { + operation Tagged(factor : Double, op : Qubit => Unit, label : String) : Result { + use q = Qubit(); + op(q); + MResetZ(q) + } + operation DoH(q : Qubit) : Unit { H(q); } + } + "#}; + let caps = Profile::AdaptiveRIF.into(); + let (store, pkg, items) = + compile_and_locate_items(source, &[("Tagged", true), ("DoH", true)], caps); + + let do_h = Value::Global(fir_id_for(pkg, items["DoH"]), FunctorApp::default()); + let args = Value::Tuple( + vec![Value::Double(1.5), do_h, Value::String("test".into())].into(), + None, + ); + + let qir = callable_args_to_qir(&store, pkg, items["Tagged"], &args, caps); + assert!( + qir.contains("__quantum__qis__h__body"), + "expected H gate in QIR:\n{qir}" + ); +} + +// ---- Synthetic path: nested struct (UDT inside UDT) with callable ---- + +#[test] +fn synthetic_path_nested_struct_with_callable_generates_qir() { + // Two levels of UDT wrapping: Config(Inner: OpBox, N: Int) where + // OpBox(Op: Qubit => Unit, Id: Int). This exercises UDT pure-type descent + // and nested field-chain replacement in defunctionalization. + // Inner UDTs need 2+ fields to avoid the single-field-UDT unwrap issue + // where the Value::Tuple shape misaligns with the erased type. + let source = indoc::indoc! {r#" + namespace Test { + newtype OpBox = (Op: Qubit => Unit, Id: Int); + newtype Config = (Inner: OpBox, N: Int); + operation RunConfig(cfg: Config) : Result { + use q = Qubit(); + cfg::Inner::Op(q); + MResetZ(q) + } + operation DoX(q: Qubit) : Unit { X(q); } + } + "#}; + let caps = Profile::AdaptiveRIF.into(); + let (store, pkg, items) = compile_and_locate_items( + source, + &[ + ("RunConfig", true), + ("DoX", true), + ("Config", false), + ("OpBox", false), + ], + caps, + ); + + let do_x = Value::Global(fir_id_for(pkg, items["DoX"]), FunctorApp::default()); + let inner = Value::Tuple( + vec![do_x, Value::Int(1)].into(), + Some(Rc::new(fir_id_for(pkg, items["OpBox"]))), + ); + let config = Value::Tuple( + vec![inner, Value::Int(5)].into(), + Some(Rc::new(fir_id_for(pkg, items["Config"]))), + ); + + let qir = callable_args_to_qir(&store, pkg, items["RunConfig"], &config, caps); + assert!( + qir.contains("__quantum__qis__x__body"), + "expected X gate in QIR:\n{qir}" + ); +} + +#[test] +fn synthetic_path_callable_field_taking_udt_with_callable_generates_qir() { + // Outer wraps a callable whose input is Inner, and Inner itself wraps a + // callable. This exercises UDT expansion through arrow input types, not + // just nested UDT fields that directly contain callable values. + let source = indoc::indoc! {r#" + namespace Test { + newtype Inner = (NestedOp: Qubit => Unit, Id: Int); + newtype Outer = (ApplyInner: Inner => Result, Id: Int); + + operation Invoke(outer: Outer) : Result { + let inner = Inner(DoH, 2); + outer::ApplyInner(inner) + } + + operation UseInner(inner: Inner) : Result { + use q = Qubit(); + inner::NestedOp(q); + MResetZ(q) + } + + operation DoH(q: Qubit) : Unit { H(q); } } "#}; let caps = Profile::AdaptiveRIF.into(); let (store, pkg, items) = compile_and_locate_items( source, - &[("Invoke", true), ("DoX", true), ("CtlBox", false)], + &[ + ("Invoke", true), + ("UseInner", true), + ("DoH", true), + ("Inner", false), + ("Outer", false), + ], caps, ); - let controlled_do_x = Value::Global( - fir_id_for(pkg, items["DoX"]), - FunctorApp { - adjoint: false, - controlled: 1, - }, - ); - let boxed = Value::Tuple( - vec![controlled_do_x, Value::Int(0)].into(), - Some(Rc::new(fir_id_for(pkg, items["CtlBox"]))), + let use_inner = Value::Global(fir_id_for(pkg, items["UseInner"]), FunctorApp::default()); + let outer = Value::Tuple( + vec![use_inner, Value::Int(1)].into(), + Some(Rc::new(fir_id_for(pkg, items["Outer"]))), ); let target_hir = hir_id_for(pkg, items["Invoke"]); let (codegen_fir, _backend) = prepare_codegen_fir_from_callable_args( - &store, target_hir, &boxed, caps, + &store, target_hir, &outer, caps, ) .unwrap_or_else(|errors| { panic!( - "controlled UDT-wrapped callable should produce CodegenFir, got: {}", + "callable field taking a UDT with a callable should produce CodegenFir, got: {}", format_interpret_errors(errors) ) }); @@ -2990,35 +3963,197 @@ fn synthetic_path_udt_wrapped_controlled_callable_preserves_functor() { ) .unwrap_or_else(|e| panic!("synthetic entry QIR generation should succeed: {e:?}")); assert!( - qir.contains("__quantum__qis__cx__body"), - "expected controlled X gate in QIR:\n{qir}" + qir.contains("__quantum__qis__h__body"), + "expected H gate in QIR:\n{qir}" ); } -// ---- Synthetic path: struct wrapping a callable field ---- +// ---- Synthetic path: tuple arg where only one element is callable ---- #[test] -fn synthetic_path_single_field_struct_wrapping_callable_generates_qir() { - // Single-field UDT constructors are transparent in Value form: OpBox(DoH) - // is represented as the bare Global callable value. +fn synthetic_path_tuple_with_one_callable_among_many_scalars() { + // (Int, Int, Qubit => Unit, Bool, Int) — callable buried deep in a wide tuple. let source = indoc::indoc! {r#" namespace Test { - newtype OpBox = (Op: Qubit => Unit); - operation RunBoxed(b: OpBox) : Result { + operation Wide(a : Int, b : Int, op : Qubit => Unit, flag : Bool, c : Int) : Result { use q = Qubit(); - b::Op(q); + if flag { op(q); } + MResetZ(q) + } + operation DoH(q : Qubit) : Unit { H(q); } + } + "#}; + let caps = Profile::AdaptiveRIF.into(); + let (store, pkg, items) = + compile_and_locate_items(source, &[("Wide", true), ("DoH", true)], caps); + + let do_h = Value::Global(fir_id_for(pkg, items["DoH"]), FunctorApp::default()); + let args = Value::Tuple( + vec![ + Value::Int(1), + Value::Int(2), + do_h, + Value::Bool(true), + Value::Int(4), + ] + .into(), + None, + ); + + let qir = callable_args_to_qir(&store, pkg, items["Wide"], &args, caps); + assert!( + qir.contains("__quantum__qis__h__body"), + "expected H gate in QIR:\n{qir}" + ); +} + +// ---- Synthetic path: plain tuple with callable ---- + +#[test] +fn plain_tuple_with_callable_takes_synthetic_path() { + // A plain `Value::Tuple(_, None)` (no UDT tag) containing a callable takes + // the same synthetic path as UDT values. + let source = indoc::indoc! {r#" + namespace Test { + operation RunPair(op : Qubit => Unit, n : Int) : Result { + use q = Qubit(); + for _ in 0..n - 1 { op(q); } + MResetZ(q) + } + operation DoH(q : Qubit) : Unit { H(q); } + } + "#}; + let caps = Profile::AdaptiveRIF.into(); + let (store, pkg, items) = + compile_and_locate_items(source, &[("RunPair", true), ("DoH", true)], caps); + + let do_h = Value::Global(fir_id_for(pkg, items["DoH"]), FunctorApp::default()); + // Plain tuple — no UDT tag. + let args = Value::Tuple(vec![do_h, Value::Int(2)].into(), None); + + let qir = callable_args_to_qir(&store, pkg, items["RunPair"], &args, caps); + assert!( + qir.contains("__quantum__qis__h__body"), + "expected H gate in QIR:\n{qir}" + ); +} + +// ---- Synthetic path: struct with two callable fields ---- + +#[test] +fn synthetic_path_struct_with_two_callable_fields_generates_qir() { + // A newtype with two arrow fields. Both are wrapped in the UDT. + let source = indoc::indoc! {r#" + namespace Test { + newtype Ops = (First: Qubit => Unit, Second: Qubit => Unit); + operation RunOps(ops: Ops) : Result { + use q = Qubit(); + ops::First(q); + ops::Second(q); MResetZ(q) } operation DoH(q: Qubit) : Unit { H(q); } + operation DoX(q: Qubit) : Unit { X(q); } + } + "#}; + let caps = Profile::AdaptiveRIF.into(); + let (store, pkg, items) = compile_and_locate_items( + source, + &[ + ("RunOps", true), + ("DoH", true), + ("DoX", true), + ("Ops", false), + ], + caps, + ); + + let do_h = Value::Global(fir_id_for(pkg, items["DoH"]), FunctorApp::default()); + let do_x = Value::Global(fir_id_for(pkg, items["DoX"]), FunctorApp::default()); + let ops = Value::Tuple( + vec![do_h, do_x].into(), + Some(Rc::new(fir_id_for(pkg, items["Ops"]))), + ); + + let qir = callable_args_to_qir(&store, pkg, items["RunOps"], &ops, caps); + assert!( + qir.contains("__quantum__qis__h__body"), + "expected H gate in QIR:\n{qir}" + ); + assert!( + qir.contains("__quantum__qis__x__body"), + "expected X gate in QIR:\n{qir}" + ); + // `First` maps to `DoH` and `Second` to `DoX`. Confirm the per-field + // dispatch resolves in that order, guarding against a field-index mix-up + // where the second callable field collapses onto the first. + let h_pos = qir.find("__quantum__qis__h__body").expect("H gate present"); + let x_pos = qir.find("__quantum__qis__x__body").expect("X gate present"); + assert!( + h_pos < x_pos, + "expected First (H) to be emitted before Second (X):\n{qir}" + ); +} + +// ---- Synthetic path: callable with Pauli and Result args ---- + +#[test] +fn synthetic_path_callable_with_pauli_and_result_values() { + // Exercises the Pauli and Result branches of `lower_value_to_expr`. + let source = indoc::indoc! {r#" + namespace Test { + operation Measure(op : Qubit => Unit, basis : Pauli) : Result { + use q = Qubit(); + op(q); + MResetZ(q) + } + operation DoH(q : Qubit) : Unit { H(q); } } "#}; let caps = Profile::AdaptiveRIF.into(); let (store, pkg, items) = - compile_and_locate_items(source, &[("RunBoxed", true), ("DoH", true)], caps); + compile_and_locate_items(source, &[("Measure", true), ("DoH", true)], caps); let do_h = Value::Global(fir_id_for(pkg, items["DoH"]), FunctorApp::default()); + let args = Value::Tuple( + vec![do_h, Value::Pauli(qsc_fir::fir::Pauli::Z)].into(), + None, + ); - let qir = callable_args_to_qir(&store, pkg, items["RunBoxed"], &do_h, caps); + let qir = callable_args_to_qir(&store, pkg, items["Measure"], &args, caps); + assert!( + qir.contains("__quantum__qis__h__body"), + "expected H gate in QIR:\n{qir}" + ); +} + +// ---- Synthetic path: callable whose RETURN type is a closure ---- + +#[test] +fn callable_returning_closure_arg_generates_qir() { + // `MakeOp` returns a closure (`() => H(First(qs))`). Passing it to `DoOp` + // previously panicked with "global not present" because codegen re-invoked + // the original target after the producer closure had been erased. The + // synthetic-entry route must generate QIR containing the H gate instead. + let source = indoc::indoc! {r#" + namespace Test { + function First<'T>(arr : 'T[]) : 'T { arr[0] } + function MakeOp(qs : Qubit[]) : Unit => Unit is Adj + Ctl { + () => H(First(qs)) + } + operation DoOp(make : Qubit[] -> Unit => Unit is Adj + Ctl) : Unit is Adj + Ctl { + use qs = Qubit[1]; + let op = make(qs); + op(); + } + } + "#}; + let caps = Profile::AdaptiveRIF.into(); + let (store, pkg, items) = + compile_and_locate_items(source, &[("DoOp", true), ("MakeOp", true)], caps); + + let make_op = Value::Global(fir_id_for(pkg, items["MakeOp"]), FunctorApp::default()); + let qir = callable_args_to_qir(&store, pkg, items["DoOp"], &make_op, caps); assert!( qir.contains("__quantum__qis__h__body"), "expected H gate in QIR:\n{qir}" @@ -3026,435 +4161,1163 @@ fn synthetic_path_single_field_struct_wrapping_callable_generates_qir() { } #[test] -fn synthetic_path_struct_wrapping_callable_and_tag_generates_qir() { - // A newtype that wraps a callable and a non-callable field. - // This keeps tuple structure in the runtime Value while still exercising - // UDT pure-type discovery. +fn chemistry_like_sequential_partial_application_generates_qir() { + let source = indoc::indoc! {r#" + namespace Test { + import Std.Arrays.Subarray; + + operation ApplyFirstStep(systems : Qubit[]) : Unit { + for q in systems { + H(q); + } + } + + operation ApplySecondStep(systems : Qubit[]) : Unit { + for q in systems { + X(q); + } + } + + operation ApplyThirdStep(systems : Qubit[]) : Unit { + for q in systems { + Z(q); + } + } + + operation ApplySequential( + first : Qubit[] => Unit, + second : Qubit[] => Unit, + systems : Qubit[] + ) : Unit { + first(systems); + second(systems); + } + + function MakeSequentialOp( + first : Qubit[] => Unit, + second : Qubit[] => Unit + ) : Qubit[] => Unit { + ApplySequential(first, second, _) + } + + function MaxInt(values : Int[]) : Int { + mutable max = values[0]; + for idx in 1 .. Length(values) - 1 { + let value = values[idx]; + if value > max { + set max = value; + } + } + return max; + } + + operation MakeSequentialCircuit( + first : Qubit[] => Unit, + second : Qubit[] => Unit, + targets : Int[] + ) : Unit { + if Length(targets) == 0 { + return (); + } else { + let maxTarget = MaxInt(targets); + use qs = Qubit[1 + maxTarget]; + ApplySequential(first, second, Subarray(targets, qs)); + } + } + } + "#}; + let caps = Profile::Base.into(); + let (store, pkg, items) = compile_and_locate_items( + source, + &[ + ("MakeSequentialCircuit", true), + (".lambda", true), + ("ApplyFirstStep", true), + ("ApplySecondStep", true), + ("ApplyThirdStep", true), + ], + caps, + ); + + let first_step = Value::Global( + fir_id_for(pkg, items["ApplyFirstStep"]), + FunctorApp::default(), + ); + let second_step = Value::Global( + fir_id_for(pkg, items["ApplySecondStep"]), + FunctorApp::default(), + ); + let third_step = Value::Global( + fir_id_for(pkg, items["ApplyThirdStep"]), + FunctorApp::default(), + ); + let sequential = Value::Closure(Box::new(qsc_eval::val::Closure { + fixed_args: vec![first_step, second_step].into(), + id: fir_id_for(pkg, items[".lambda"]), + functor: FunctorApp::default(), + })); + let args = Value::Tuple( + vec![ + sequential, + third_step, + Value::Array(vec![Value::Int(0), Value::Int(1)].into()), + ] + .into(), + None, + ); + + let qir = callable_args_to_qir(&store, pkg, items["MakeSequentialCircuit"], &args, caps); + for expected_gate in [ + "__quantum__qis__h__body", + "__quantum__qis__x__body", + "__quantum__qis__z__body", + ] { + assert!( + qir.contains(expected_gate), + "expected {expected_gate} in QIR:\n{qir}" + ); + } +} + +#[test] +fn chemistry_like_controlled_factory_generates_qir() { let source = indoc::indoc! {r#" namespace Test { - newtype OpBox = (Op: Qubit => Unit, Tag: Int); - operation RunBoxed(b: OpBox) : Result { - use q = Qubit(); - b::Op(q); - MResetZ(q) + operation PrepareIdentity(qs : Qubit[]) : Unit is Adj + Ctl {} + + operation SelectIdentity(systems : Qubit[], ancilla : Qubit[]) : Unit is Adj + Ctl {} + + function MakeControlledPrepSelPrepOp( + prepareOp : Qubit[] => Unit is Adj + Ctl, + selectOp : (Qubit[], Qubit[]) => Unit is Adj + Ctl, + numSystemQubits : Int, + numAncillaQubits : Int, + power : Int + ) : (Qubit, Qubit[]) => Unit { + (control, allQubits) => { + let systems = allQubits[0..numSystemQubits - 1]; + let ancilla = allQubits[numSystemQubits...]; + for _ in 0..power - 1 { + Controlled prepareOp([control], systems); + Controlled selectOp([control], (systems, ancilla)); + } + } + } + + operation MakeControlledPrepSelPrepCircuit( + prepareOp : Qubit[] => Unit is Adj + Ctl, + selectOp : (Qubit[], Qubit[]) => Unit is Adj + Ctl, + numSystemQubits : Int, + numAncillaQubits : Int, + power : Int + ) : Unit { + use control = Qubit(); + use systems = Qubit[numSystemQubits + numAncillaQubits]; + let op = MakeControlledPrepSelPrepOp( + prepareOp, + selectOp, + numSystemQubits, + numAncillaQubits, + power + ); + op(control, systems); } - operation DoH(q: Qubit) : Unit { H(q); } } "#}; - let caps = Profile::AdaptiveRIF.into(); + let caps = Profile::Base.into(); let (store, pkg, items) = compile_and_locate_items( source, - &[("RunBoxed", true), ("DoH", true), ("OpBox", false)], + &[ + ("MakeControlledPrepSelPrepCircuit", true), + ("PrepareIdentity", true), + ("SelectIdentity", true), + ], caps, ); - let do_h = Value::Global(fir_id_for(pkg, items["DoH"]), FunctorApp::default()); - let boxed = Value::Tuple( - vec![do_h, Value::Int(0)].into(), - Some(Rc::new(fir_id_for(pkg, items["OpBox"]))), + let prepare = Value::Global( + fir_id_for(pkg, items["PrepareIdentity"]), + FunctorApp::default(), + ); + let select = Value::Global( + fir_id_for(pkg, items["SelectIdentity"]), + FunctorApp::default(), + ); + let args = Value::Tuple( + vec![prepare, select, Value::Int(1), Value::Int(1), Value::Int(1)].into(), + None, ); - let qir = callable_args_to_qir(&store, pkg, items["RunBoxed"], &boxed, caps); + let qir = callable_args_to_qir( + &store, + pkg, + items["MakeControlledPrepSelPrepCircuit"], + &args, + caps, + ); assert!( - qir.contains("__quantum__qis__h__body"), - "expected H gate in QIR:\n{qir}" + qir.contains("define i64 @ENTRYPOINT__main()"), + "expected entry point in QIR:\n{qir}" ); } #[test] -fn synthetic_path_udt_wrapped_adjoint_callable_preserves_functor() { +fn chemistry_like_controlled_psp_wrapper_generates_qir() { let source = indoc::indoc! {r#" namespace Test { - newtype OpBox = (Op: Qubit => Unit is Adj, Tag: Int); - operation RunBoxed(b: OpBox) : Result { - use q = Qubit(); - b::Op(q); - MResetZ(q) + operation PrepareIdentity(qs : Qubit[]) : Unit is Adj + Ctl {} + + operation SelectIdentity(ancilla : Qubit[], systems : Qubit[]) : Unit is Adj + Ctl {} + + operation PrepSelPrep( + prepareOp : Qubit[] => Unit is Adj + Ctl, + selectOp : (Qubit[], Qubit[]) => Unit is Adj + Ctl, + systems : Qubit[], + ancilla : Qubit[] + ) : Unit is Adj + Ctl { + body ... { + prepareOp(ancilla); + selectOp(ancilla, systems); + Adjoint prepareOp(ancilla); + } + adjoint auto; + controlled (ctls, ...) { + prepareOp(ancilla); + Controlled selectOp(ctls, (ancilla, systems)); + Adjoint prepareOp(ancilla); + } + controlled adjoint auto; + } + + function MakeControlledPrepSelPrepOp( + prepareOp : Qubit[] => Unit is Adj + Ctl, + selectOp : (Qubit[], Qubit[]) => Unit is Adj + Ctl, + numSystemQubits : Int, + numAncillaQubits : Int, + power : Int + ) : (Qubit, Qubit[]) => Unit { + (control, allQubits) => { + let systems = allQubits[0..numSystemQubits - 1]; + let ancilla = allQubits[numSystemQubits...]; + for _ in 0..power - 1 { + Controlled PrepSelPrep([control], (prepareOp, selectOp, systems, ancilla)); + } + } + } + + operation MakeControlledPrepSelPrepCircuit( + prepareOp : Qubit[] => Unit is Adj + Ctl, + selectOp : (Qubit[], Qubit[]) => Unit is Adj + Ctl, + numSystemQubits : Int, + numAncillaQubits : Int, + power : Int + ) : Unit { + use control = Qubit(); + use systems = Qubit[numSystemQubits + numAncillaQubits]; + let op = MakeControlledPrepSelPrepOp( + prepareOp, + selectOp, + numSystemQubits, + numAncillaQubits, + power + ); + op(control, systems); } - operation DoS(q: Qubit) : Unit is Adj { S(q); } } "#}; - let caps = Profile::AdaptiveRIF.into(); + let caps = Profile::Base.into(); let (store, pkg, items) = compile_and_locate_items( source, - &[("RunBoxed", true), ("DoS", true), ("OpBox", false)], + &[ + ("MakeControlledPrepSelPrepCircuit", true), + ("PrepareIdentity", true), + ("SelectIdentity", true), + ], caps, ); - let adjoint_do_s = Value::Global( - fir_id_for(pkg, items["DoS"]), - FunctorApp { - adjoint: true, - controlled: 0, - }, + let prepare = Value::Global( + fir_id_for(pkg, items["PrepareIdentity"]), + FunctorApp::default(), ); - let boxed = Value::Tuple( - vec![adjoint_do_s, Value::Int(0)].into(), - Some(Rc::new(fir_id_for(pkg, items["OpBox"]))), + let select = Value::Global( + fir_id_for(pkg, items["SelectIdentity"]), + FunctorApp::default(), + ); + let args = Value::Tuple( + vec![prepare, select, Value::Int(1), Value::Int(1), Value::Int(1)].into(), + None, ); - let target_hir = hir_id_for(pkg, items["RunBoxed"]); - let (codegen_fir, _backend) = prepare_codegen_fir_from_callable_args( - &store, target_hir, &boxed, caps, - ) - .unwrap_or_else(|errors| { - panic!( - "adjoint UDT-wrapped callable should produce CodegenFir, got: {}", - format_interpret_errors(errors) - ) - }); - let entry = crate::codegen::qir::entry_from_codegen_fir(&codegen_fir); - let qir = qsc_codegen::qir::fir_to_qir( - &codegen_fir.fir_store, + let qir = callable_args_to_qir( + &store, + pkg, + items["MakeControlledPrepSelPrepCircuit"], + &args, caps, - &codegen_fir.compute_properties, - &entry, - ) - .unwrap_or_else(|e| panic!("synthetic entry QIR generation should succeed: {e:?}")); + ); assert!( - qir.contains("__quantum__qis__s__adj"), - "expected adjoint S gate in QIR:\n{qir}" + qir.contains("define i64 @ENTRYPOINT__main()"), + "expected entry point in QIR:\n{qir}" ); } -// ---- Synthetic path: callable arg with additional non-callable tuple values ---- - #[test] -fn synthetic_path_callable_with_double_and_string_generates_qir() { - // Target takes (factor: Double, op: Qubit => Unit, label: String). - // All three value types exercise different branches in `lower_value_to_expr`. +fn chemistry_like_state_preparation_closure_with_empty_expansion_ops_generates_qir() { let source = indoc::indoc! {r#" namespace Test { - operation Tagged(factor : Double, op : Qubit => Unit, label : String) : Result { - use q = Qubit(); - op(q); - MResetZ(q) + struct StatePreparationParams { + rowMap : Int[], + stateVector : Double[], + expansionOps : Int[][], + numQubits : Int + } + + operation ApplyStatePreparation(params : StatePreparationParams, qs : Qubit[]) : Unit is Adj + Ctl { + if Length(params.expansionOps) != 0 { + X(qs[0]); + } + } + + function MakeStatePreparationOp( + rowMap : Int[], + stateVector : Double[], + expansionOps : Int[][], + numQubits : Int + ) : Qubit[] => Unit is Adj + Ctl { + ApplyStatePreparation( + new StatePreparationParams { + rowMap = rowMap, + stateVector = stateVector, + expansionOps = expansionOps, + numQubits = numQubits + }, + _ + ) + } + + operation SelectIdentity(systems : Qubit[], ancilla : Qubit[]) : Unit is Adj + Ctl {} + + function MakeControlledPrepSelPrepOp( + prepareOp : Qubit[] => Unit is Adj + Ctl, + selectOp : (Qubit[], Qubit[]) => Unit is Adj + Ctl, + numSystemQubits : Int, + numAncillaQubits : Int, + power : Int + ) : (Qubit, Qubit[]) => Unit { + (control, allQubits) => { + let systems = allQubits[0..numSystemQubits - 1]; + let ancilla = allQubits[numSystemQubits...]; + for _ in 0..power - 1 { + Controlled prepareOp([control], systems); + Controlled selectOp([control], (systems, ancilla)); + } + } + } + + operation MakeControlledPrepSelPrepCircuit( + prepareOp : Qubit[] => Unit is Adj + Ctl, + selectOp : (Qubit[], Qubit[]) => Unit is Adj + Ctl, + numSystemQubits : Int, + numAncillaQubits : Int, + power : Int + ) : Unit { + use control = Qubit(); + use systems = Qubit[numSystemQubits + numAncillaQubits]; + let op = MakeControlledPrepSelPrepOp( + prepareOp, + selectOp, + numSystemQubits, + numAncillaQubits, + power + ); + op(control, systems); } - operation DoH(q : Qubit) : Unit { H(q); } } "#}; - let caps = Profile::AdaptiveRIF.into(); - let (store, pkg, items) = - compile_and_locate_items(source, &[("Tagged", true), ("DoH", true)], caps); + let caps = Profile::Base.into(); + let (store, pkg, items) = compile_and_locate_items( + source, + &[ + ("MakeControlledPrepSelPrepCircuit", true), + ("ApplyStatePreparation", true), + ("SelectIdentity", true), + ], + caps, + ); - let do_h = Value::Global(fir_id_for(pkg, items["DoH"]), FunctorApp::default()); + let state_params = Value::Tuple( + vec![ + Value::Array(vec![Value::Int(0)].into()), + Value::Array(vec![Value::Double(1.0), Value::Double(0.0)].into()), + Value::Array(vec![].into()), + Value::Int(1), + ] + .into(), + None, + ); + let prepare = Value::Closure(Box::new(qsc_eval::val::Closure { + fixed_args: vec![state_params].into(), + id: fir_id_for(pkg, items["ApplyStatePreparation"]), + functor: FunctorApp::default(), + })); + let select = Value::Global( + fir_id_for(pkg, items["SelectIdentity"]), + FunctorApp::default(), + ); let args = Value::Tuple( - vec![Value::Double(1.5), do_h, Value::String("test".into())].into(), + vec![prepare, select, Value::Int(1), Value::Int(1), Value::Int(1)].into(), None, ); - let qir = callable_args_to_qir(&store, pkg, items["Tagged"], &args, caps); + let qir = callable_args_to_qir( + &store, + pkg, + items["MakeControlledPrepSelPrepCircuit"], + &args, + caps, + ); + // This test exercises the controlled-dispatch compile path for a closure whose + // fixed captured struct must be threaded through `Controlled prepareOp`. Because + // `expansionOps = []`, `ApplyStatePreparation`'s guarded `X(qs[0])` is never + // emitted, so a dropped-vs-threaded capture produces identical (empty) gate + // output here; the QIR cannot discriminate the capture-threading fix on its own. + // The point of the case is that this controlled-dispatch shape compiles cleanly + // to a valid entry point. The semantic assertion that the capture actually + // reaches the controlled call lives at the FIR level in a companion test. assert!( - qir.contains("__quantum__qis__h__body"), - "expected H gate in QIR:\n{qir}" + qir.contains("define i64 @ENTRYPOINT__main()"), + "expected entry point in QIR:\n{qir}" ); } -// ---- Synthetic path: nested struct (UDT inside UDT) with callable ---- - #[test] -fn synthetic_path_nested_struct_with_callable_generates_qir() { - // Two levels of UDT wrapping: Config(Inner: OpBox, N: Int) where - // OpBox(Op: Qubit => Unit, Id: Int). This exercises UDT pure-type descent - // and nested field-chain replacement in defunctionalization. - // Inner UDTs need 2+ fields to avoid the single-field-UDT unwrap issue - // where the Value::Tuple shape misaligns with the erased type. +fn chemistry_like_standard_qpe_callable_array_generates_qir() { let source = indoc::indoc! {r#" namespace Test { - newtype OpBox = (Op: Qubit => Unit, Id: Int); - newtype Config = (Inner: OpBox, N: Int); - operation RunConfig(cfg: Config) : Result { - use q = Qubit(); - cfg::Inner::Op(q); - MResetZ(q) + struct StandardPhaseEstimationParams { + statePrep : Qubit[] => Unit, + controlledUnitary : ((Qubit, Qubit[]) => Unit)[], + phaseQubitPrep : Qubit[] => Unit, + numBits : Int, + systems : Int[], + numAncillaQubits : Int + } + + operation PrepareSystems(systems : Qubit[]) : Unit { + X(systems[0]); + } + + operation PreparePhase(ancillas : Qubit[]) : Unit { + for q in ancillas { + H(q); + } + } + + operation ControlledFirst(control : Qubit, targets : Qubit[]) : Unit { + Controlled X([control], targets[0]); + } + + operation ControlledSecond(control : Qubit, targets : Qubit[]) : Unit { + Controlled Z([control], targets[0]); + } + + operation RunStandardQPE(params : StandardPhaseEstimationParams) : Result[] { + use qs = Qubit[params.numBits + Length(params.systems) + params.numAncillaQubits]; + let ancillas = qs[0..params.numBits - 1]; + let allTargets = qs[params.numBits...]; + + params.statePrep(allTargets); + params.phaseQubitPrep(ancillas); + + for ancillaIdx in 0..params.numBits - 1 { + params.controlledUnitary[ancillaIdx](ancillas[ancillaIdx], allTargets); + } + + ResetAll(allTargets); + mutable results = [Zero, size = params.numBits]; + for idx in 0..params.numBits - 1 { + set results w/= idx <- MResetZ(ancillas[idx]); + } + return results; + } + + operation MakeStandardQPECircuit( + statePrep : Qubit[] => Unit, + controlledUnitary : ((Qubit, Qubit[]) => Unit)[], + numBits : Int, + systems : Int[], + phaseQubitPrep : Qubit[] => Unit, + numAncillaQubits : Int + ) : Result[] { + return RunStandardQPE(new StandardPhaseEstimationParams { + statePrep = statePrep, + controlledUnitary = controlledUnitary, + phaseQubitPrep = phaseQubitPrep, + numBits = numBits, + systems = systems, + numAncillaQubits = numAncillaQubits + }); } - operation DoX(q: Qubit) : Unit { X(q); } } "#}; - let caps = Profile::AdaptiveRIF.into(); + let caps = Profile::Base.into(); let (store, pkg, items) = compile_and_locate_items( source, &[ - ("RunConfig", true), - ("DoX", true), - ("Config", false), - ("OpBox", false), + ("MakeStandardQPECircuit", true), + ("PrepareSystems", true), + ("PreparePhase", true), + ("ControlledFirst", true), + ("ControlledSecond", true), ], caps, ); - let do_x = Value::Global(fir_id_for(pkg, items["DoX"]), FunctorApp::default()); - let inner = Value::Tuple( - vec![do_x, Value::Int(1)].into(), - Some(Rc::new(fir_id_for(pkg, items["OpBox"]))), + let state_prep = Value::Global( + fir_id_for(pkg, items["PrepareSystems"]), + FunctorApp::default(), ); - let config = Value::Tuple( - vec![inner, Value::Int(5)].into(), - Some(Rc::new(fir_id_for(pkg, items["Config"]))), + let controlled_unitaries = Value::Array( + vec![ + Value::Global( + fir_id_for(pkg, items["ControlledFirst"]), + FunctorApp::default(), + ), + Value::Global( + fir_id_for(pkg, items["ControlledSecond"]), + FunctorApp::default(), + ), + ] + .into(), + ); + let phase_prep = Value::Global( + fir_id_for(pkg, items["PreparePhase"]), + FunctorApp::default(), + ); + let args = Value::Tuple( + vec![ + state_prep, + controlled_unitaries, + Value::Int(2), + Value::Array(vec![Value::Int(0)].into()), + phase_prep, + Value::Int(0), + ] + .into(), + None, ); - let qir = callable_args_to_qir(&store, pkg, items["RunConfig"], &config, caps); + let qir = callable_args_to_qir(&store, pkg, items["MakeStandardQPECircuit"], &args, caps); + // The dispatch loop calls controlledUnitary[0] then [1]. Index 0 is + // `ControlledFirst` (Controlled X -> cx) and index 1 is `ControlledSecond` + // (Controlled Z -> cz). Both act on the single system/target qubit (index 2); + // there is no QFT in this variant, so the controlled gates are exactly the two + // dispatch calls. The pre-fix defect collapsed every array element to element + // 0, which would emit two identical cx and no cz. Anchoring on the qubit-2 + // target isolates the dispatch subsequence from the state-prep `X`. + let dispatch_gates: Vec<&str> = qir + .lines() + .filter(|line| line.trim_end().ends_with("inttoptr (i64 2 to %Qubit*))")) + .filter_map(|line| { + if line.contains("__quantum__qis__cx__body") { + Some("cx") + } else if line.contains("__quantum__qis__cz__body") { + Some("cz") + } else { + None + } + }) + .collect(); + assert_eq!( + dispatch_gates, + ["cx", "cz"], + "expected ordered per-index dispatch gates [cx, cz]; got {dispatch_gates:?}:\n{qir}" + ); assert!( - qir.contains("__quantum__qis__x__body"), - "expected X gate in QIR:\n{qir}" + qir.contains("define i64 @ENTRYPOINT__main()"), + "expected entry point in QIR:\n{qir}" ); } #[test] -fn synthetic_path_callable_field_taking_udt_with_callable_generates_qir() { - // Outer wraps a callable whose input is Inner, and Inner itself wraps a - // callable. This exercises UDT expansion through arrow input types, not - // just nested UDT fields that directly contain callable values. +fn chemistry_like_standard_qpe_callable_array_of_closures_generates_qir() { let source = indoc::indoc! {r#" namespace Test { - newtype Inner = (NestedOp: Qubit => Unit, Id: Int); - newtype Outer = (ApplyInner: Inner => Result, Id: Int); + import Std.Arrays.Subarray; + import Std.Canon.ApplyQFT; - operation Invoke(outer: Outer) : Result { - let inner = Inner(DoH, 2); - outer::ApplyInner(inner) + struct ControlledParams { + angle : Double, + repetitions : Int } - operation UseInner(inner: Inner) : Result { - use q = Qubit(); - inner::NestedOp(q); - MResetZ(q) + struct StatePrepParams { + rowMap : Int[], + stateVector : Double[], + expansionOps : Int[][], + numQubits : Int } - operation DoH(q: Qubit) : Unit { H(q); } + struct StandardPhaseEstimationParams { + statePrep : Qubit[] => Unit, + controlledUnitary : ((Qubit, Qubit[]) => Unit)[], + phaseQubitPrep : Qubit[] => Unit, + numBits : Int, + ancillas : Int[], + systems : Int[], + numAncillaQubits : Int + } + + operation PrepareSystems(systems : Qubit[]) : Unit { + X(systems[0]); + } + + operation StatePreparation(params : StatePrepParams, systems : Qubit[]) : Unit { + if Length(params.stateVector) > 0 { + X(systems[0]); + } + } + + function MakeStatePreparationOp(params : StatePrepParams) : Qubit[] => Unit { + StatePreparation(params, _) + } + + operation PreparePhase(ancillas : Qubit[]) : Unit { + for q in ancillas { + H(q); + } + } + + function MakePreparePhaseOp() : Qubit[] => Unit { + PreparePhase(_) + } + + operation RepControlled(params : ControlledParams, control : Qubit, targets : Qubit[]) : Unit { + for _ in 1..params.repetitions { + if params.angle > 0.0 { + Controlled X([control], targets[0]); + } else { + Controlled Z([control], targets[0]); + } + } + } + + operation RunStandardQPE(params : StandardPhaseEstimationParams) : Result[] { + let totalQubits = params.numBits + Length(params.systems) + params.numAncillaQubits; + use qs = Qubit[totalQubits]; + let ancillas = Subarray(params.ancillas, qs); + let systems = Subarray(params.systems, qs); + let unitaryAncillas = if params.numAncillaQubits == 0 { + [] + } else { + qs[params.numBits + Length(params.systems)..Length(qs) - 1] + }; + let allTargets = systems + unitaryAncillas; + + params.statePrep(systems); + params.phaseQubitPrep(ancillas); + + for ancillaIdx in 0..params.numBits - 1 { + params.controlledUnitary[ancillaIdx](ancillas[ancillaIdx], allTargets); + } + + Adjoint ApplyQFT(ancillas); + + ResetAll(allTargets); + mutable results = [Zero, size = params.numBits]; + for idx in 0..params.numBits - 1 { + set results w/= idx <- MResetZ(ancillas[idx]); + } + return results; + } + + operation MakeStandardQPECircuit( + statePrep : Qubit[] => Unit, + controlledUnitary : ((Qubit, Qubit[]) => Unit)[], + numBits : Int, + ancillas : Int[], + systems : Int[], + phaseQubitPrep : Qubit[] => Unit, + numAncillaQubits : Int + ) : Result[] { + return RunStandardQPE(new StandardPhaseEstimationParams { + statePrep = statePrep, + controlledUnitary = controlledUnitary, + phaseQubitPrep = phaseQubitPrep, + numBits = numBits, + ancillas = ancillas, + systems = systems, + numAncillaQubits = numAncillaQubits + }); + } } "#}; - let caps = Profile::AdaptiveRIF.into(); + let caps = Profile::Base.into(); let (store, pkg, items) = compile_and_locate_items( source, &[ - ("Invoke", true), - ("UseInner", true), - ("DoH", true), - ("Inner", false), - ("Outer", false), + ("MakeStandardQPECircuit", true), + ("PrepareSystems", true), + ("StatePreparation", true), + ("PreparePhase", true), + ("RepControlled", true), + ("ControlledParams", false), + ("StatePrepParams", false), ], caps, ); - let use_inner = Value::Global(fir_id_for(pkg, items["UseInner"]), FunctorApp::default()); - let outer = Value::Tuple( - vec![use_inner, Value::Int(1)].into(), - Some(Rc::new(fir_id_for(pkg, items["Outer"]))), + let state_prep_params = Value::Tuple( + vec![ + Value::Array(vec![Value::Int(1), Value::Int(0)].into()), + Value::Array(vec![Value::Double(0.6), Value::Double(0.8)].into()), + Value::Array(vec![Value::Array(vec![Value::Int(0)].into())].into()), + Value::Int(2), + ] + .into(), + None, + ); + let state_prep = Value::Closure(Box::new(qsc_eval::val::Closure { + fixed_args: vec![state_prep_params].into(), + id: fir_id_for(pkg, items["StatePreparation"]), + functor: FunctorApp::default(), + })); + let rep_controlled_id = fir_id_for(pkg, items["RepControlled"]); + let controlled_unitaries = Value::Array( + (0..6) + .map(|idx| { + let params = Value::Tuple( + vec![ + Value::Double(if idx % 2 == 0 { 1.0 } else { -1.0 }), + Value::Int(1), + ] + .into(), + None, + ); + Value::Closure(Box::new(qsc_eval::val::Closure { + fixed_args: vec![params].into(), + id: rep_controlled_id, + functor: FunctorApp::default(), + })) + }) + .collect::>() + .into(), + ); + let phase_prep = Value::Closure(Box::new(qsc_eval::val::Closure { + fixed_args: vec![].into(), + id: fir_id_for(pkg, items["PreparePhase"]), + functor: FunctorApp::default(), + })); + let args = Value::Tuple( + vec![ + state_prep, + controlled_unitaries, + Value::Int(6), + Value::Array((0..6).map(Value::Int).collect::>().into()), + Value::Array(vec![Value::Int(6)].into()), + phase_prep, + Value::Int(0), + ] + .into(), + None, ); - let target_hir = hir_id_for(pkg, items["Invoke"]); - let (codegen_fir, _backend) = prepare_codegen_fir_from_callable_args( - &store, target_hir, &outer, caps, - ) - .unwrap_or_else(|errors| { - panic!( - "callable field taking a UDT with a callable should produce CodegenFir, got: {}", - format_interpret_errors(errors) - ) - }); - let entry = crate::codegen::qir::entry_from_codegen_fir(&codegen_fir); - let qir = qsc_codegen::qir::fir_to_qir( - &codegen_fir.fir_store, - caps, - &codegen_fir.compute_properties, - &entry, - ) - .unwrap_or_else(|e| panic!("synthetic entry QIR generation should succeed: {e:?}")); + let qir = callable_args_to_qir(&store, pkg, items["MakeStandardQPECircuit"], &args, caps); + // The dispatch loop calls controlledUnitary[0..5] in order. Each element is a + // closure over `RepControlled` capturing `angle`: even indices capture 1.0 + // (angle > 0.0 -> Controlled X -> cx) and odd indices capture -1.0 + // (Controlled Z -> cz), so the correct dispatch sequence alternates + // cx, cz, cx, cz, cx, cz. The pre-fix defect collapsed every closure to + // closure 0's captured angle (1.0), emitting six identical cx and no dispatch + // cz. All six dispatch gates act on the single system qubit (index 6). The + // `Adjoint ApplyQFT(ancillas)` that follows emits many cx among the ancilla + // qubits (indices 0..5) but never targets qubit 6 and emits no cz, so + // anchoring on the qubit-6 target isolates the dispatch subsequence. + let dispatch_gates: Vec<&str> = qir + .lines() + .filter(|line| line.trim_end().ends_with("inttoptr (i64 6 to %Qubit*))")) + .filter_map(|line| { + if line.contains("__quantum__qis__cx__body") { + Some("cx") + } else if line.contains("__quantum__qis__cz__body") { + Some("cz") + } else { + None + } + }) + .collect(); + assert_eq!( + dispatch_gates, + ["cx", "cz", "cx", "cz", "cx", "cz"], + "expected alternating per-index dispatch gates; got {dispatch_gates:?}:\n{qir}" + ); assert!( - qir.contains("__quantum__qis__h__body"), - "expected H gate in QIR:\n{qir}" + qir.contains("define i64 @ENTRYPOINT__main()"), + "expected entry point in QIR:\n{qir}" ); } -// ---- Synthetic path: tuple arg where only one element is callable ---- - #[test] -fn synthetic_path_tuple_with_one_callable_among_many_scalars() { - // (Int, Int, Qubit => Unit, Bool, Int) — callable buried deep in a wide tuple. +fn chemistry_like_iqpe_params_struct_generates_qir() { let source = indoc::indoc! {r#" namespace Test { - operation Wide(a : Int, b : Int, op : Qubit => Unit, flag : Bool, c : Int) : Result { - use q = Qubit(); - if flag { op(q); } - MResetZ(q) + import Std.Arrays.Subarray; + + struct IterativePhaseEstimationParams { + statePrep : Qubit[] => Unit, + repControlledUnitary : (Qubit, Qubit[]) => Unit, + accumulatePhase : Double, + phaseQubit : Int, + systems : Int[], + numAncillaQubits : Int + } + + operation PrepareSystems(systems : Qubit[]) : Unit {} + + operation RepControlledUnitary(control : Qubit, targets : Qubit[]) : Unit {} + + operation RunIQPE(params : IterativePhaseEstimationParams) : Result[] { + use qs = Qubit[Length(params.systems) + 1 + params.numAncillaQubits]; + let phaseQubit = qs[params.phaseQubit]; + let systems = Subarray(params.systems, qs); + let ancillas = if params.numAncillaQubits == 0 { + [] + } else { + qs[1 + Length(params.systems)..Length(qs) - 1] + }; + let allTargets = systems + ancillas; + + params.statePrep(systems); + + within { + H(phaseQubit); + } apply { + Rz(params.accumulatePhase, phaseQubit); + params.repControlledUnitary(phaseQubit, allTargets); + } + ResetAll(allTargets); + return [MResetZ(phaseQubit)]; + } + + operation MakeIQPECircuit( + statePrep : Qubit[] => Unit, + repControlledUnitary : (Qubit, Qubit[]) => Unit, + accumulatePhase : Double, + phaseQubit : Int, + systems : Int[], + numAncillaQubits : Int + ) : Result[] { + return RunIQPE(new IterativePhaseEstimationParams { + statePrep = statePrep, + repControlledUnitary = repControlledUnitary, + accumulatePhase = accumulatePhase, + phaseQubit = phaseQubit, + systems = systems, + numAncillaQubits = numAncillaQubits + }); } - operation DoH(q : Qubit) : Unit { H(q); } } "#}; - let caps = Profile::AdaptiveRIF.into(); - let (store, pkg, items) = - compile_and_locate_items(source, &[("Wide", true), ("DoH", true)], caps); + let caps = Profile::Base.into(); + let (store, pkg, items) = compile_and_locate_items( + source, + &[ + ("MakeIQPECircuit", true), + ("PrepareSystems", true), + ("RepControlledUnitary", true), + ], + caps, + ); - let do_h = Value::Global(fir_id_for(pkg, items["DoH"]), FunctorApp::default()); + let state_prep = Value::Global( + fir_id_for(pkg, items["PrepareSystems"]), + FunctorApp::default(), + ); + let rep_controlled_unitary = Value::Global( + fir_id_for(pkg, items["RepControlledUnitary"]), + FunctorApp::default(), + ); let args = Value::Tuple( vec![ + state_prep, + rep_controlled_unitary, + Value::Double(0.25), + Value::Int(0), + Value::Array(vec![Value::Int(1)].into()), Value::Int(1), - Value::Int(2), - do_h, - Value::Bool(true), - Value::Int(4), ] .into(), None, ); - let qir = callable_args_to_qir(&store, pkg, items["Wide"], &args, caps); + let qir = callable_args_to_qir(&store, pkg, items["MakeIQPECircuit"], &args, caps); assert!( - qir.contains("__quantum__qis__h__body"), - "expected H gate in QIR:\n{qir}" + qir.contains("define i64 @ENTRYPOINT__main()"), + "expected entry point in QIR:\n{qir}" ); } -// ---- Synthetic path: plain tuple with callable ---- - #[test] -fn plain_tuple_with_callable_takes_synthetic_path() { - // A plain `Value::Tuple(_, None)` (no UDT tag) containing a callable takes - // the same synthetic path as UDT values. +fn chemistry_like_iqpe_with_controlled_psp_closure_generates_qir() { let source = indoc::indoc! {r#" namespace Test { - operation RunPair(op : Qubit => Unit, n : Int) : Result { - use q = Qubit(); - for _ in 0..n - 1 { op(q); } - MResetZ(q) + struct IterativePhaseEstimationParams { + statePrep : Qubit[] => Unit, + repControlledUnitary : (Qubit, Qubit[]) => Unit, + accumulatePhase : Double, + phaseQubit : Int, + systems : Int[], + numAncillaQubits : Int } - operation DoH(q : Qubit) : Unit { H(q); } - } - "#}; - let caps = Profile::AdaptiveRIF.into(); - let (store, pkg, items) = - compile_and_locate_items(source, &[("RunPair", true), ("DoH", true)], caps); - let do_h = Value::Global(fir_id_for(pkg, items["DoH"]), FunctorApp::default()); - // Plain tuple — no UDT tag. - let args = Value::Tuple(vec![do_h, Value::Int(2)].into(), None); + operation PrepareIdentity(qs : Qubit[]) : Unit is Adj + Ctl {} - let qir = callable_args_to_qir(&store, pkg, items["RunPair"], &args, caps); - assert!( - qir.contains("__quantum__qis__h__body"), - "expected H gate in QIR:\n{qir}" - ); -} + operation SelectIdentity(ancilla : Qubit[], systems : Qubit[]) : Unit is Adj + Ctl {} -// ---- Synthetic path: struct with two callable fields ---- + operation PrepSelPrep( + prepareOp : Qubit[] => Unit is Adj + Ctl, + selectOp : (Qubit[], Qubit[]) => Unit is Adj + Ctl, + systems : Qubit[], + ancilla : Qubit[] + ) : Unit is Adj + Ctl { + body ... { + prepareOp(ancilla); + selectOp(ancilla, systems); + Adjoint prepareOp(ancilla); + } + adjoint auto; + controlled (ctls, ...) { + prepareOp(ancilla); + Controlled selectOp(ctls, (ancilla, systems)); + Adjoint prepareOp(ancilla); + } + controlled adjoint auto; + } -#[test] -fn synthetic_path_struct_with_two_callable_fields_generates_qir() { - // A newtype with two arrow fields. Both are wrapped in the UDT. - let source = indoc::indoc! {r#" - namespace Test { - newtype Ops = (First: Qubit => Unit, Second: Qubit => Unit); - operation RunOps(ops: Ops) : Result { - use q = Qubit(); - ops::First(q); - ops::Second(q); - MResetZ(q) + function MakeControlledPrepSelPrepOp( + prepareOp : Qubit[] => Unit is Adj + Ctl, + selectOp : (Qubit[], Qubit[]) => Unit is Adj + Ctl, + numSystemQubits : Int, + numAncillaQubits : Int, + power : Int + ) : (Qubit, Qubit[]) => Unit { + (control, allQubits) => { + let systems = allQubits[0..numSystemQubits - 1]; + let ancilla = allQubits[numSystemQubits...]; + for _ in 0..power - 1 { + Controlled PrepSelPrep([control], (prepareOp, selectOp, systems, ancilla)); + } + } + } + + operation RunIQPE(params : IterativePhaseEstimationParams) : Result[] { + use qs = Qubit[Length(params.systems) + 1 + params.numAncillaQubits]; + let phaseQubit = qs[params.phaseQubit]; + let allTargets = qs[1...]; + + params.statePrep(allTargets); + + within { + H(phaseQubit); + } apply { + Rz(params.accumulatePhase, phaseQubit); + params.repControlledUnitary(phaseQubit, allTargets); + } + ResetAll(allTargets); + return [MResetZ(phaseQubit)]; + } + + operation MakeIQPECircuit( + statePrep : Qubit[] => Unit, + repControlledUnitary : (Qubit, Qubit[]) => Unit, + accumulatePhase : Double, + phaseQubit : Int, + systems : Int[], + numAncillaQubits : Int + ) : Result[] { + return RunIQPE(new IterativePhaseEstimationParams { + statePrep = statePrep, + repControlledUnitary = repControlledUnitary, + accumulatePhase = accumulatePhase, + phaseQubit = phaseQubit, + systems = systems, + numAncillaQubits = numAncillaQubits + }); } - operation DoH(q: Qubit) : Unit { H(q); } - operation DoX(q: Qubit) : Unit { X(q); } } "#}; - let caps = Profile::AdaptiveRIF.into(); + let caps = Profile::Base.into(); let (store, pkg, items) = compile_and_locate_items( source, &[ - ("RunOps", true), - ("DoH", true), - ("DoX", true), - ("Ops", false), + ("MakeIQPECircuit", true), + ("PrepareIdentity", true), + ("SelectIdentity", true), + (".lambda", true), ], caps, ); - let do_h = Value::Global(fir_id_for(pkg, items["DoH"]), FunctorApp::default()); - let do_x = Value::Global(fir_id_for(pkg, items["DoX"]), FunctorApp::default()); - let ops = Value::Tuple( - vec![do_h, do_x].into(), - Some(Rc::new(fir_id_for(pkg, items["Ops"]))), + let prepare = Value::Global( + fir_id_for(pkg, items["PrepareIdentity"]), + FunctorApp::default(), ); - - let qir = callable_args_to_qir(&store, pkg, items["RunOps"], &ops, caps); - assert!( - qir.contains("__quantum__qis__h__body"), - "expected H gate in QIR:\n{qir}" + let select = Value::Global( + fir_id_for(pkg, items["SelectIdentity"]), + FunctorApp::default(), ); - assert!( - qir.contains("__quantum__qis__x__body"), - "expected X gate in QIR:\n{qir}" + let rep_controlled_unitary = Value::Closure(Box::new(qsc_eval::val::Closure { + fixed_args: vec![prepare.clone(), select, Value::Int(1), Value::Int(1)].into(), + id: fir_id_for(pkg, items[".lambda"]), + functor: FunctorApp::default(), + })); + let args = Value::Tuple( + vec![ + prepare, + rep_controlled_unitary, + Value::Double(0.25), + Value::Int(0), + Value::Array(vec![Value::Int(1)].into()), + Value::Int(1), + ] + .into(), + None, ); - // `First` maps to `DoH` and `Second` to `DoX`. Confirm the per-field - // dispatch resolves in that order, guarding against a field-index mix-up - // where the second callable field collapses onto the first. - let h_pos = qir.find("__quantum__qis__h__body").expect("H gate present"); - let x_pos = qir.find("__quantum__qis__x__body").expect("X gate present"); + + let qir = callable_args_to_qir(&store, pkg, items["MakeIQPECircuit"], &args, caps); assert!( - h_pos < x_pos, - "expected First (H) to be emitted before Second (X):\n{qir}" + qir.contains("define i64 @ENTRYPOINT__main()"), + "expected entry point in QIR:\n{qir}" ); } -// ---- Synthetic path: callable with Pauli and Result args ---- - #[test] -fn synthetic_path_callable_with_pauli_and_result_values() { - // Exercises the Pauli and Result branches of `lower_value_to_expr`. +fn chemistry_like_iqpe_with_udt_capture_closure_generates_base_profile_qir() { let source = indoc::indoc! {r#" namespace Test { - operation Measure(op : Qubit => Unit, basis : Pauli) : Result { - use q = Qubit(); - op(q); - MResetZ(q) + struct ControlledParams { + pauliExponents : Pauli[][], + pauliCoefficients : Double[], + repetitions : Int } - operation DoH(q : Qubit) : Unit { H(q); } - } - "#}; - let caps = Profile::AdaptiveRIF.into(); - let (store, pkg, items) = - compile_and_locate_items(source, &[("Measure", true), ("DoH", true)], caps); - let do_h = Value::Global(fir_id_for(pkg, items["DoH"]), FunctorApp::default()); - let args = Value::Tuple( - vec![do_h, Value::Pauli(qsc_fir::fir::Pauli::Z)].into(), - None, - ); + struct IterativePhaseEstimationParams { + statePrep : Qubit[] => Unit, + repControlledUnitary : (Qubit, Qubit[]) => Unit, + accumulatePhase : Double, + phaseQubit : Int, + systems : Int[], + numAncillaQubits : Int + } - let qir = callable_args_to_qir(&store, pkg, items["Measure"], &args, caps); - assert!( - qir.contains("__quantum__qis__h__body"), - "expected H gate in QIR:\n{qir}" - ); -} + operation PrepareSystems(systems : Qubit[]) : Unit { + X(systems[0]); + } -// ---- Synthetic path: callable whose RETURN type is a closure ---- + operation RepControlledUnitary(params : ControlledParams, control : Qubit, systems : Qubit[]) : Unit { + for _ in 1..params.repetitions { + for idx in 0..Length(params.pauliExponents) - 1 { + if params.pauliCoefficients[idx] > 0.0 { + Controlled X([control], systems[0]); + } else { + Controlled Z([control], systems[0]); + } + } + } + } -#[test] -fn callable_returning_closure_arg_generates_qir() { - // `MakeOp` returns a closure (`() => H(First(qs))`). Passing it to `DoOp` - // previously panicked with "global not present" because codegen re-invoked - // the original target after the producer closure had been erased. The - // synthetic-entry route must generate QIR containing the H gate instead. - let source = indoc::indoc! {r#" - namespace Test { - function First<'T>(arr : 'T[]) : 'T { arr[0] } - function MakeOp(qs : Qubit[]) : Unit => Unit is Adj + Ctl { - () => H(First(qs)) + operation RunIQPE(params : IterativePhaseEstimationParams) : Result[] { + use qs = Qubit[Length(params.systems) + 1 + params.numAncillaQubits]; + let phaseQubit = qs[params.phaseQubit]; + let allTargets = qs[1...]; + + params.statePrep(allTargets); + + within { + H(phaseQubit); + } apply { + Rz(params.accumulatePhase, phaseQubit); + params.repControlledUnitary(phaseQubit, allTargets); + } + ResetAll(allTargets); + return [MResetZ(phaseQubit)]; } - operation DoOp(make : Qubit[] -> Unit => Unit is Adj + Ctl) : Unit is Adj + Ctl { - use qs = Qubit[1]; - let op = make(qs); - op(); + + operation MakeIQPECircuit( + statePrep : Qubit[] => Unit, + repControlledUnitary : (Qubit, Qubit[]) => Unit, + accumulatePhase : Double, + phaseQubit : Int, + systems : Int[], + numAncillaQubits : Int + ) : Result[] { + return RunIQPE(new IterativePhaseEstimationParams { + statePrep = statePrep, + repControlledUnitary = repControlledUnitary, + accumulatePhase = accumulatePhase, + phaseQubit = phaseQubit, + systems = systems, + numAncillaQubits = numAncillaQubits + }); } } "#}; - let caps = Profile::AdaptiveRIF.into(); - let (store, pkg, items) = - compile_and_locate_items(source, &[("DoOp", true), ("MakeOp", true)], caps); + let caps = Profile::Base.into(); + let (store, pkg, items) = compile_and_locate_items( + source, + &[ + ("MakeIQPECircuit", true), + ("PrepareSystems", true), + ("RepControlledUnitary", true), + ("ControlledParams", false), + ], + caps, + ); - let make_op = Value::Global(fir_id_for(pkg, items["MakeOp"]), FunctorApp::default()); - let qir = callable_args_to_qir(&store, pkg, items["DoOp"], &make_op, caps); + let state_prep = Value::Global( + fir_id_for(pkg, items["PrepareSystems"]), + FunctorApp::default(), + ); + let params = Value::Tuple( + vec![ + Value::Array( + vec![Value::Array( + vec![Value::Pauli(qsc_fir::fir::Pauli::X)].into(), + )] + .into(), + ), + Value::Array(vec![Value::Double(1.0)].into()), + Value::Int(1), + ] + .into(), + None, + ); + let rep_controlled_unitary = Value::Closure(Box::new(qsc_eval::val::Closure { + fixed_args: vec![params].into(), + id: fir_id_for(pkg, items["RepControlledUnitary"]), + functor: FunctorApp::default(), + })); + let args = Value::Tuple( + vec![ + state_prep, + rep_controlled_unitary, + Value::Double(0.0), + Value::Int(0), + Value::Array(vec![Value::Int(1)].into()), + Value::Int(0), + ] + .into(), + None, + ); + + let qir = callable_args_to_qir(&store, pkg, items["MakeIQPECircuit"], &args, caps); assert!( - qir.contains("__quantum__qis__h__body"), - "expected H gate in QIR:\n{qir}" + qir.contains("define i64 @ENTRYPOINT__main()"), + "expected entry point in QIR:\n{qir}" + ); + assert!( + qir.contains("__quantum__qis__cx__body"), + "expected threaded Controlled X capture in QIR:\n{qir}" ); } diff --git a/source/compiler/qsc/src/codegen/tests/base_profile.rs b/source/compiler/qsc/src/codegen/tests/base_profile.rs index 593aded0c34..0d479843a28 100644 --- a/source/compiler/qsc/src/codegen/tests/base_profile.rs +++ b/source/compiler/qsc/src/codegen/tests/base_profile.rs @@ -4,7 +4,7 @@ use expect_test::expect; use qsc_data_structures::target::{Profile, TargetCapabilityFlags}; -use super::compile_source_to_qir; +use super::{compile_source_to_qir, compile_source_to_qir_result}; static CAPABILITIES: std::sync::LazyLock = std::sync::LazyLock::new(|| TargetCapabilityFlags::from(Profile::Base)); @@ -533,3 +533,597 @@ fn cross_package_apply_if_greater_le_generates_qir() { "expected valid Base-profile QIR for the cross-package ApplyIfGreaterLE program; got:\n{qir}" ); } + +/// Passing two producer-function-returned closures to a higher-order operation +/// must lower to valid Base-profile QIR with both rotations present. The two +/// `Make(angle)` calls each return a `Rotate(angle, _)` partial application; the +/// higher-order `ApplyTwo` consumes both arrow arguments, so defunctionalize must +/// specialize both in one pass. This is the reported regression repro. +#[test] +fn two_callable_args_via_producer_function() { + let source = "namespace Test { + import Std.Intrinsic.*; + import Std.Measurement.*; + operation Rotate(angle : Double, q : Qubit) : Unit { Rx(angle, q); } + function Make(angle : Double) : (Qubit => Unit) { return Rotate(angle, _); } + operation ApplyTwo(a : Qubit => Unit, b : Qubit => Unit) : Result { + use q = Qubit(); + a(q); + b(q); + return MResetZ(q); + } + @EntryPoint() + operation Main() : Result { return ApplyTwo(Make(0.5), Make(0.3)); } + }"; + + let qir = compile_source_to_qir(source, *CAPABILITIES); + assert!( + qir.contains("define i64 @ENTRYPOINT__main()") + && qir.contains("__quantum__qis__rx__body(double 0.5,") + && qir.contains("__quantum__qis__rx__body(double 0.3,"), + "expected valid Base-profile QIR with both rx(0.5) and rx(0.3) rotations; got:\n{qir}" + ); +} + +/// One inline partial application plus one producer-function-returned closure +/// must lower to valid Base-profile QIR with both rotations present. The inline +/// `Rotate(0.5, _)` resolves immediately while `Make(0.3)` returns a closure, so +/// defunctionalize must specialize both arrow arguments of the single +/// `ApplyTwo` call together rather than deferring one slot across iterations. +#[test] +fn two_callable_args_inline_then_producer() { + let source = "namespace Test { + import Std.Intrinsic.*; + import Std.Measurement.*; + operation Rotate(angle : Double, q : Qubit) : Unit { Rx(angle, q); } + function Make(angle : Double) : (Qubit => Unit) { return Rotate(angle, _); } + operation ApplyTwo(a : Qubit => Unit, b : Qubit => Unit) : Result { + use q = Qubit(); + a(q); + b(q); + return MResetZ(q); + } + @EntryPoint() + operation Main() : Result { return ApplyTwo(Rotate(0.5, _), Make(0.3)); } + }"; + + let qir = compile_source_to_qir(source, *CAPABILITIES); + assert!( + qir.contains("define i64 @ENTRYPOINT__main()") + && qir.contains("__quantum__qis__rx__body(double 0.5,") + && qir.contains("__quantum__qis__rx__body(double 0.3,"), + "expected valid Base-profile QIR with both rx(0.5) and rx(0.3) rotations; got:\n{qir}" + ); +} + +/// Three producer-function-returned closures sharing the same returned-closure +/// target must lower to valid Base-profile QIR with all three rotations present. +/// Each `Make(angle)` returns a `Rotate(angle, _)` partial application targeting +/// the same lambda, so the combined specialization must thread each slot's +/// distinct capture without collapsing them. +#[test] +fn three_callable_args_all_producers() { + let source = "namespace Test { + import Std.Intrinsic.*; + import Std.Measurement.*; + operation Rotate(angle : Double, q : Qubit) : Unit { Rx(angle, q); } + function Make(angle : Double) : (Qubit => Unit) { return Rotate(angle, _); } + operation ApplyThree(a : Qubit => Unit, b : Qubit => Unit, c : Qubit => Unit) : Result { + use q = Qubit(); + a(q); + b(q); + c(q); + return MResetZ(q); + } + @EntryPoint() + operation Main() : Result { return ApplyThree(Make(0.1), Make(0.2), Make(0.3)); } + }"; + + let qir = compile_source_to_qir(source, *CAPABILITIES); + assert!( + qir.contains("define i64 @ENTRYPOINT__main()") + && qir.contains("__quantum__qis__rx__body(double 0.1,") + && qir.contains("__quantum__qis__rx__body(double 0.2,") + && qir.contains("__quantum__qis__rx__body(double 0.3,"), + "expected valid Base-profile QIR with rx(0.1), rx(0.2), and rx(0.3) rotations; got:\n{qir}" + ); +} + +/// A global operation argument alongside two same-target producer closures must +/// lower to valid Base-profile QIR with the global gate and both rotations +/// present. The `H` argument resolves to a global while the two `Make(angle)` +/// arguments return same-target closures, exercising mixed slot kinds in one +/// combined specialization. +#[test] +fn three_callable_args_global_then_producers() { + let source = "namespace Test { + import Std.Intrinsic.*; + import Std.Measurement.*; + operation Rotate(angle : Double, q : Qubit) : Unit { Rx(angle, q); } + function Make(angle : Double) : (Qubit => Unit) { return Rotate(angle, _); } + operation ApplyThree(a : Qubit => Unit, b : Qubit => Unit, c : Qubit => Unit) : Result { + use q = Qubit(); + a(q); + b(q); + c(q); + return MResetZ(q); + } + @EntryPoint() + operation Main() : Result { return ApplyThree(H, Make(0.2), Make(0.3)); } + }"; + + let qir = compile_source_to_qir(source, *CAPABILITIES); + assert!( + qir.contains("define i64 @ENTRYPOINT__main()") + && qir.contains("__quantum__qis__h__body(") + && qir.contains("__quantum__qis__rx__body(double 0.2,") + && qir.contains("__quantum__qis__rx__body(double 0.3,"), + "expected valid Base-profile QIR with H, rx(0.2), and rx(0.3); got:\n{qir}" + ); +} + +/// Two producer-function-returned closures carried as the arrow fields of a +/// single tuple-valued parameter must lower to valid Base-profile QIR with both +/// rotations present. `ApplyTwoTup` destructures its `(a, b)` tuple parameter +/// and calls each field, so defunctionalize must specialize both nested arrow +/// fields together in one pass rather than deferring one across iterations. +#[test] +fn two_callable_args_tuple_param_producers() { + let source = "namespace Test { + import Std.Intrinsic.*; + import Std.Measurement.*; + operation Rotate(angle : Double, q : Qubit) : Unit { Rx(angle, q); } + function Make(angle : Double) : (Qubit => Unit) { return Rotate(angle, _); } + operation ApplyTwoTup(ops : (Qubit => Unit, Qubit => Unit)) : Result { + use q = Qubit(); + let (a, b) = ops; + a(q); + b(q); + return MResetZ(q); + } + @EntryPoint() + operation Main() : Result { return ApplyTwoTup((Make(0.5), Make(0.3))); } + }"; + + let qir = compile_source_to_qir(source, *CAPABILITIES); + assert!( + qir.contains("define i64 @ENTRYPOINT__main()") + && qir.contains("__quantum__qis__rx__body(double 0.5,") + && qir.contains("__quantum__qis__rx__body(double 0.3,"), + "expected valid Base-profile QIR with both rx(0.5) and rx(0.3) rotations; got:\n{qir}" + ); +} + +// A single-element tuple parameter `(Qubit => Unit,)` whose only field is a +// producer closure routes through the per-row singular defunctionalization +// path, because the combined path requires two or more closure members in the +// same tuple. Removing the consumed callable field empties the parameter's +// tuple, so its slot and destructuring are dropped while the call site supplies +// only the appended capture. The applied-twice body must inline the producer's +// rotation at each call site. +#[test] +fn callable_args_tuple_param_producer() { + let source = "namespace Test { + import Std.Intrinsic.*; + import Std.Measurement.*; + operation Rotate(angle : Double, q : Qubit) : Unit { Rx(angle, q); } + function Make(angle : Double) : (Qubit => Unit) { return Rotate(angle, _); } + operation ApplyTup(ops : (Qubit => Unit,)) : Result { + use q = Qubit(); + let (a,) = ops; + a(q); + a(q); + return MResetZ(q); + } + @EntryPoint() + operation Main() : Result { return ApplyTup((Make(0.5),)); } + }"; + + let qir = compile_source_to_qir(source, *CAPABILITIES); + assert!( + qir.contains("define i64 @ENTRYPOINT__main()") + && qir.contains("__quantum__qis__rx__body(double 0.5,"), + "expected valid Base-profile QIR with rx(0.5) rotation; got:\n{qir}" + ); +} + +/// A single-element tuple producer parameter applied exactly once still routes +/// through the per-row singular path; dropping the consumed slot must inline the +/// producer's rotation a single time. +#[test] +fn callable_args_tuple_param_producer_single_application() { + let source = "namespace Test { + import Std.Intrinsic.*; + import Std.Measurement.*; + operation Rotate(angle : Double, q : Qubit) : Unit { Rx(angle, q); } + function Make(angle : Double) : (Qubit => Unit) { return Rotate(angle, _); } + operation ApplyTupOnce(ops : (Qubit => Unit,)) : Result { + use q = Qubit(); + let (a,) = ops; + a(q); + return MResetZ(q); + } + @EntryPoint() + operation Main() : Result { return ApplyTupOnce((Make(0.5),)); } + }"; + + let qir = compile_source_to_qir(source, *CAPABILITIES); + assert_eq!( + unitary_gate_sequence(&qir), + vec!["rx(0.5)"], + "expected a single rx(0.5) rotation; got:\n{qir}" + ); +} + +/// A single-element tuple parameter whose only field is a captureless global +/// callable routes through the per-row path's `Bind` arm, which needs no +/// capture threading and so does no outer tuple wrapping. The unit-typed binding +/// it leaves behind matches the unit argument the call supplies, so this case +/// must keep compiling correctly alongside the producer-closure fix. +#[test] +fn callable_args_tuple_param_global() { + let source = "namespace Test { + import Std.Intrinsic.*; + import Std.Measurement.*; + operation ApplyTupGlobal(ops : (Qubit => Unit,)) : Result { + use q = Qubit(); + let (a,) = ops; + a(q); + a(q); + return MResetZ(q); + } + @EntryPoint() + operation Main() : Result { return ApplyTupGlobal((H,)); } + }"; + + let qir = compile_source_to_qir(source, *CAPABILITIES); + assert_eq!( + unitary_gate_sequence(&qir), + vec!["h", "h"], + "expected two h gates from the applied-twice global callable; got:\n{qir}" + ); +} + +// --------------------------------------------------------------------------- +// Mixed branch-dispatch: a callable parameter dispatched over several +// candidates, called alongside single-valued callable arguments at other +// parameter slots. +// --------------------------------------------------------------------------- + +/// Extracts the ordered sequence of quantum gate intrinsics from emitted QIR, +/// e.g. `["h", "rx(0.5)", "x", "rx(0.5)", "mz"]`. Rotation gates include their +/// `double` angle argument so distinct rotations are distinguishable. +fn extract_gate_sequence(qir: &str) -> Vec { + const MARKER: &str = "__quantum__qis__"; + let mut seq = Vec::new(); + for line in qir.lines() { + // Only count actual `call` instructions, not `declare` prototypes. + if !line.trim_start().starts_with("call ") { + continue; + } + let Some(pos) = line.find(MARKER) else { + continue; + }; + let after = &line[pos + MARKER.len()..]; + let name: String = after.chars().take_while(|c| *c != '_').collect(); + if let Some(paren) = after.find('(') { + let args = &after[paren + 1..]; + if let Some(dpos) = args.find("double ") { + let angle: String = args[dpos + "double ".len()..] + .chars() + .take_while(|c| *c != ',' && *c != ')') + .collect(); + seq.push(format!("{name}({})", angle.trim())); + continue; + } + } + seq.push(name); + } + seq +} + +/// Drops measurement/reset/result-readout intrinsics, keeping only unitary +/// gates so the body sequence can be compared against an expected gate order. +fn unitary_gate_sequence(qir: &str) -> Vec { + extract_gate_sequence(qir) + .into_iter() + .filter(|g| { + let head = g.split('(').next().unwrap_or(g); + !matches!( + head, + "mz" | "mresetz" | "m" | "reset" | "read_result" | "result_record_output" + ) + }) + .collect() +} + +/// The callable parameter `f` is dispatched over several candidates `[H, X]` +/// and called alongside a single-valued global sibling `g = Y` at a different +/// parameter slot. The rewrite must keep every dispatch candidate. Restricting +/// the branch-split candidate set to the single dispatched parameter lets each +/// specialized leaf thread the sibling as a runtime argument in its original +/// slot; without that restriction the sibling is incorrectly included in the +/// index dispatch and the call collapses to a single default, dropping `X` and +/// emitting `h, y, h, y` instead of the expected `h, y, x, y`. +#[test] +fn index_dispatch_with_global_sibling_keeps_all_candidates() { + let source = "namespace Test { + import Std.Intrinsic.*; + import Std.Measurement.*; + operation ApplyTwo(f : Qubit => Unit, g : Qubit => Unit, q : Qubit) : Unit { f(q); g(q); } + @EntryPoint() + operation Main() : Result { + use q = Qubit(); + let ops = [H, X]; + for op in ops { ApplyTwo(op, Y, q); } + return MResetZ(q); + } + }"; + + let qir = compile_source_to_qir(source, *CAPABILITIES); + let seq = unitary_gate_sequence(&qir); + assert_eq!( + seq, + vec!["h", "y", "x", "y"], + "expected h,y,x,y (all dispatch candidates preserved); got {seq:?}\n{qir}" + ); +} + +/// The callable parameter `f` is dispatched over several candidates `[H, X]` +/// and called alongside a producer-closure sibling `g = Make(0.5)`, which is a +/// `Rotate(0.5, _)` partial application, at a different parameter slot. The +/// rewrite must keep every dispatch candidate and inline the producer closure +/// into each leaf. Each candidate is specialized into one combined +/// specialization formed as `[candidate] + Make(0.5)`, so the producer closure +/// is consumed before its body could be cleared, emitting `h, rx(0.5), x, +/// rx(0.5)`. +#[test] +fn index_dispatch_with_producer_closure_sibling_inlines_each_leaf() { + let source = "namespace Test { + import Std.Intrinsic.*; + import Std.Measurement.*; + operation Rotate(angle : Double, q : Qubit) : Unit { Rx(angle, q); } + function Make(angle : Double) : (Qubit => Unit) { return Rotate(angle, _); } + operation ApplyTwo(f : Qubit => Unit, g : Qubit => Unit, q : Qubit) : Unit { f(q); g(q); } + @EntryPoint() + operation Main() : Result { + use q = Qubit(); + let ops = [H, X]; + for op in ops { ApplyTwo(op, Make(0.5), q); } + return MResetZ(q); + } + }"; + + let qir = compile_source_to_qir(source, *CAPABILITIES); + let seq = unitary_gate_sequence(&qir); + assert_eq!( + seq, + vec!["h", "rx(0.5)", "x", "rx(0.5)"], + "expected h,rx(0.5),x,rx(0.5) (producer closure inlined into each leaf); got {seq:?}\n{qir}" + ); +} + +/// The callable parameter `f = [H, X]` is dispatched over several candidates at +/// slot 0, called alongside a producer-closure sibling `g = Make(0.5)` at slot +/// 1 and a single-valued global sibling `h = Z` at slot 2. The rewrite must keep +/// every candidate, inline the producer closure into each leaf, and thread the +/// global in its original slot, emitting `h, rx(0.5), z, x, rx(0.5), z`. +#[test] +fn index_dispatch_with_producer_and_global_siblings_inlines_each_leaf() { + let source = "namespace Test { + import Std.Intrinsic.*; + import Std.Measurement.*; + operation Rotate(angle : Double, q : Qubit) : Unit { Rx(angle, q); } + function Make(angle : Double) : (Qubit => Unit) { return Rotate(angle, _); } + operation ApplyThree(f : Qubit => Unit, g : Qubit => Unit, h : Qubit => Unit, q : Qubit) : Unit { f(q); g(q); h(q); } + @EntryPoint() + operation Main() : Result { + use q = Qubit(); + let ops = [H, X]; + for op in ops { ApplyThree(op, Make(0.5), Z, q); } + return MResetZ(q); + } + }"; + + let qir = compile_source_to_qir(source, *CAPABILITIES); + let seq = unitary_gate_sequence(&qir); + assert_eq!( + seq, + vec!["h", "rx(0.5)", "z", "x", "rx(0.5)", "z"], + "expected h,rx(0.5),z,x,rx(0.5),z (producer inlined, global threaded); got {seq:?}\n{qir}" + ); +} + +// --------------------------------------------------------------------------- +// Non-inline tuple arguments: a multi-callable HOF call whose argument tuple is +// a pre-bound local such as `let args = (...); Apply(args)` rather than an +// inline tuple literal. The combined rewrite projects the surviving slots +// through the local's initializer so the reduced arguments match the reduced +// callee. +// --------------------------------------------------------------------------- + +/// The multi-parameter HOF `ApplyTwo(a, b)` is called with a pre-bound tuple +/// local of producer closures, `let args = (Make(0.5), Make(0.3)); +/// ApplyTwo(args)`. The rewrite must reduce the non-inline argument to match the +/// combined specialization, inlining both producer closures and emitting +/// `rx(0.5), rx(0.3)`. +#[test] +fn var_bound_tuple_multi_param_hof_reduces_args() { + let source = "namespace Test { + import Std.Intrinsic.*; + import Std.Measurement.*; + operation Rotate(angle : Double, q : Qubit) : Unit { Rx(angle, q); } + function Make(angle : Double) : (Qubit => Unit) { return Rotate(angle, _); } + operation ApplyTwo(a : Qubit => Unit, b : Qubit => Unit) : Result { + use q = Qubit(); + a(q); + b(q); + return MResetZ(q); + } + @EntryPoint() + operation Main() : Result { + let args = (Make(0.5), Make(0.3)); + return ApplyTwo(args); + } + }"; + + let qir = compile_source_to_qir(source, *CAPABILITIES); + let seq = unitary_gate_sequence(&qir); + assert_eq!( + seq, + vec!["rx(0.5)", "rx(0.3)"], + "expected rx(0.5),rx(0.3) from a pre-bound tuple argument; got {seq:?}\n{qir}" + ); +} + +/// The single tuple-valued-parameter HOF `ApplyTwoTup(ops)` is called with a +/// pre-bound tuple local of producer closures, `let ops = (Make(0.5), +/// Make(0.3)); ApplyTwoTup(ops)`. The rewrite must reduce the non-inline +/// argument the same way, emitting `rx(0.5), rx(0.3)`. +#[test] +fn var_bound_tuple_single_tuple_param_hof_reduces_args() { + let source = "namespace Test { + import Std.Intrinsic.*; + import Std.Measurement.*; + operation Rotate(angle : Double, q : Qubit) : Unit { Rx(angle, q); } + function Make(angle : Double) : (Qubit => Unit) { return Rotate(angle, _); } + operation ApplyTwoTup(ops : (Qubit => Unit, Qubit => Unit)) : Result { + use q = Qubit(); + let (a, b) = ops; + a(q); + b(q); + return MResetZ(q); + } + @EntryPoint() + operation Main() : Result { + let ops = (Make(0.5), Make(0.3)); + return ApplyTwoTup(ops); + } + }"; + + let qir = compile_source_to_qir(source, *CAPABILITIES); + let seq = unitary_gate_sequence(&qir); + assert_eq!( + seq, + vec!["rx(0.5)", "rx(0.3)"], + "expected rx(0.5),rx(0.3) from a pre-bound single tuple-param argument; got {seq:?}\n{qir}" + ); +} + +/// The cleanest non-inline demonstrator uses captureless global callables in a +/// pre-bound tuple, `let args = (H, X); ApplyTwo(args)`. With no producer layer +/// the rewrite simply projects the two globals out of the bound tuple, emitting +/// `h, x`. +#[test] +fn var_bound_tuple_global_callables_reduces_args() { + let source = "namespace Test { + import Std.Intrinsic.*; + import Std.Measurement.*; + operation ApplyTwo(a : Qubit => Unit, b : Qubit => Unit) : Result { + use q = Qubit(); + a(q); + b(q); + return MResetZ(q); + } + @EntryPoint() + operation Main() : Result { + let args = (H, X); + return ApplyTwo(args); + } + }"; + + let qir = compile_source_to_qir(source, *CAPABILITIES); + let seq = unitary_gate_sequence(&qir); + assert_eq!( + seq, + vec!["h", "x"], + "expected h,x from a pre-bound tuple of global callables; got {seq:?}\n{qir}" + ); +} + +/// Out of scope for the non-inline combined rewrite is a +/// function-returning-tuple argument such as `ApplyTwoTup(MakePair())`. The +/// analysis cannot project the arrow fields out of a `Call` result, so the +/// callable stays dynamic and defunctionalization rejects it cleanly rather +/// than miscompiling. +#[test] +fn function_returning_tuple_argument_stays_dynamic() { + let source = "namespace Test { + import Std.Intrinsic.*; + import Std.Measurement.*; + function MakePair() : (Qubit => Unit, Qubit => Unit) { return (H, X); } + operation ApplyTwoTup(ops : (Qubit => Unit, Qubit => Unit)) : Result { + use q = Qubit(); + let (a, b) = ops; + a(q); + b(q); + return MResetZ(q); + } + @EntryPoint() + operation Main() : Result { return ApplyTwoTup(MakePair()); } + }"; + + let result = compile_source_to_qir_result(source, *CAPABILITIES); + assert!( + result.is_err(), + "expected a clean rejection for a function-returning-tuple argument; got Ok:\n{}", + result.unwrap_or_default() + ); +} + +/// A partial-application closure that captures a struct with a computed field +/// must specialize under the base profile. +/// +/// `MakePrepOp` builds a `PrepParams` struct whose `numQubits` field is +/// `Length(stateVector) + Length(rowMap)`, a value computed from the factory's +/// parameters, and returns a closure capturing that struct. The closure is +/// forwarded through the `RunOp` wrapper. +/// +/// Under the base profile a callable that declines to a dynamic call is a hard +/// error that aborts QIR generation, so the computed captured field must +/// specialize. Specialization rebuilds the struct in `Main`, rebinding the +/// parameter references inside the computed field to the caller-scope arguments +/// `[1.0, 0.0]` and `[0]`. With `numQubits = 3`, the specialized closure emits +/// the gated `X` gate, so the test expects base-profile QIR to generate +/// successfully rather than fail during the FIR transform. +#[test] +fn computed_capture_field_specializes_base_profile() { + let source = "namespace Test { + import Std.Intrinsic.*; + import Std.Measurement.*; + struct PrepParams { + stateVector : Double[], + rowMap : Int[], + numQubits : Int + } + operation ApplyPrep(params : PrepParams, q : Qubit) : Unit { + if params.numQubits != 0 { + X(q); + } + } + function MakePrepOp(stateVector : Double[], rowMap : Int[]) : Qubit => Unit { + let params = new PrepParams { + stateVector = stateVector, + rowMap = rowMap, + numQubits = Length(stateVector) + Length(rowMap) + }; + ApplyPrep(params, _) + } + operation RunOp(op : Qubit => Unit, q : Qubit) : Unit { + op(q); + } + @EntryPoint() + operation Main() : Result { + use q = Qubit(); + let prep = MakePrepOp([1.0, 0.0], [0]); + RunOp(prep, q); + return MResetZ(q); + } + }"; + + let qir = compile_source_to_qir(source, *CAPABILITIES); + assert!( + qir.contains("__quantum__qis__x__body"), + "expected the specialized computed-field closure to emit the gated X gate; got:\n{qir}" + ); +} diff --git a/source/compiler/qsc/src/interpret/tests.rs b/source/compiler/qsc/src/interpret/tests.rs index 664e7a5d8d8..b9aae55d6a5 100644 --- a/source/compiler/qsc/src/interpret/tests.rs +++ b/source/compiler/qsc/src/interpret/tests.rs @@ -1310,6 +1310,52 @@ mod given_interpreter { assert_qir_has_three_h_gates(&qir); } + #[test] + fn qirgen_from_callable_with_nested_closure_arg_generates_inner_effect() { + let mut interpreter = get_interpreter_with_capabilities(TargetCapabilityFlags::empty()); + let (result, output) = line( + &mut interpreter, + indoc! {r#" + operation InvokeOne(op : Qubit => Unit) : Unit { + use q = Qubit(); + op(q); + } + + function MakeRz(theta : Double) : Qubit => Unit { + Rz(theta, _) + } + + function MakeOuter(inner : Qubit => Unit) : Qubit => Unit { + inner(_) + } + "#}, + ); + is_only_value(&result, &output, &Value::unit()); + + let invoke_one = user_global(&interpreter, "InvokeOne"); + + let (closure_result, closure_output) = line( + &mut interpreter, + "let inner = MakeRz(4.0); MakeOuter(inner)", + ); + assert!( + closure_output.is_empty(), + "unexpected output while creating nested closure: {closure_output}" + ); + let outer = closure_result.expect("expected nested closure value"); + + let qir = interpreter + .qirgen_from_callable(&invoke_one, outer) + .expect("expected success"); + + assert_eq!( + qir.matches("call void @__quantum__qis__rz__body(double 4.0,") + .count(), + 1, + "expected one inner captured rotation in QIR:\n{qir}" + ); + } + #[test] fn qirgen_from_callable_with_arrow_input_reports_runtime_capability_errors() { let mut interpreter = get_interpreter_with_capabilities( diff --git a/source/compiler/qsc_fir_transforms/src/arg_promote.rs b/source/compiler/qsc_fir_transforms/src/arg_promote.rs index 6dfdc8f1cfd..e973aebc44a 100644 --- a/source/compiler/qsc_fir_transforms/src/arg_promote.rs +++ b/source/compiler/qsc_fir_transforms/src/arg_promote.rs @@ -28,11 +28,11 @@ //! signature/body rewrite ([`promote_callable`]) → call-site rewrite //! ([`rewrite_call_sites`]). Peels one tuple nesting level per round, like //! tuple-decompose. -//! - **Post-convergence normalization.** [`normalize_call_arg_types`] runs once -//! after the fixed point to make argument expression types exactly match -//! callable input types (e.g. `T` → `(T,)` wrapping for single-element tuple -//! inputs). Run once, not per round, to avoid `(T,)` churn polluting change -//! detection. +//! - **Post-convergence normalization.** [`normalize_reachable_call_arg_types`] +//! runs once after the fixed point to make argument expression types exactly +//! match callable input types (e.g. `T` → `(T,)` wrapping for single-element +//! tuple inputs). Run once, not per round, to avoid `(T,)` churn polluting +//! change detection. //! - **Functor-applied callees** (`Adjoint`/`Controlled`) are handled directly: //! [`resolve_direct_item_callee`] unwraps the `UnOp` functor wrappers and //! [`rewrite_controlled_call_site`] preserves the control-tuple layers and @@ -49,8 +49,11 @@ mod cross_package_tests; #[cfg(test)] mod semantic_equivalence_tests; -use crate::EMPTY_EXEC_RANGE; -use crate::fir_builder::{alloc_local_var_expr, decompose_binding_to_leaves, functored_specs}; +use crate::fir_builder::{ + alloc_block, alloc_block_expr, alloc_call_expr, alloc_expr_stmt, alloc_field_path_expr, + alloc_local_var, alloc_local_var_expr, alloc_tuple_expr, decompose_binding_to_leaves, + functored_specs, +}; use crate::package_assigners::PackageAssigners; use crate::reachability::collect_reachable_from_entry; use crate::walk_utils::{ @@ -60,13 +63,12 @@ use crate::walk_utils::{ use qsc_data_structures::span::Span; use qsc_fir::assigner::Assigner; use qsc_fir::fir::{ - Block, CallableDecl, CallableImpl, Expr, ExprId, ExprKind, Field, FieldPath, Functor, Ident, - ItemKind, LocalItemId, LocalVarId, Mutability, Package, PackageId, PackageLookup, PackageStore, - Pat, PatId, PatKind, Res, SpecDecl, SpecImpl, Stmt, StmtId, StmtKind, StoreItemId, UnOp, + CallableDecl, CallableImpl, Expr, ExprId, ExprKind, Field, Functor, ItemKind, LocalItemId, + LocalVarId, Mutability, Package, PackageId, PackageLookup, PackageStore, PatId, PatKind, Res, + SpecDecl, SpecImpl, StmtId, StoreItemId, UnOp, }; use qsc_fir::ty::{Prim, Ty}; use rustc_hash::{FxHashMap, FxHashSet}; -use std::rc::Rc; /// Base name for the synthesized local that holds a materialized call /// argument before it is projected into a promoted callable's scalar inputs @@ -121,7 +123,7 @@ type ParamLeafRemap = (LocalVarId, Ty, LeafRemap); /// - Rewrites only entry-reachable callables. /// - Leaves first-class and closure-target callables unchanged. /// - Normalizes call argument shapes to match callable input types via -/// [`normalize_call_arg_types`]. +/// [`normalize_reachable_call_arg_types`]. /// /// # Mutations /// - Rewrites callable input patterns and specialization bodies. @@ -609,10 +611,12 @@ fn collect_first_class_callables( first_class } -/// Collects the `StoreItemId`s of callables that are targets of -/// `Closure(_, local_item_id)` in the reachable package closure. A closure -/// target resolves in the package that contains the `Closure` expression, so -/// the recorded `StoreItemId` uses that containing package. +/// Collects the `StoreItemId`s of callables that are targets of closure-like +/// dispatch in the reachable package closure. Before defunctionalization this +/// is a direct `Closure(_, local_item_id)` expression. After defunctionalization +/// an indexed closure-array dispatch branch calls the target item directly but +/// still uses the closure-call ABI `(captures..., original_args)`, so the target +/// signature must remain stable for those call sites. fn collect_closure_targets( store: &PackageStore, package_id: PackageId, @@ -640,6 +644,7 @@ fn collect_closure_targets( item: *local_item_id, }); } + collect_closure_abi_direct_call_target(package, pkg, expr, &mut targets); }); } @@ -656,6 +661,7 @@ fn collect_closure_targets( item: *local_item_id, }); } + collect_closure_abi_direct_call_target(package, pkg, expr, &mut targets); }, ); } @@ -665,6 +671,121 @@ fn collect_closure_targets( targets } +/// Records `expr`'s callee as a closure-ABI dispatch target when `expr` is a +/// same-package direct call that still passes the grouped closure-call payload +/// `(captures..., original_args)`. +/// +/// This is the post-defunctionalization case flagged by +/// [`collect_closure_targets`]: an indexed closure-array branch lowers to a +/// direct `Call(Var(Res::Item(id)), args)`, but the argument tuple keeps the +/// closure-call ABI shape rather than the callable's own parameter shape. Such +/// a target's signature must stay stable, so it is excluded from promotion. +/// +/// Only in-package callees are recorded (a foreign item's arity is fixed by its +/// own package's passes, so it is never a promotion candidate here). Non-calls, +/// calls through non-item callees, and calls that use the plain direct-call +/// argument shape are ignored via [`call_uses_grouped_closure_payload`]. +fn collect_closure_abi_direct_call_target( + package: &Package, + package_id: PackageId, + expr: &Expr, + targets: &mut FxHashSet, +) { + // Only interested in call expressions... + let ExprKind::Call(callee_id, arg_id) = expr.kind else { + return; + }; + // ...whose callee is a direct reference to a named item (not a first-class + // value or a functor-applied callee). + let callee_expr = package.get_expr(callee_id); + let ExprKind::Var(Res::Item(item_id), _) = callee_expr.kind else { + return; + }; + // A foreign callee's arity is governed by its own package, so it is never a + // promotion candidate we need to protect here. + if item_id.package != package_id { + return; + } + // Only record it if the call still passes the grouped closure payload; a + // plain direct call with the callable's own argument shape is safe to + // promote and must not be excluded. + if !call_uses_grouped_closure_payload(package, item_id.item, arg_id, Some(&callee_expr.ty)) { + return; + } + targets.insert(StoreItemId { + package: item_id.package, + item: item_id.item, + }); +} + +/// Reports whether a call's argument tuple is shaped like the closure-call ABI +/// `(captures..., original_args)` rather than the callee's own parameter tuple. +/// +/// The closure-call ABI passes each captured variable as a leading scalar and +/// bundles the callable's original arguments into a single trailing tuple, so +/// for a target whose flattened input is `(c0, c1, a0, a1)` the call site looks +/// like: +/// +/// ```text +/// // target signature (flattened): (c0, c1, a0, a1) +/// f(cap0, cap1, (arg0, arg1)) // grouped closure payload -> true +/// f(cap0, cap1, arg0, arg1) // plain direct-call shape -> false +/// ``` +/// +/// The shape is confirmed by matching arities: after replacing the trailing +/// tuple with its own elements, the total argument count must equal the +/// target's parameter count. The target's parameter tuple is read from +/// `callee_ty` when it is an `Arrow` (the call-site view), falling back to the +/// item's declared input pattern otherwise. +fn call_uses_grouped_closure_payload( + package: &Package, + item_id: LocalItemId, + arg_id: ExprId, + callee_ty: Option<&Ty>, +) -> bool { + // The argument must be a tuple with at least a capture and the trailing + // grouped-args tuple. + let ExprKind::Tuple(args) = &package.get_expr(arg_id).kind else { + return false; + }; + if args.len() < 2 { + return false; + } + + // Determine the target's parameter tuple: prefer the call site's arrow view + // of the callee, falling back to the item's declared input pattern type. + let item_input = || { + let Some(ItemKind::Callable(decl)) = package.items.get(item_id).map(|item| &item.kind) + else { + return None; + }; + Some(package.get_pat(decl.input).ty.clone()) + }; + let target_input = match callee_ty { + Some(Ty::Arrow(arrow)) => arrow.input.as_ref().clone(), + _ => item_input().unwrap_or(Ty::Err), + }; + let Ty::Tuple(target_items) = target_input else { + return false; + }; + // Under the grouped ABI the target has strictly more parameters than the + // call has arguments (the trailing tuple stands in for several of them). + if target_items.len() <= args.len() { + return false; + } + + // The trailing argument must itself be a tuple (the bundled original args). + let trailing_arg_ty = &package + .get_expr(*args.last().expect("args is non-empty")) + .ty; + let Ty::Tuple(trailing_items) = trailing_arg_ty else { + return false; + }; + // Confirm the shape by arity: leading captures plus the unbundled trailing + // args must exactly account for every target parameter. + args.len() - 1 + trailing_items.len() == target_items.len() +} + /// Flattens an entire callable input into one flat tuple of scalar leaves, /// dissolving all inter-parameter grouping, then remaps every promotable /// parameter's body field reads to its scalar leaves. @@ -1052,18 +1173,7 @@ fn build_leaf_tuple( // (handled by the early return above), so this fallback is unreachable for // well-formed flattened inputs. Fall back to a unit tuple to keep the // rewrite total. - let expr_id = assigner.next_expr(); - package.exprs.insert( - expr_id, - Expr { - id: expr_id, - span: Span::default(), - ty: sub_ty.clone(), - kind: ExprKind::Tuple(vec![]), - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); - return expr_id; + return alloc_tuple_expr(package, assigner, vec![], sub_ty.clone(), Span::default()); }; let mut child_ids = Vec::with_capacity(elems.len()); @@ -1080,18 +1190,13 @@ fn build_leaf_tuple( child_path.pop(); } - let expr_id = assigner.next_expr(); - package.exprs.insert( - expr_id, - Expr { - id: expr_id, - span: Span::default(), - ty: sub_ty.clone(), - kind: ExprKind::Tuple(child_ids), - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); - expr_id + alloc_tuple_expr( + package, + assigner, + child_ids, + sub_ty.clone(), + Span::default(), + ) } /// Navigates a (possibly nested) tuple type by a positional `path`, returning @@ -1278,35 +1383,15 @@ fn create_projection_temp_binding( arg_ty: &Ty, tmp_counter: &mut u32, ) -> (LocalVarId, StmtId) { - let local_id = assigner.next_local(); - let pat_id = assigner.next_pat(); let temp_name = next_arg_promote_tmp_name(tmp_counter); - package.pats.insert( - pat_id, - Pat { - id: pat_id, - span: Span::default(), - ty: arg_ty.clone(), - kind: PatKind::Bind(Ident { - id: local_id, - span: Span::default(), - name: Rc::from(temp_name.as_str()), - }), - }, - ); - - let stmt_id = assigner.next_stmt(); - package.stmts.insert( - stmt_id, - Stmt { - id: stmt_id, - span: Span::default(), - kind: StmtKind::Local(Mutability::Immutable, pat_id, arg_id), - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); - - (local_id, stmt_id) + alloc_local_var( + package, + assigner, + &temp_name, + arg_ty, + arg_id, + Mutability::Immutable, + ) } /// Returns `true` when the promotion leaf at `path` can be projected out of the @@ -1366,23 +1451,14 @@ fn project_leaf_through_tuple_literal( return current; } - let field_expr_id = assigner.next_expr(); - package.exprs.insert( - field_expr_id, - Expr { - id: field_expr_id, - span: Span::default(), - ty: leaf_ty.clone(), - kind: ExprKind::Field( - current, - Field::Path(FieldPath { - indices: rest.to_vec(), - }), - ), - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); - field_expr_id + alloc_field_path_expr( + package, + assigner, + current, + rest.to_vec(), + leaf_ty.clone(), + Span::default(), + ) } /// Attempts to build the flat projected tuple argument directly from a @@ -1441,17 +1517,7 @@ fn try_inline_tuple_literal_projection( .map(|(_, leaf_ty)| leaf_ty.clone()) .collect(), ); - let new_arg_id = assigner.next_expr(); - package.exprs.insert( - new_arg_id, - Expr { - id: new_arg_id, - span: Span::default(), - ty: tuple_ty, - kind: ExprKind::Tuple(field_expr_ids), - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); + let new_arg_id = alloc_tuple_expr(package, assigner, field_expr_ids, tuple_ty, Span::default()); Some(new_arg_id) } @@ -1495,24 +1561,17 @@ fn create_projected_tuple_arg( } else { arg_id }; - let field_expr_id = assigner.next_expr(); - let field_expr = qsc_fir::fir::Expr { - id: field_expr_id, - span: Span::default(), - ty: leaf_ty.clone(), - kind: ExprKind::Field( - field_base_id, - Field::Path(FieldPath { - indices: path.clone(), - }), - ), - exec_graph_range: EMPTY_EXEC_RANGE, - }; - package.exprs.insert(field_expr_id, field_expr); + let field_expr_id = alloc_field_path_expr( + package, + assigner, + field_base_id, + path.clone(), + leaf_ty.clone(), + Span::default(), + ); field_expr_ids.push(field_expr_id); } - let new_arg_id = assigner.next_expr(); let tuple_ty = Ty::Tuple( promotion .leaves @@ -1520,15 +1579,7 @@ fn create_projected_tuple_arg( .map(|(_, leaf_ty)| leaf_ty.clone()) .collect(), ); - let new_arg = qsc_fir::fir::Expr { - id: new_arg_id, - span: Span::default(), - ty: tuple_ty, - kind: ExprKind::Tuple(field_expr_ids), - exec_graph_range: EMPTY_EXEC_RANGE, - }; - package.exprs.insert(new_arg_id, new_arg); - new_arg_id + alloc_tuple_expr(package, assigner, field_expr_ids, tuple_ty, Span::default()) } /// Wraps a single promoted payload expression in a one-element tuple argument. @@ -1538,16 +1589,13 @@ fn create_single_tuple_arg( arg_id: ExprId, elem_types: &[Ty], ) -> ExprId { - let new_arg_id = assigner.next_expr(); - let new_arg = qsc_fir::fir::Expr { - id: new_arg_id, - span: Span::default(), - ty: Ty::Tuple(elem_types.to_vec()), - kind: ExprKind::Tuple(vec![arg_id]), - exec_graph_range: EMPTY_EXEC_RANGE, - }; - package.exprs.insert(new_arg_id, new_arg); - new_arg_id + alloc_tuple_expr( + package, + assigner, + vec![arg_id], + Ty::Tuple(elem_types.to_vec()), + Span::default(), + ) } /// Builds a block expression that evaluates a leading statement before @@ -1560,40 +1608,17 @@ fn create_payload_block( ) -> ExprId { let result_ty = package.get_expr(result_expr_id).ty.clone(); - let result_stmt_id = assigner.next_stmt(); - package.stmts.insert( - result_stmt_id, - Stmt { - id: result_stmt_id, - span: Span::default(), - kind: StmtKind::Expr(result_expr_id), - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); + let result_stmt_id = alloc_expr_stmt(package, assigner, result_expr_id, Span::default()); - let block_id = assigner.next_block(); - package.blocks.insert( - block_id, - Block { - id: block_id, - span: Span::default(), - ty: result_ty.clone(), - stmts: vec![leading_stmt_id, result_stmt_id], - }, + let block_id = alloc_block( + package, + assigner, + vec![leading_stmt_id, result_stmt_id], + result_ty.clone(), + Span::default(), ); - let block_expr_id = assigner.next_expr(); - package.exprs.insert( - block_expr_id, - Expr { - id: block_expr_id, - span: Span::default(), - ty: result_ty, - kind: ExprKind::Block(block_id), - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); - block_expr_id + alloc_block_expr(package, assigner, block_id, result_ty, Span::default()) } /// Returns `true` when `elems` is already the fully-flattened argument list: @@ -1710,38 +1735,23 @@ fn wrap_call_in_block( call_ty: &Ty, leading_stmt_id: StmtId, ) { - let inner_call_id = assigner.next_expr(); - package.exprs.insert( - inner_call_id, - Expr { - id: inner_call_id, - span: Span::default(), - ty: call_ty.clone(), - kind: ExprKind::Call(callee_id, new_arg_id), - exec_graph_range: EMPTY_EXEC_RANGE, - }, + let inner_call_id = alloc_call_expr( + package, + assigner, + callee_id, + new_arg_id, + call_ty.clone(), + Span::default(), ); - let call_stmt_id = assigner.next_stmt(); - package.stmts.insert( - call_stmt_id, - Stmt { - id: call_stmt_id, - span: Span::default(), - kind: StmtKind::Expr(inner_call_id), - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); + let call_stmt_id = alloc_expr_stmt(package, assigner, inner_call_id, Span::default()); - let block_id = assigner.next_block(); - package.blocks.insert( - block_id, - Block { - id: block_id, - span: Span::default(), - ty: call_ty.clone(), - stmts: vec![leading_stmt_id, call_stmt_id], - }, + let block_id = alloc_block( + package, + assigner, + vec![leading_stmt_id, call_stmt_id], + call_ty.clone(), + Span::default(), ); let call_mut = package @@ -1934,16 +1944,12 @@ fn rebuild_controlled_arg_layers( package.get_expr(controls).ty.clone(), package.get_expr(current).ty.clone(), ]); - let tuple_id = assigner.next_expr(); - package.exprs.insert( - tuple_id, - Expr { - id: tuple_id, - span: Span::default(), - ty: tuple_ty, - kind: ExprKind::Tuple(vec![controls, current]), - exec_graph_range: EMPTY_EXEC_RANGE, - }, + let tuple_id = alloc_tuple_expr( + package, + assigner, + vec![controls, current], + tuple_ty, + Span::default(), ); current = tuple_id; } diff --git a/source/compiler/qsc_fir_transforms/src/defunctionalize.rs b/source/compiler/qsc_fir_transforms/src/defunctionalize.rs index 71b3d8ac2bc..4b89890d2ab 100644 --- a/source/compiler/qsc_fir_transforms/src/defunctionalize.rs +++ b/source/compiler/qsc_fir_transforms/src/defunctionalize.rs @@ -13,27 +13,28 @@ //! - **Specialization, not classical defunctionalization.** Instead of a //! tagged union plus an `apply` dispatcher, each higher-order-function (HOF) //! call site whose concrete callable argument is known at compile time gets -//! its own specialized clone of the HOF with the callable parameter replaced -//! by a direct call. `Apply(q => Y(q), target)` becomes a call to a -//! `Apply_specialized_Y` clone. Single-bound tuple parameters containing -//! callable values are handled via a split locator (top-level slot + nested -//! field path). +//! its own specialized clone of the HOF, with the callable parameter replaced +//! by a direct call. `Apply(q => Y(q), target)` becomes a call to an +//! `Apply_specialized_Y` clone. A callable value nested inside a single tuple +//! parameter is located by a top-level parameter slot plus a nested field +//! path. //! - **Establishes [`crate::invariants::InvariantLevel::PostDefunc`]:** no //! `ExprKind::Closure`, no arrow-typed parameters, and all dispatch is //! direct in reachable code. -//! - **Fixpoint loop.** Each iteration runs: pre-pass (promote single-use -//! callable locals, collapse identity closures `(a) => f(a)` to `f`) → -//! analysis (find callable params + concrete call sites) → specialize (clone -//! per concrete arg combo, deduped by [`types::SpecKey`]) → rewrite (redirect -//! call sites, drop the callable arg, thread captures as extra args) → -//! closure tracking/cleanup. **Closure cleanup is convergence-critical:** it +//! - **Fixpoint loop.** Each iteration runs five steps in order. The pre-pass +//! promotes single-use callable locals and collapses identity closures such +//! as `(a) => f(a)` down to `f`. Analysis finds callable parameters and +//! concrete call sites. Specialize clones a HOF once per concrete argument +//! combination, deduplicated by [`types::SpecKey`]. Rewrite redirects call +//! sites, drops the callable argument, and threads captured values through as +//! extra arguments. A final closure-cleanup step is convergence-critical: it //! replaces consumed closures with `Tuple([])` so they stop counting as -//! work. The iteration cap is scaled dynamically between `MIN_ITERATIONS` -//! and `MAX_ITERATIONS`. -//! Non-convergence appends [`Error::FixpointNotReached`] only if no other -//! diagnostic fired (so a real earlier error is not buried). +//! remaining work. The iteration cap scales dynamically between +//! `MIN_ITERATIONS` and `MAX_ITERATIONS`. Non-convergence appends +//! [`Error::FixpointNotReached`], but only when no other diagnostic already +//! fired, so a real earlier error is not buried. //! - **Diagnostics:** [`Error::ExcessiveSpecializations`] is a non-fatal -//! warning; other errors are fatal because the intermediate FIR may violate +//! warning. Other errors are fatal because the intermediate FIR may violate //! downstream invariants. //! - Synthesized expressions use `EMPTY_EXEC_RANGE`; //! `crate::exec_graph_rebuild` repairs exec graphs later. @@ -59,8 +60,8 @@ use crate::walk_utils::collect_expr_ids_in_entry_and_local_callables; use qsc_data_structures::functors::FunctorApp; use qsc_data_structures::span::Span; use qsc_fir::fir::{ - ExprId, ExprKind, ItemKind, LocalItemId, Package, PackageId, PackageLookup, PackageStore, Res, - StoreItemId, + ExprId, ExprKind, ItemId, ItemKind, LocalItemId, Package, PackageId, PackageLookup, + PackageStore, Res, StoreItemId, }; use qsc_fir::ty::Ty; use rustc_hash::{FxHashMap, FxHashSet}; @@ -69,13 +70,14 @@ use types::{ peel_body_functors, }; -/// Lower bound on the analysis → specialize → rewrite iteration limit. +/// Lower bound on the analysis => specialize => rewrite iteration limit. /// /// The loop always runs at least this many iterations. After the first -/// iteration the limit is recomputed (see [`check_convergence`]) as -/// `max(callable_params.len(), remaining_count).clamp(MIN_ITERATIONS, MAX_ITERATIONS)`, -/// so the floor of 5 gives one iteration of margin beyond the deepest observed -/// HOF chain (4 levels in the chemistry library's Trotter simulation pipeline). +/// iteration [`check_convergence`] recomputes the limit as +/// `max(callable_params.len(), remaining_count).clamp(MIN_ITERATIONS, MAX_ITERATIONS)`. +/// The floor of 5 gives one iteration of margin beyond the deepest HOF chain +/// seen in practice, which is the four-level Trotter simulation pipeline in the +/// chemistry library. const MIN_ITERATIONS: usize = 5; /// Upper bound on the dynamically-computed iteration limit, capping the work @@ -194,6 +196,21 @@ pub(crate) fn defunctionalize( } } + // A `UnsupportedMultipleCallableArrays` guard skips its offending group, so + // the callable arrays that group would have specialized stay unresolved and + // their forwarding consumers (e.g. an inner HOF call taking the still- + // abstract array parameters) surface as generic `DynamicCallable` + // diagnostics. Those are downstream consequences of the guarded shape, so + // drop them and report only the specific root-cause diagnostic, mirroring + // how `emit_fixpoint_error` withholds a generic non-convergence report once + // a more actionable error has already fired. + if errors + .iter() + .any(|e| matches!(e, Error::UnsupportedMultipleCallableArrays(_))) + { + errors.retain(|e| !matches!(e, Error::DynamicCallable(_))); + } + emit_fixpoint_error( store, package_id, @@ -244,6 +261,17 @@ fn run_specialization( .collect()), ); spec_errors.retain(|e| !matches!(e, Error::ExcessiveSpecializations(..))); + // `UnsupportedMultipleCallableArrays` is intentionally not swept by the + // per-iteration `DynamicCallable` retain, so the guarded group re-reports it + // every fixpoint iteration. Drop any whose span already survives in `errors` + // so a single diagnostic persists across iterations rather than one copy per + // pass. + spec_errors.retain(|e| match e { + Error::UnsupportedMultipleCallableArrays(span) => !errors.iter().any( + |existing| matches!(existing, Error::UnsupportedMultipleCallableArrays(s) if s == span), + ), + _ => true, + }); errors.append(&mut spec_errors); spec_map } @@ -286,14 +314,82 @@ fn track_specialized_closures( specialized_closure_targets: &mut FxHashSet, specialized_items: &mut FxHashSet, ) { + // Group by package and call expression once, shared by the consistency + // check below and the combined-keying registration. Grouping matches the + // specializer's grouping and stays correct when call sites span packages. + let mut groups: FxHashMap<(PackageId, ExprId), Vec<&CallSite>> = FxHashMap::default(); + for cs in &analysis.call_sites { + groups + .entry((cs.call_pkg_id, cs.call_expr_id)) + .or_default() + .push(cs); + } + + // Single-arg keying: records producer closures consumed by branch-split / + // condition-dispatch specializations, whose per-candidate specs are keyed + // individually. for cs in &analysis.call_sites { let spec_key = build_spec_key(cs); if spec_map.contains_key(&spec_key) && let ConcreteCallable::Closure { target, .. } = &cs.callable_arg { + // Internal consistency check. When a producer-closure argument is a + // single-valued sibling of a parameter that is dispatched over + // several candidates, recording it as consumed here would let + // `cleanup_consumed_closures` clear its producer body while the + // dispatched siblings are still live, un-inlined call sites. The + // next iteration + // would then re-read the cleared body as `Dynamic` and the call + // would compile to incorrect output. The combined per-candidate + // specialization handles this shape instead, so this + // single-argument per-row specialization should never exist for it. + // If it does, that specialization did not run, so stop with a clear + // error rather than emitting incorrect QIR. + if let Some(group) = groups.get(&(cs.call_pkg_id, cs.call_expr_id)) + && closure_constant_sibling_of_dispatch(group, cs) + { + // A `Dynamic` sibling means `partition_mixed_branch_split` + // intentionally declined this group so the unresolved argument + // can surface as `DynamicCallable`. In that case the per-row + // closure spec may exist only as a transient side effect of + // collecting diagnostics; do not mark its producer body as + // consumed, and do not treat the absence of the mixed combined + // spec as an internal rewrite/specialization disagreement. + if group + .iter() + .any(|member| matches!(member.callable_arg, ConcreteCallable::Dynamic)) + { + continue; + } + panic!( + "internal error in defunctionalize: producer-closure target {target:?} is a \ + single-valued sibling of a parameter dispatched over several candidates at \ + call expression {:?} in package {:?}, but is being recorded as consumed via \ + its own per-row specialization without combined specialization. Clearing its \ + producer body now would leave the dispatched siblings referring to a removed \ + body and produce incorrect output.", + cs.call_expr_id, cs.call_pkg_id, + ); + } specialized_closure_targets.insert(StoreItemId::from((cs.call_pkg_id, *target))); } } + // Combined keying: a multi-arrow-param call produces one specialization + // keyed by the combined key, so every participating producer body must be + // recorded under that combined key. The combined and single-arg key spaces + // are disjoint by argument count, so this is additive: missing a member + // here would leave a stray `Closure` that `exec_graph_rebuild` rejects. + for group in groups.values() { + let combined_key = build_combined_spec_key_for_group(group[0].hof_item_id, group); + if spec_map.contains_key(&combined_key) { + for cs in group { + if let ConcreteCallable::Closure { target, .. } = &cs.callable_arg { + specialized_closure_targets + .insert(StoreItemId::from((cs.call_pkg_id, *target))); + } + } + } + } for direct_call_site in &analysis.direct_call_sites { if let ConcreteCallable::Closure { target, .. } = &direct_call_site.callable { specialized_closure_targets @@ -304,7 +400,8 @@ fn track_specialized_closures( } /// Checks whether the fixed-point loop should terminate. Returns `true` when -/// the loop should break (converged or stuck). +/// the loop should break, either because it has converged or because it is +/// stuck. fn check_convergence( store: &PackageStore, package_id: PackageId, @@ -386,6 +483,11 @@ fn cleanup_consumed_closures_per_package( return; } + // A freshly specialized item can still be the only live path to a producer + // in the same iteration. Defer that producer so cleanup does not erase the + // body before the next specialization pass can inline it. + let deferred_items = items_called_from_skipped_items(store, skip_items); + for pkg_id in collect_reachable_package_closure(entry_pkg_id, reachable) { let targets_local: FxHashSet = specialized_targets .iter() @@ -395,11 +497,17 @@ fn cleanup_consumed_closures_per_package( if targets_local.is_empty() { continue; } - let skip_local: FxHashSet = skip_items + let mut skip_local: FxHashSet = skip_items .iter() .filter(|s| s.package == pkg_id) .map(|s| s.item) .collect(); + skip_local.extend( + deferred_items + .iter() + .filter(|s| s.package == pkg_id) + .map(|s| s.item), + ); let local_item_ids: Vec = { let package = store.get(pkg_id); reachable_local_callables(package, pkg_id, reachable) @@ -417,6 +525,38 @@ fn cleanup_consumed_closures_per_package( } } +/// Finds direct callees used by freshly specialized items so their producer +/// bodies survive until the next defunctionalization iteration. +fn items_called_from_skipped_items( + store: &PackageStore, + skip_items: &FxHashSet, +) -> FxHashSet { + let mut called_items = FxHashSet::default(); + + for skipped_item in skip_items { + let package = store.get(skipped_item.package); + let item = package.get_item(skipped_item.item); + if let ItemKind::Callable(decl) = &item.kind { + crate::walk_utils::for_each_expr_in_callable_impl( + package, + &decl.implementation, + &mut |_expr_id, expr| { + if let ExprKind::Call(callee_id, _) = &expr.kind { + let (base_id, _) = peel_body_functors(package, *callee_id); + if let ExprKind::Var(Res::Item(item_id), _) = + &package.get_expr(base_id).kind + { + called_items.insert(StoreItemId::from((item_id.package, item_id.item))); + } + } + }, + ); + } + } + + called_items +} + /// Replaces all remaining closure expressions whose target callable was /// consumed by specialization with Unit values, clearing references so /// subsequent iterations do not count them as work remaining. @@ -700,9 +840,18 @@ fn ty_contains_arrow_through_udts(store: &PackageStore, ty: &Ty) -> bool { } } -/// Builds the deduplication key for a call site's specialization. -pub(crate) fn build_spec_key(call_site: &CallSite) -> SpecKey { - let concrete_key = match &call_site.callable_arg { +/// Maps a single concrete callable argument to its hashable dedup key. +/// +/// Closures are keyed only by their package-qualified target and functor; +/// captured values are threaded as ordinary call arguments and are not part of +/// the dispatch identity. A `Dynamic` argument is filtered out before reaching +/// specialization but still yields a deterministic key. +fn concrete_callable_key( + call_pkg_id: PackageId, + callable_arg: &ConcreteCallable, + hof_item_id: ItemId, +) -> ConcreteCallableKey { + match callable_arg { ConcreteCallable::Global { item_id, functor } => ConcreteCallableKey::Global { item_id: *item_id, functor: *functor, @@ -710,24 +859,167 @@ pub(crate) fn build_spec_key(call_site: &CallSite) -> SpecKey { ConcreteCallable::Closure { target, functor, .. } => ConcreteCallableKey::Closure { - target: StoreItemId::from((call_site.call_pkg_id, *target)), + target: StoreItemId::from((call_pkg_id, *target)), functor: *functor, + occurrence: None, }, - ConcreteCallable::Dynamic => { - // Dynamic callables are filtered out before reaching here, but - // provide a deterministic key regardless. - ConcreteCallableKey::Global { - item_id: call_site.hof_item_id, - functor: FunctorApp::default(), - } + ConcreteCallable::Dynamic => ConcreteCallableKey::Global { + item_id: hof_item_id, + functor: FunctorApp::default(), + }, + } +} + +/// Builds the deduplication key for a single call site's specialization. This +/// is the length-1 shim over [`build_combined_spec_key`]; single-arrow-param +/// HOF keys are therefore byte-identical to the pre-combined behavior. +pub(crate) fn build_spec_key(call_site: &CallSite) -> SpecKey { + build_combined_spec_key(call_site.hof_item_id, &[call_site]) +} + +/// Builds the combined deduplication key for a group of `Single`-resolved call +/// sites that share one `call_expr_id`, one per arrow parameter of the HOF. +/// +/// The group is sorted by `(top_level_param, field_path)` ascending so that the +/// resulting `concrete_args` ordering is deterministic and position-aligned +/// with the parameter order the specialize/rewrite sides consume. Distinct +/// argument combinations therefore map to distinct keys, while identical +/// combinations deduplicate to one specialization, including same-target +/// producer closures whose differing captures are not part of the key. +pub(crate) fn build_combined_spec_key(hof_id: ItemId, group: &[&CallSite]) -> SpecKey { + build_combined_spec_key_with_occurrences(hof_id, group, false) +} + +/// Builds the combined dedup key for a group, picking the right occurrence +/// policy for the group's shape. +/// +/// A normal multi-argument group has one member per distinct parameter +/// position, so occurrences never repeat. A *static callable-array* group is +/// the exception: several members fill the same array parameter position, and +/// those repeats must stay distinct in the key (see +/// [`build_static_callable_array_combined_spec_key`]). This dispatches to the +/// occurrence-preserving builder for array groups and the plain builder +/// otherwise. +pub(crate) fn build_combined_spec_key_for_group(hof_id: ItemId, group: &[&CallSite]) -> SpecKey { + if is_static_callable_array_combined_group(group) { + build_static_callable_array_combined_spec_key(hof_id, group) + } else { + build_combined_spec_key(hof_id, group) + } +} + +/// Builds the combined dedup key for a static callable-array group, keeping +/// each repeated array slot distinct. +/// +/// When several closures fill the same array-of-callable parameter, keying them +/// all identically (their captures are not part of the key) would collapse +/// distinct elements into one and lose the array's element ordering. Preserving +/// the per-position occurrence index keeps `[f, g, f]` distinct from `[f, f, g]`. +pub(crate) fn build_static_callable_array_combined_spec_key( + hof_id: ItemId, + group: &[&CallSite], +) -> SpecKey { + build_combined_spec_key_with_occurrences(hof_id, group, true) +} + +/// Shared implementation behind the combined-spec-key builders: turns a group +/// of call sites into one deterministic [`SpecKey`]. +/// +/// The members are sorted by parameter position so the key's argument order is +/// stable and aligns with what the specialize/rewrite phases consume. Each +/// member is then reduced to its concrete-callable key. +/// +/// `preserve_repeated_occurrences` controls how repeats at the same position +/// are keyed. Same-target closures normally key identically (their captures are +/// not part of the dispatch identity), so a plain multi-arg call +/// deduplicates them. For a static callable-array group that is wrong — the +/// array needs every element kept apart — so when the flag is set, positions +/// used more than once get an occurrence index stamped into their closure key. +/// +/// # Transformation +/// +/// ```text +/// // array position filled by three closures over the same target `f`: +/// // preserve_repeated_occurrences = false => [f, f, f] (collapses) +/// // preserve_repeated_occurrences = true => [f#0, f#1, f#2] (distinct) +/// ``` +fn build_combined_spec_key_with_occurrences( + hof_id: ItemId, + group: &[&CallSite], + preserve_repeated_occurrences: bool, +) -> SpecKey { + // Sort by parameter slot (and field path within it) so the resulting key is + // order-independent of how the call sites were discovered. + let mut members: Vec<&CallSite> = group.to_vec(); + members.sort_by(|a, b| { + a.top_level_param + .cmp(&b.top_level_param) + .then_with(|| a.field_path.cmp(&b.field_path)) + }); + // For array groups, first tally how many members land on each position so we + // only bother stamping occurrence indices where a position actually repeats. + let mut position_counts: FxHashMap<(usize, Vec), usize> = FxHashMap::default(); + if preserve_repeated_occurrences { + for cs in &members { + *position_counts + .entry((cs.top_level_param, cs.field_path.clone())) + .or_default() += 1; } - }; + } + // Running per-position counter used to hand out 0, 1, 2, ... to repeats. + let mut occurrences: FxHashMap<(usize, Vec), usize> = FxHashMap::default(); + let concrete_args = members + .iter() + .map(|cs| { + let position = (cs.top_level_param, cs.field_path.clone()); + // Only assign an occurrence index when this is an array group and + // this position is used more than once; otherwise leave it `None` + // so ordinary calls keep their original (dedup-friendly) keys. + let occurrence = (preserve_repeated_occurrences + && position_counts.get(&position).copied().unwrap_or_default() > 1) + .then(|| { + let next = occurrences.entry(position).or_default(); + let value = *next; + *next += 1; + value + }); + // Reduce the argument to its dedup key, then stamp the occurrence + // index into it when the value is a closure. + let mut key = concrete_callable_key(cs.call_pkg_id, &cs.callable_arg, cs.hof_item_id); + if let ConcreteCallableKey::Closure { + occurrence: slot, .. + } = &mut key + { + *slot = occurrence; + } + key + }) + .collect(); SpecKey { - hof_id: StoreItemId::from((call_site.hof_item_id.package, call_site.hof_item_id.item)), - concrete_args: vec![concrete_key], + hof_id: StoreItemId::from((hof_id.package, hof_id.item)), + concrete_args, } } +/// Reports whether a group is a *static callable-array* group: two or more +/// members filling the exact same parameter position. +/// +/// Under normal combined specialization each member occupies a distinct +/// position, so a repeat signals the callable-array case — several static +/// elements supplied for one array-of-callable parameter — which needs the +/// occurrence-preserving key so its elements are not collapsed. +pub(crate) fn is_static_callable_array_combined_group(group: &[&CallSite]) -> bool { + // Count members per position; any position hit twice or more means the same + // slot is being filled repeatedly, i.e. a static callable array. + let mut positions: FxHashMap<(usize, Vec), usize> = FxHashMap::default(); + for call_site in group { + *positions + .entry((call_site.top_level_param, call_site.field_path.clone())) + .or_default() += 1; + } + positions.values().any(|count| *count >= 2) +} + /// Builds the index path from a call's argument tuple to the position of /// a callable parameter, accounting for functor control wrappers and /// tuple-patterned inputs. @@ -743,3 +1035,465 @@ pub(crate) fn build_param_input_path( path.extend(param.field_path.iter().copied()); path } + +/// Determines whether a group of call sites that share one call expression +/// forms a genuine multi-argument higher-order call eligible for combined +/// specialization, where every arrow parameter is specialized together against +/// one clone in a single fixpoint iteration. +/// +/// Both the specialize and rewrite phases consult this predicate so they agree +/// on exactly which call sites are combined. Any disagreement would strand a +/// combined specialization without a matching call-site rewrite, or a rewrite +/// without its specialization. A group qualifies only when all of the following +/// hold: +/// +/// - it has at least two members. A single arrow parameter stays on the per-row +/// path, byte-identical to the pre-combined behavior. +/// - every member resolves a static callable with no branch condition and is +/// not `Dynamic`, so branch-split candidate sets keep their dispatch path. +/// - every member supplies a callable for a distinct parameter position, which +/// is its top-level slot plus the field path into any nested tuple. This makes +/// the group a genuine multi-argument call rather than a branch-split +/// candidate set that resolves the same parameter many ways. +/// - the call carries no outer controlled functor, whose nested argument tuple +/// the top-level combined removal does not model. +/// - every nested member, meaning one that selects an arrow field of a +/// tuple-valued parameter, is single-level, and the group covers every field +/// of that parameter's tuple, so the combined removal can drop the whole +/// top-level slot. Partial field coverage such as a surviving non-arrow +/// element, deeper nesting, or a slot whose type does not resolve to a direct +/// tuple keeps the call on the per-row path. +/// +/// `package` must own `group`'s shared call expression. +pub(super) fn is_combined_eligible(package: &Package, group: &[&CallSite]) -> bool { + if group.len() < 2 { + return false; + } + if group + .iter() + .any(|s| !s.condition.is_empty() || matches!(s.callable_arg, ConcreteCallable::Dynamic)) + { + return false; + } + // Distinct parameter positions mean a genuine multi-argument call rather + // than a branch-split candidate set that resolves the same parameter many + // ways. Static candidates for one array-of-arrow parameter are the one + // exception: the array index lives inside the HOF body, so one clone needs + // all candidates in order to synthesize the in-body dispatch. + let static_callable_array_group = is_static_callable_array_group(package, group) + || has_static_top_level_callable_array_position(package, group); + let mut param_positions: Vec<(usize, &[usize])> = group + .iter() + .map(|s| (s.top_level_param, s.field_path.as_slice())) + .collect(); + param_positions.sort_unstable(); + if !param_positions.windows(2).all(|w| w[0] != w[1]) && !static_callable_array_group { + return false; + } + // An outer controlled functor nests the argument tuple one level per + // control layer; the combined top-level removal does not model that + // nesting, so such calls stay on the per-row path. + let call_expr = package.get_expr(group[0].call_expr_id); + let ExprKind::Call(callee_id, _) = call_expr.kind else { + return false; + }; + let (_, functor) = peel_body_functors(package, callee_id); + if functor.controlled != 0 { + return false; + } + if static_callable_array_group { + return true; + } + // A member selects either a top-level arrow parameter, identified by an + // empty field path, or a single immediate arrow field of a tuple-valued + // parameter. The combined removal drops a whole top-level slot, so a nested + // member is only eligible when its group covers every field of that slot's + // tuple; otherwise the surviving fields would be dropped along with the + // removed ones. + let Ty::Arrow(ref arrow) = package.get_expr(callee_id).ty else { + return false; + }; + let mut nested_fields: FxHashMap> = FxHashMap::default(); + let mut uses_tuple_input = false; + for s in group { + match s.field_path.as_slice() { + [] => {} + [field] => { + uses_tuple_input = s.hof_input_is_tuple; + nested_fields + .entry(s.top_level_param) + .or_default() + .push(*field); + } + // Deeper nesting is not modeled by the single-level combined + // removal, so the whole group stays on the per-row path. + _ => return false, + } + } + let arrow_input = resolve_udt_ty(package, &arrow.input); + for (slot, mut fields) in nested_fields { + // For a multi-parameter HOF the arrow input is a tuple of parameters + // and the tuple-valued parameter sits at `slot`; for a single + // tuple-valued parameter the arrow input is that tuple. + let container = if uses_tuple_input { + match &arrow_input { + Ty::Tuple(tys) => tys.get(slot), + _ => None, + } + } else { + Some(&arrow_input) + }; + let Some(Ty::Tuple(slot_tys)) = container else { + return false; + }; + fields.sort_unstable(); + fields.dedup(); + if fields.len() != slot_tys.len() { + return false; + } + } + true +} + +/// Reports whether `group` is a set of static candidates for a single +/// array-of-callable parameter (`(Qubit => Unit)[]` and the like). +/// +/// Unlike [`is_static_callable_array_combined_group`], which only counts +/// repeated positions, this also confirms two things: that every member truly +/// sits at the *same* parameter position (and is a clean static candidate — no +/// branch condition, not `Dynamic`), and that the type at that position +/// resolves to an `Array` whose element is an `Arrow`. Those extra checks are +/// why this is used for combined-eligibility, where the actual element type +/// matters, rather than the cheap positional pre-check. +/// +/// The members describe the elements of one forwarded callable array, so the +/// group specializes to a single clone that dispatches on the array index +/// inside the HOF body. +fn is_static_callable_array_group(package: &Package, group: &[&CallSite]) -> bool { + // Take the first member as the reference position; an empty group is not an + // array group. + let Some(first) = group.first() else { + return false; + }; + // Every member must fill the exact same parameter slot with a clean static + // candidate. Any position mismatch, branch condition, or `Dynamic` value + // means this is not a single-array candidate set. + if group.iter().any(|call_site| { + call_site.top_level_param != first.top_level_param + || call_site.field_path != first.field_path + || call_site.hof_input_is_tuple != first.hof_input_is_tuple + || !call_site.condition.is_empty() + || matches!(call_site.callable_arg, ConcreteCallable::Dynamic) + }) { + return false; + } + + // Recover the HOF's arrow type so we can inspect the type sitting at the + // shared parameter position. + let call_expr = package.get_expr(first.call_expr_id); + let ExprKind::Call(callee_id, _) = call_expr.kind else { + return false; + }; + let Ty::Arrow(ref arrow) = package.get_expr(callee_id).ty else { + return false; + }; + + // Descend to the container type at the top-level slot: for a tuple-input HOF + // that is the element at `top_level_param`; otherwise the whole input is the + // single parameter. + let arrow_input = resolve_udt_ty(package, &arrow.input); + let container = if first.hof_input_is_tuple { + match &arrow_input { + Ty::Tuple(tys) => tys.get(first.top_level_param), + _ => None, + } + } else { + Some(&arrow_input) + }; + + let Some(container) = container else { + return false; + }; + // Follow the field path into any nested tuple to reach the exact selected + // type; a non-tuple hop along the way disqualifies the group. + let selected_ty = first + .field_path + .iter() + .try_fold(container, |ty, index| match ty { + Ty::Tuple(tys) => tys.get(*index), + _ => None, + }); + // The group is a static callable array only if that type is an array of + // arrows. + matches!(selected_ty, Some(Ty::Array(item_ty)) if matches!(item_ty.as_ref(), Ty::Arrow(_))) +} + +/// Returns every repeated top-level position in `group`, regardless of the +/// forwarded value's type. +/// +/// A position is `(top_level_param, field_path)`; it is *repeated* when two or +/// more members populate it. This is the raw grouping the callable-array +/// analysis builds on before any type filter, letting callers reason about +/// *all* repeated positions rather than only the callable-array ones. +/// +/// Returns an empty vector when any member carries a branch condition or a +/// `Dynamic` callable, matching the fail-fast guard the single-array +/// eligibility check applied before this shared analysis was factored out. +fn repeated_top_level_positions(group: &[&CallSite]) -> Vec<(usize, Vec)> { + let mut candidates_per_position: FxHashMap<(usize, Vec), usize> = FxHashMap::default(); + for call_site in group { + if !call_site.condition.is_empty() + || matches!(call_site.callable_arg, ConcreteCallable::Dynamic) + { + return Vec::new(); + } + *candidates_per_position + .entry((call_site.top_level_param, call_site.field_path.clone())) + .or_default() += 1; + } + + let mut positions: Vec<(usize, Vec)> = candidates_per_position + .into_iter() + .filter(|(_, count)| *count >= 2) + .map(|(position, _)| position) + .collect(); + positions.sort_unstable(); + positions +} + +/// Returns every repeated top-level position in `group` whose forwarded value +/// resolves to an array of callables (`Ty::Array` of `Ty::Arrow`). +/// +/// A position is `(top_level_param, field_path)`; it is *repeated* when two or +/// more members supply a callable for it, which is how the analysis records the +/// elements of one forwarded callable array. A single such position is the +/// supported single-array shape; two or more mean two distinct callable arrays +/// are forwarded through the same call, which the combined removal does not +/// model. +/// +/// This layers the `Array(Arrow)` type filter on top of +/// [`repeated_top_level_positions`], so it inherits the same branch-condition +/// and `Dynamic` fail-fast guard. +/// +/// `package` must own `group`'s shared call expression. +pub(super) fn static_callable_array_positions( + package: &Package, + group: &[&CallSite], +) -> Vec<(usize, Vec)> { + let positions = repeated_top_level_positions(group); + if positions.is_empty() { + return Vec::new(); + } + + let call_expr = package.get_expr(group[0].call_expr_id); + let ExprKind::Call(callee_id, _) = call_expr.kind else { + return Vec::new(); + }; + let Ty::Arrow(ref arrow) = package.get_expr(callee_id).ty else { + return Vec::new(); + }; + let arrow_input = resolve_udt_ty(package, &arrow.input); + + // Filtering the already-sorted `positions` preserves the sort order, so the + // result stays sorted like the pre-refactor implementation guaranteed. + positions + .into_iter() + .filter(|(top_level_param, field_path)| { + let container = if group[0].hof_input_is_tuple { + match &arrow_input { + Ty::Tuple(input_tys) => input_tys.get(*top_level_param), + _ => None, + } + } else { + Some(&arrow_input) + }; + let Some(container) = container else { + return false; + }; + let selected_ty = field_path.iter().try_fold(container, |ty, index| match ty { + Ty::Tuple(tys) => tys.get(*index), + _ => None, + }); + matches!( + selected_ty, + Some(Ty::Array(item_ty)) if matches!(item_ty.as_ref(), Ty::Arrow(_)) + ) + }) + .collect() +} + +/// Returns `true` only when `group` has **exactly one repeated top-level +/// position across all types** and that single position is a callable array. +/// +/// The combined single-array removal models exactly one forwarded callable +/// array, so the eligibility check is deliberately strict. Requiring the full +/// [`repeated_top_level_positions`] set (any type) to hold a single element — +/// rather than only counting the callable-array positions from +/// [`static_callable_array_positions`] — rejects a group that also repeats a +/// second, non-callable-array position. Such a second repeated position (of +/// *any* type) carries state the combined path cannot represent, so the group +/// must stay on the per-row path. This preserves the pre-refactor behavior, +/// where the `Array(Arrow)` type filter was applied only *after* the +/// exactly-one-repeated-position check. +fn has_static_top_level_callable_array_position(package: &Package, group: &[&CallSite]) -> bool { + let repeated_positions = repeated_top_level_positions(group); + let [position] = repeated_positions.as_slice() else { + return false; + }; + if !static_callable_array_positions(package, group).contains(position) { + return false; + } + if !position.1.is_empty() + && group.iter().any(|call_site| { + call_site.top_level_param != position.0 || call_site.field_path.is_empty() + }) + { + return false; + } + true +} + +/// Returns `true` when `group` forwards two or more distinct callable arrays +/// through a single higher-order-function call, meaning two or more repeated +/// top-level positions each resolve to an array of callables. +/// +/// This shape is not supported by the single-array combined removal: leaving it +/// on the per-row path would silently collapse each multi-candidate array to a +/// single member, so the specialization driver rejects it with a hard +/// diagnostic instead. +pub(super) fn has_multiple_forwarded_callable_arrays( + package: &Package, + group: &[&CallSite], +) -> bool { + static_callable_array_positions(package, group).len() >= 2 +} + +fn resolve_udt_ty(package: &Package, ty: &Ty) -> Ty { + match ty { + Ty::Udt(Res::Item(item_id)) => { + let Some(item) = package.items.get(item_id.item) else { + return ty.clone(); + }; + let ItemKind::Ty(_, udt) = &item.kind else { + return ty.clone(); + }; + resolve_udt_ty(package, &udt.get_pure_ty()) + } + Ty::Tuple(elems) => Ty::Tuple( + elems + .iter() + .map(|elem| resolve_udt_ty(package, elem)) + .collect(), + ), + Ty::Array(elem) => Ty::Array(Box::new(resolve_udt_ty(package, elem))), + Ty::Arrow(arrow) => Ty::Arrow(Box::new(qsc_fir::ty::Arrow { + kind: arrow.kind, + input: Box::new(resolve_udt_ty(package, &arrow.input)), + output: Box::new(resolve_udt_ty(package, &arrow.output)), + functors: arrow.functors, + })), + _ => ty.clone(), + } +} + +/// Splits a per-row group that shares one call expression into the parameter +/// that is dispatched over several candidates and its single-valued sibling +/// parameters, when the group has the mixed branch-split shape that the +/// combined per-candidate specialization handles. +/// +/// Returns `Some((dispatch_candidates, constants))` only when all of the +/// following hold: +/// +/// - exactly one parameter position, meaning a top-level slot plus field path, +/// carries two or more candidates. This is the dispatched parameter, for +/// example `f = [H, X]`. +/// - at least one member sits at a different position. These are the +/// single-valued siblings, for example `g = Make(0.5)` and `h = Z`. +/// - at least one sibling is a producer `Closure`. This is the case the per-row +/// path compiles incorrectly; sibling globals alone keep the +/// restricted-dispatch path, which already threads them as runtime arguments. +/// - no sibling is `Dynamic`. An unresolved sibling cannot be specialized +/// together and must surface its own `DynamicCallable` diagnostic. +/// +/// `dispatch_candidates` are every member at the dispatched position, with their +/// conditions preserved; `constants` are every other member. Both the +/// specialize and rewrite phases consult this predicate so they agree on which +/// groups route through the combined per-candidate specializations. +pub(super) fn partition_mixed_branch_split<'a>( + group: &[&'a CallSite], +) -> Option<(Vec<&'a CallSite>, Vec<&'a CallSite>)> { + let mut candidates_per_position: FxHashMap<(usize, Vec), usize> = FxHashMap::default(); + for cs in group { + *candidates_per_position + .entry((cs.top_level_param, cs.field_path.clone())) + .or_default() += 1; + } + let dispatched_positions: Vec<(usize, Vec)> = candidates_per_position + .iter() + .filter(|(_, count)| **count >= 2) + .map(|(position, _)| position.clone()) + .collect(); + if dispatched_positions.len() != 1 { + return None; + } + let dispatch_position = &dispatched_positions[0]; + let dispatch: Vec<&CallSite> = group + .iter() + .copied() + .filter(|cs| (cs.top_level_param, cs.field_path.clone()) == *dispatch_position) + .collect(); + let constants: Vec<&CallSite> = group + .iter() + .copied() + .filter(|cs| (cs.top_level_param, cs.field_path.clone()) != *dispatch_position) + .collect(); + if constants.is_empty() { + return None; + } + if constants + .iter() + .any(|cs| matches!(cs.callable_arg, ConcreteCallable::Dynamic)) + { + return None; + } + if !constants + .iter() + .any(|cs| matches!(cs.callable_arg, ConcreteCallable::Closure { .. })) + { + return None; + } + Some((dispatch, constants)) +} + +/// Consistency-check predicate: returns `true` when `cs` is a single-valued +/// producer-closure sibling, meaning its own parameter position carries exactly +/// one candidate, of a parameter that is dispatched over several candidates at a +/// different position with two or more candidates within `group`. +/// +/// This is exactly the shape whose producer body `track_specialized_closures` +/// must not record as consumed before the combined per-candidate specialization +/// removes it from the live call sites. Recording it clears the producer body +/// while the dispatched siblings still reference it, reintroducing the incorrect +/// output. The combined specialization prevents the per-row specialization that +/// triggers the recording, so this predicate is only true when that +/// specialization did not run. +fn closure_constant_sibling_of_dispatch(group: &[&CallSite], cs: &CallSite) -> bool { + if !matches!(cs.callable_arg, ConcreteCallable::Closure { .. }) { + return false; + } + let own_position = (cs.top_level_param, cs.field_path.clone()); + let mut candidates_per_position: FxHashMap<(usize, Vec), usize> = FxHashMap::default(); + for member in group { + *candidates_per_position + .entry((member.top_level_param, member.field_path.clone())) + .or_default() += 1; + } + let own_count = candidates_per_position + .get(&own_position) + .copied() + .unwrap_or(0); + let has_other_dispatched_position = candidates_per_position + .iter() + .any(|(position, count)| *position != own_position && *count >= 2); + own_count == 1 && has_other_dispatched_position +} diff --git a/source/compiler/qsc_fir_transforms/src/defunctionalize/analysis.rs b/source/compiler/qsc_fir_transforms/src/defunctionalize/analysis.rs index 8dc67fa3c04..ff512ac2b2d 100644 --- a/source/compiler/qsc_fir_transforms/src/defunctionalize/analysis.rs +++ b/source/compiler/qsc_fir_transforms/src/defunctionalize/analysis.rs @@ -32,11 +32,13 @@ use crate::fir_builder::functored_specs; use qsc_data_structures::functors::FunctorApp; use qsc_data_structures::span::Span; use qsc_fir::fir::{ - BinOp, BlockId, CallableImpl, ExprId, ExprKind, Field, FieldAssign, FieldPath, ItemId, - ItemKind, Lit, LocalVarId, Mutability, Package, PackageId, PackageLookup, PackageStore, PatId, - PatKind, Res, SpecImpl, StmtKind, StoreItemId, StringComponent, UnOp, + BinOp, Block, BlockId, CallableImpl, CallableKind, Expr, ExprId, ExprKind, Field, FieldAssign, + FieldPath, ItemId, ItemKind, Lit, LocalVarId, Mutability, Package, PackageId, PackageLookup, + PackageStore, Pat, PatId, PatKind, Res, SpecImpl, Stmt, StmtId, StmtKind, StoreItemId, + StringComponent, UnOp, }; use qsc_fir::ty::Ty; +use qsc_fir::visit::{self, Visitor}; use rustc_hash::{FxHashMap, FxHashSet}; /// Combined local variable state for the analysis phase. @@ -102,6 +104,18 @@ fn find_callable_params( let pkg = store.get(store_id.package); let item = pkg.get_item(store_id.item); if let ItemKind::Callable(decl) = &item.kind { + // An intrinsic callable has no body for the pass to rewrite, so its + // callable parameters can never be invoked in a way that could be + // specialized. Treating one as a higher-order function and dropping + // the parameter would corrupt intrinsics that consume the argument + // as data rather than invoking it (for example `Length`, whose + // element type can monomorphize to a callable array): the parameter + // would be removed from the signature while call sites still pass + // the argument. Skip intrinsics so their callable arguments survive + // unchanged. + if matches!(decl.implementation, CallableImpl::Intrinsic) { + continue; + } let params = extract_arrow_params(store, pkg, store_id, decl.input); if !params.is_empty() { result.insert(store_id, params); @@ -202,6 +216,17 @@ fn extract_arrow_params_from_ty( field_path.pop(); } } + Ty::Array(item_ty) if matches!(item_ty.as_ref(), Ty::Arrow(_)) => { + params.push(CallableParam::new( + context.callable_id, + context.param_pat_id, + context.top_level_param, + field_path.clone(), + context.param_var, + param_ty.clone(), + context.hof_input_is_tuple, + )); + } Ty::Udt(Res::Item(item_id)) => { let package = context.store.get(item_id.package); let item = package.get_item(item_id.item); @@ -367,69 +392,18 @@ fn inspect_call_expr( if let Some((hof_store_id, hof_functor, hof_callable_params)) = resolve_hof_callee(pkg, *callee_expr_id, hof_params) { - let uses_tuple_input = hof_uses_tuple_input_pattern(store, hof_store_id); - for cp in hof_callable_params { - let input_path = super::build_param_input_path(uses_tuple_input, cp, hof_functor); - let resolved_arg_id = extract_arg_at_path(pkg, *args_expr_id, &input_path); - let allow_scoped_capture_exprs = matches!( - pkg.get_expr(resolved_arg_id).kind, - ExprKind::Block(_) | ExprKind::If(_, _, _) - ); - let resolved = resolve_callee_at_path( - pkg, - store, - locals, - *args_expr_id, - &input_path, - 0, - allow_scoped_capture_exprs, - &FxHashSet::default(), - package_id, - ); - match resolved { - CalleeLattice::Single(cc) => { - call_sites.push(CallSite { - call_expr_id: expr_id, - call_pkg_id: package_id, - hof_item_id: ItemId { - package: hof_store_id.package, - item: hof_store_id.item, - }, - callable_arg: cc, - arg_expr_id: resolved_arg_id, - condition: vec![], - }); - } - CalleeLattice::Multi(candidates) => { - for (cc, cond) in candidates { - call_sites.push(CallSite { - call_expr_id: expr_id, - call_pkg_id: package_id, - hof_item_id: ItemId { - package: hof_store_id.package, - item: hof_store_id.item, - }, - callable_arg: cc, - arg_expr_id: resolved_arg_id, - condition: cond, - }); - } - } - CalleeLattice::Dynamic | CalleeLattice::Bottom => { - call_sites.push(CallSite { - call_expr_id: expr_id, - call_pkg_id: package_id, - hof_item_id: ItemId { - package: hof_store_id.package, - item: hof_store_id.item, - }, - callable_arg: ConcreteCallable::Dynamic, - arg_expr_id: resolved_arg_id, - condition: vec![], - }); - } - } - } + record_hof_call_sites( + store, + pkg, + expr_id, + *args_expr_id, + locals, + hof_store_id, + hof_functor, + hof_callable_params, + call_sites, + package_id, + ); return; } @@ -488,6 +462,101 @@ fn inspect_call_expr( ); } +/// Records a [`CallSite`] for every arrow parameter of a resolved HOF callee. +/// +/// For each callable parameter of the HOF, the argument at the parameter's +/// input path is resolved to its reaching-definitions lattice: a single +/// concrete callable yields one unconditional call site, a `Multi` lattice +/// yields one conditioned call site per candidate (the branch-split set), and a +/// dynamic or bottom lattice yields a single dynamic call site so the pass +/// surfaces an honest diagnostic later. +#[allow(clippy::too_many_arguments)] +fn record_hof_call_sites( + store: &PackageStore, + pkg: &Package, + expr_id: ExprId, + args_expr_id: ExprId, + locals: &LocalState, + hof_store_id: StoreItemId, + hof_functor: FunctorApp, + hof_callable_params: &[CallableParam], + call_sites: &mut Vec, + package_id: PackageId, +) { + let uses_tuple_input = hof_uses_tuple_input_pattern(store, hof_store_id); + for cp in hof_callable_params { + let input_path = super::build_param_input_path(uses_tuple_input, cp, hof_functor); + let resolved_arg_id = extract_arg_at_path(pkg, args_expr_id, &input_path); + let allow_scoped_capture_exprs = matches!( + pkg.get_expr(resolved_arg_id).kind, + ExprKind::Block(_) | ExprKind::If(_, _, _) + ); + let resolved = resolve_callee_at_path( + pkg, + store, + locals, + args_expr_id, + &input_path, + 0, + allow_scoped_capture_exprs, + &FxHashSet::default(), + package_id, + ); + match resolved { + CalleeLattice::Single(cc) => { + call_sites.push(CallSite { + call_expr_id: expr_id, + call_pkg_id: package_id, + hof_item_id: ItemId { + package: hof_store_id.package, + item: hof_store_id.item, + }, + top_level_param: cp.top_level_param, + field_path: cp.field_path.clone(), + hof_input_is_tuple: cp.hof_input_is_tuple, + callable_arg: cc, + arg_expr_id: resolved_arg_id, + condition: vec![], + }); + } + CalleeLattice::Multi(candidates) => { + for (cc, cond) in candidates { + call_sites.push(CallSite { + call_expr_id: expr_id, + call_pkg_id: package_id, + hof_item_id: ItemId { + package: hof_store_id.package, + item: hof_store_id.item, + }, + top_level_param: cp.top_level_param, + field_path: cp.field_path.clone(), + hof_input_is_tuple: cp.hof_input_is_tuple, + callable_arg: cc, + arg_expr_id: resolved_arg_id, + condition: cond, + }); + } + } + CalleeLattice::Dynamic | CalleeLattice::Bottom => { + call_sites.push(CallSite { + call_expr_id: expr_id, + call_pkg_id: package_id, + hof_item_id: ItemId { + package: hof_store_id.package, + item: hof_store_id.item, + }, + top_level_param: cp.top_level_param, + field_path: cp.field_path.clone(), + hof_input_is_tuple: cp.hof_input_is_tuple, + callable_arg: ConcreteCallable::Dynamic, + arg_expr_id: resolved_arg_id, + condition: vec![], + }); + } + } + } +} + /// Returns `true` when an expression subtree contains an `ExprKind::Hole` /// placeholder, which marks partial applications that the pass does not /// yet specialize. @@ -668,10 +737,15 @@ fn extract_arg_at_path(pkg: &Package, args_expr_id: ExprId, path: &[usize]) -> E } let args_expr = pkg.get_expr(args_expr_id); if let ExprKind::Tuple(elements) = &args_expr.kind { - if path.len() == 1 { - elements[path[0]] - } else { - extract_arg_at_path(pkg, elements[path[0]], &path[1..]) + // Defensive `.get()` mirrors `resolve_callee_at_path`, which walks the + // same path over the same argument expression: an out-of-range index + // falls back to the whole argument rather than panicking, keeping the + // two path walkers in lockstep instead of one crashing where the other + // degrades. + match elements.get(path[0]) { + Some(&element_id) if path.len() == 1 => element_id, + Some(&element_id) => extract_arg_at_path(pkg, element_id, &path[1..]), + None => args_expr_id, } } else { // Single-parameter callable: the args expression IS the argument. @@ -698,6 +772,25 @@ fn resolve_callee_at_path( } if path.is_empty() { + if matches!(pkg.get_expr(args_expr_id).ty, Ty::Array(_)) + && let Some(candidates) = resolve_indexed_callable_candidates( + pkg, + store, + locals, + args_expr_id, + depth + 1, + allow_scoped_capture_exprs, + scoped_capture_vars, + package_id, + ) + { + return CalleeLattice::Multi( + candidates + .into_iter() + .map(|callable| (callable, vec![])) + .collect(), + ); + } return resolve_callee( pkg, store, @@ -731,6 +824,26 @@ fn resolve_callee_at_path( indices: path.to_vec(), }; if let Some(field_value_id) = resolve_struct_field(pkg, locals, args_expr_id, &field_path, 0) { + if matches!(pkg.get_expr(field_value_id).ty, Ty::Array(_)) + && let Some(candidates) = resolve_indexed_callable_candidates( + pkg, + store, + locals, + field_value_id, + depth + 1, + allow_scoped_capture_exprs, + scoped_capture_vars, + package_id, + ) + { + return CalleeLattice::Multi( + candidates + .into_iter() + .map(|callable| (callable, vec![])) + .collect(), + ); + } + return resolve_callee( pkg, store, @@ -755,9 +868,11 @@ fn resolve_callee_at_path( ) } -/// Resolves an expression to a [`CalleeLattice`] by peeling functor -/// applications, following single-assignment immutable locals, resolving -/// if-value-expressions, and recognising closures and global item references. +/// Resolves a callee expression to its reaching-definitions lattice of concrete +/// callables by peeling functor wrappers, following single-assignment immutable +/// locals, resolving if-value-expressions, recognising closures and global item +/// references, and tracing same-package callable returns — up to a recursion +/// depth limit. #[allow( clippy::only_used_in_recursion, clippy::too_many_lines, @@ -1030,6 +1145,45 @@ fn resolve_callee( } /// Resolves a callable nested at `path` inside an aggregate expression. +/// +/// Where [`resolve_callee`] resolves an expression that *is* a callable, +/// this function resolves a callable that is *inside* an expression at a +/// tuple/struct field path — e.g. the `.op` field of a UDT, or element `[1]` +/// of a tuple. The distinction matters because the aggregate itself is not +/// callable; only a specific field within it is. +/// +/// # Why this exists separately from `resolve_callee` +/// +/// A `Field(inner, path)` expression first attempts direct struct-field +/// resolution via [`resolve_struct_field`] (which finds the initializer when +/// the aggregate is a literal construction). When that fast path fails — +/// because the aggregate flows through a local, a block tail, an `if` +/// branch, or a same-package callable return — this function recursively +/// *projects* into the intermediate expression kinds to locate the callable +/// at the requested path. It handles tuples, locals, blocks, `if`/`else`, +/// calls (both callable-returning functions and UDT constructors), struct +/// literals, and nested field accesses. +/// +/// # Key semantic difference: `if` branches are never literal-folded +/// +/// Unlike `resolve_callee`'s `If` arm (which folds a constant condition to +/// select a single branch), this function always joins both branches into a +/// `CalleeLattice::Multi`. Folding would leave the unselected branch's +/// closure target unregistered for specialization, and +/// `cleanup_consumed_closures` would be unable to neutralize the surviving +/// `ExprKind::Closure` node, breaking fixpoint convergence. The resulting +/// `Multi` is materialized as a constant-conditioned dispatch by the rewrite +/// phase's `branch_split_direct_call_rewrite`. +/// +/// # Callers +/// +/// - [`resolve_callee`] — when a `Field(inner, Path)` has no direct struct +/// resolution. +/// - [`resolve_callable_return`] — to trace a callable through the return +/// value of a same-package function along an `output_path`. +/// - [`bind_callable_pat_projections`] — to resolve arrow-typed sub-bindings +/// in destructuring patterns by indexing into the initializer along a +/// field path. #[allow(clippy::too_many_arguments, clippy::too_many_lines)] fn resolve_callee_projection( pkg: &Package, @@ -1291,6 +1445,8 @@ fn resolve_callee_projection( } } +/// Reports whether following `path` into the (possibly nested tuple) type `ty` +/// lands on an arrow type. fn output_path_resolves_to_arrow(store: &PackageStore, ty: &Ty, path: &[usize]) -> bool { match ty { Ty::Arrow(_) => path.is_empty(), @@ -1314,17 +1470,16 @@ fn output_path_resolves_to_arrow(store: &PackageStore, ty: &Ty, path: &[usize]) } } -/// Attempts to resolve a callable-returning call whose target lives in the -/// same package by treating the target body as a straight-line function, -/// binding its parameters to the call's argument expressions and tracing -/// the result back to a concrete callable. -#[allow(clippy::too_many_arguments)] /// Resolves the callable value returned by a (possibly cross-package) callable -/// invoked at a call site. The callee's body is read from its owning package -/// (`item_id.package`), while the call arguments and caller lattice come from -/// the caller's package (`pkg` / `package_id`). The returned closure's capture -/// expressions therefore remain caller-package nodes, which is what the call -/// site rewrite consumes. +/// invoked at a call site by treating the target body as a straight-line +/// function, binding its parameters to the call's argument expressions and +/// tracing the result back to a concrete callable. +/// +/// The callee's body is read from its owning package (`item_id.package`), while +/// the call arguments and caller lattice come from the caller's package +/// (`pkg` / `package_id`). The returned closure's capture expressions therefore +/// remain caller-package nodes, which is what the call site rewrite consumes. +#[allow(clippy::too_many_arguments)] fn resolve_callable_return( pkg: &Package, store: &PackageStore, @@ -1453,6 +1608,8 @@ fn downgrade_closures_to_dynamic(lattice: CalleeLattice) -> CalleeLattice { } } +/// Resolves a branch-guard variable to a constant boolean, if analysis recorded +/// a substitution that fixes its value. fn resolve_condition_literal( pkg: &Package, locals: &LocalState, @@ -1477,6 +1634,8 @@ fn resolve_condition_literal( } } +/// Follows recorded substitutions and local definitions to resolve an +/// expression to a constant boolean, up to a recursion depth limit. fn resolve_condition_substitution_literal( pkg: &Package, locals: &LocalState, @@ -1501,6 +1660,9 @@ fn resolve_condition_substitution_literal( } } +/// Rewrites a guard expression to its recorded substitution so a guard captured +/// in one scope is expressed with values available at the dispatch site. +/// Returns the original id when no substitution applies. fn remap_condition_expr(pkg: &Package, locals: &LocalState, expr_id: ExprId) -> ExprId { let expr = pkg.get_expr(expr_id); if let ExprKind::Var(Res::Local(var), _) = &expr.kind @@ -1569,6 +1731,43 @@ fn materialize_capture_exprs_in_callable( resolve_capture_to_caller(pkg, state, param_substitutions, capture.var) { capture.expr = Some(expr); + // A resolved capture whose terminal is a producer-scope + // compound literal (struct/tuple/array constructor) still + // references the producing function's parameters through its + // inner `Var(Res::Local(_))` leaves. Record the caller-scope + // substitution for each such leaf so rewrite can deep-clone + // the literal and rebind it entirely to caller-scope values, + // instead of splicing unbound producer-scope locals into the + // caller. + if is_compound_capture_literal(pkg, expr) { + let substitutions = collect_compound_capture_substitutions( + pkg, + state, + param_substitutions, + expr, + ); + // Rebuilding the captured literal in the caller is only + // safe when every producer leaf resolves to a + // caller-scope value. If any producer + // `Var(Res::Local)` leaf is left unresolved — a producer + // non-parameter local, or a leaf inside a kind we cannot + // safely remap such as a block, closure, assignment, or + // a non-pure operation call — it would be copied verbatim + // into the caller and break the `PostDefunc` + // local-variable consistency invariant. + // + // When that happens, decline the whole closure to a + // dynamic call site. `ConcreteCallable::Dynamic` is the + // "cannot specialize" signal, so the original dynamic + // dispatch is kept and a recoverable `DynamicCallable` + // diagnostic is emitted instead of panicking. On the + // base profile that diagnostic is a hard error, which is + // preferable to generating incorrect code. + if compound_literal_has_residual_leak(pkg, &substitutions, expr) { + return ConcreteCallable::Dynamic; + } + capture.caller_substitutions = substitutions; + } } } @@ -1616,6 +1815,433 @@ fn resolve_capture_to_caller( None } +/// Reports whether an expression is a compound-literal capture terminal: a +/// struct, tuple, or array constructor whose sub-exprs may reference the +/// producing function's parameters. Such a terminal cannot be spliced into the +/// caller verbatim; its inner producer-parameter leaves must be remapped to +/// caller-scope values first (see [`collect_compound_capture_substitutions`]). +fn is_compound_capture_literal(pkg: &Package, expr_id: ExprId) -> bool { + matches!( + pkg.get_expr(expr_id).kind, + ExprKind::Struct(..) + | ExprKind::Tuple(_) + | ExprKind::Array(_) + | ExprKind::ArrayLit(_) + | ExprKind::ArrayRepeat(..) + ) +} + +/// Collects the caller-scope substitutions needed to reconstruct a +/// producer-scope compound-literal capture in the caller. +/// +/// Recurses through the safe, referentially-transparent, value-producing +/// expression kinds (compound containers — struct/tuple/array constructors — +/// plus pure `function` calls, binary/unary operators, field and index +/// accessors, index/field updates, and ranges) and, for each inner +/// `Var(Res::Local(var))` leaf, resolves `var` to its caller-scope argument +/// expression via [`resolve_capture_to_caller`]. Records `(var, caller_expr)` +/// whenever the leaf resolves to a distinct caller-scope expression, +/// de-duplicating by producer-parameter `LocalVarId`. Leaves that do not +/// resolve to a distinct caller-scope expression are left untouched. Kinds +/// outside the safe set are not recursed, so any producer leaf reachable only +/// through them is left for [`compound_literal_has_residual_leak`] to detect. +fn collect_compound_capture_substitutions( + pkg: &Package, + state: &LocalState, + param_substitutions: &FxHashMap, + expr_id: ExprId, +) -> Vec<(LocalVarId, ExprId)> { + let mut substitutions = Vec::new(); + collect_compound_capture_substitutions_into( + pkg, + state, + param_substitutions, + expr_id, + &mut substitutions, + ); + substitutions +} + +/// Recursive worker for [`collect_compound_capture_substitutions`] that walks +/// `expr_id` and appends resolved `(var, caller_expr)` pairs into +/// `substitutions`. +/// +/// Descends only through the safe, referentially-transparent, value-producing +/// expression kinds (compound containers — struct/tuple/array constructors — +/// plus pure `function` calls, binary/unary operators, field and index +/// accessors, index/field updates, and ranges). For each inner +/// `Var(Res::Local(var))` leaf it resolves `var` to its caller-scope argument +/// expression via [`resolve_capture_to_caller`] and records +/// `(var, caller_expr)` when the leaf resolves to a distinct caller-scope +/// expression not already recorded for that producer-parameter `LocalVarId`. +/// A `Call` is recursed only when its callee is a pure `function` +/// (via [`call_callee_is_pure_function`]); an `operation` callee is left in +/// place because relocating or duplicating it into caller-scope argument +/// construction would be unsound. Any other kind terminates the descent, so a +/// producer leaf reachable only through it is left for +/// [`compound_literal_has_residual_leak`] to detect. +#[allow(clippy::too_many_lines)] +fn collect_compound_capture_substitutions_into( + pkg: &Package, + state: &LocalState, + param_substitutions: &FxHashMap, + expr_id: ExprId, + substitutions: &mut Vec<(LocalVarId, ExprId)>, +) { + let expr = pkg.get_expr(expr_id); + match &expr.kind { + ExprKind::Var(Res::Local(var), _) => { + if let Some(caller_expr) = + resolve_capture_to_caller(pkg, state, param_substitutions, *var) + && caller_expr != expr_id + && !substitutions.iter().any(|(existing, _)| existing == var) + { + substitutions.push((*var, caller_expr)); + } + } + ExprKind::Tuple(elements) | ExprKind::Array(elements) | ExprKind::ArrayLit(elements) => { + for &elem in elements { + collect_compound_capture_substitutions_into( + pkg, + state, + param_substitutions, + elem, + substitutions, + ); + } + } + ExprKind::ArrayRepeat(value, size) => { + collect_compound_capture_substitutions_into( + pkg, + state, + param_substitutions, + *value, + substitutions, + ); + collect_compound_capture_substitutions_into( + pkg, + state, + param_substitutions, + *size, + substitutions, + ); + } + ExprKind::Struct(_, copy, fields) => { + if let Some(copy_id) = copy { + collect_compound_capture_substitutions_into( + pkg, + state, + param_substitutions, + *copy_id, + substitutions, + ); + } + for field in fields { + collect_compound_capture_substitutions_into( + pkg, + state, + param_substitutions, + field.value, + substitutions, + ); + } + } + // A `Call` is only referentially transparent when its callee is a pure + // `function`; an `operation` callee may carry observable side effects + // and ordering, so relocating/duplicating the call into caller-scope + // arg construction is unsound. Leave a non-pure call for the residual + // leak guard to decline. + ExprKind::Call(callee, arg) if call_callee_is_pure_function(pkg, *callee) => { + collect_compound_capture_substitutions_into( + pkg, + state, + param_substitutions, + *callee, + substitutions, + ); + collect_compound_capture_substitutions_into( + pkg, + state, + param_substitutions, + *arg, + substitutions, + ); + } + ExprKind::BinOp(_, lhs, rhs) => { + collect_compound_capture_substitutions_into( + pkg, + state, + param_substitutions, + *lhs, + substitutions, + ); + collect_compound_capture_substitutions_into( + pkg, + state, + param_substitutions, + *rhs, + substitutions, + ); + } + ExprKind::UnOp(_, operand) => { + collect_compound_capture_substitutions_into( + pkg, + state, + param_substitutions, + *operand, + substitutions, + ); + } + ExprKind::Field(base, _) => { + collect_compound_capture_substitutions_into( + pkg, + state, + param_substitutions, + *base, + substitutions, + ); + } + ExprKind::Index(base, index) => { + collect_compound_capture_substitutions_into( + pkg, + state, + param_substitutions, + *base, + substitutions, + ); + collect_compound_capture_substitutions_into( + pkg, + state, + param_substitutions, + *index, + substitutions, + ); + } + ExprKind::UpdateIndex(container, index, value) => { + collect_compound_capture_substitutions_into( + pkg, + state, + param_substitutions, + *container, + substitutions, + ); + collect_compound_capture_substitutions_into( + pkg, + state, + param_substitutions, + *index, + substitutions, + ); + collect_compound_capture_substitutions_into( + pkg, + state, + param_substitutions, + *value, + substitutions, + ); + } + ExprKind::UpdateField(record, _, value) => { + collect_compound_capture_substitutions_into( + pkg, + state, + param_substitutions, + *record, + substitutions, + ); + collect_compound_capture_substitutions_into( + pkg, + state, + param_substitutions, + *value, + substitutions, + ); + } + ExprKind::Range(start, step, end) => { + for &part in [start, step, end].into_iter().flatten() { + collect_compound_capture_substitutions_into( + pkg, + state, + param_substitutions, + part, + substitutions, + ); + } + } + ExprKind::Assign(..) + | ExprKind::AssignOp(..) + | ExprKind::AssignField(..) + | ExprKind::AssignIndex(..) + | ExprKind::Block(..) + | ExprKind::Call(..) + | ExprKind::Closure(..) + | ExprKind::Fail(..) + | ExprKind::Hole + | ExprKind::If(..) + | ExprKind::Lit(..) + | ExprKind::Return(..) + | ExprKind::String(..) + | ExprKind::Var(..) + | ExprKind::While(..) => {} + } +} + +/// Reports whether a `Call`'s callee resolves to a pure `function`. +/// +/// A Q# `function` is guaranteed side-effect free (it cannot call operations, +/// allocate qubits, or measure) and its arrow type cannot bear functors, so it +/// is referentially transparent and its call may be relocated or duplicated +/// into caller-scope argument construction without changing observable +/// behavior. An `operation` may have observable side effects and ordering, so +/// its call must not be relocated. The callee's arrow-type `kind` is the +/// discriminator and is available directly at the call site for item, local, +/// and closure callees alike. +fn call_callee_is_pure_function(pkg: &Package, callee: ExprId) -> bool { + matches!( + &pkg.get_expr(callee).ty, + Ty::Arrow(arrow) if arrow.kind == CallableKind::Function + ) +} + +/// Reports whether rebuilding a captured compound literal in the caller would +/// leave an unresolved producer local behind. +/// +/// This mirrors [`collect_compound_capture_substitutions`] and the deep-clone +/// in rewrite: it recurses the same safe, referentially-transparent kinds, +/// including the same rule that only pure `function` calls may be entered, so a +/// leaf that collect already recorded a substitution for is not flagged. A +/// `Var(Res::Local(var))` leaf reached directly is a leak when `var` has no +/// recorded substitution. Any leaf inside a kind the clone keeps verbatim — a +/// block, closure, assignment, control-flow expression, or non-pure operation +/// call — counts as a leak whenever it references a producer local. +/// +/// The set of recursed kinds must match collect and clone exactly. Recursing +/// fewer kinds would wrongly decline captures that can in fact be rebuilt; +/// recursing more would accept a residue that cannot be represented in caller +/// scope. +fn compound_literal_has_residual_leak( + pkg: &Package, + substitutions: &[(LocalVarId, ExprId)], + expr_id: ExprId, +) -> bool { + match &pkg.get_expr(expr_id).kind { + ExprKind::Var(Res::Local(var), _) => !substitutions.iter().any(|(k, _)| k == var), + ExprKind::Tuple(elems) | ExprKind::Array(elems) | ExprKind::ArrayLit(elems) => elems + .iter() + .any(|&elem| compound_literal_has_residual_leak(pkg, substitutions, elem)), + ExprKind::ArrayRepeat(value, size) => { + compound_literal_has_residual_leak(pkg, substitutions, *value) + || compound_literal_has_residual_leak(pkg, substitutions, *size) + } + ExprKind::Struct(_, copy, fields) => { + copy.is_some_and(|copy| compound_literal_has_residual_leak(pkg, substitutions, copy)) + || fields.iter().any(|field| { + compound_literal_has_residual_leak(pkg, substitutions, field.value) + }) + } + ExprKind::Call(callee, arg) if call_callee_is_pure_function(pkg, *callee) => { + compound_literal_has_residual_leak(pkg, substitutions, *callee) + || compound_literal_has_residual_leak(pkg, substitutions, *arg) + } + // Non-pure calls cannot be relocated while rebuilding a captured + // compound literal in the caller, even when the call has no producer + // locals to leak. Decline the closure instead of duplicating or moving + // operation effects. + ExprKind::Call(..) => true, + ExprKind::BinOp(_, lhs, rhs) => { + compound_literal_has_residual_leak(pkg, substitutions, *lhs) + || compound_literal_has_residual_leak(pkg, substitutions, *rhs) + } + ExprKind::UnOp(_, operand) => { + compound_literal_has_residual_leak(pkg, substitutions, *operand) + } + ExprKind::Field(base, _) => compound_literal_has_residual_leak(pkg, substitutions, *base), + ExprKind::Index(base, index) => { + compound_literal_has_residual_leak(pkg, substitutions, *base) + || compound_literal_has_residual_leak(pkg, substitutions, *index) + } + ExprKind::UpdateIndex(container, index, value) => { + compound_literal_has_residual_leak(pkg, substitutions, *container) + || compound_literal_has_residual_leak(pkg, substitutions, *index) + || compound_literal_has_residual_leak(pkg, substitutions, *value) + } + ExprKind::UpdateField(record, _, value) => { + compound_literal_has_residual_leak(pkg, substitutions, *record) + || compound_literal_has_residual_leak(pkg, substitutions, *value) + } + ExprKind::Range(start, step, end) => [start, step, end] + .into_iter() + .flatten() + .any(|&part| compound_literal_has_residual_leak(pkg, substitutions, part)), + // A non-pure `Call` (operation callee) and every other un-remappable + // kind is kept verbatim by the clone, so it leaks if it references any + // producer local. + ExprKind::Assign(..) + | ExprKind::AssignOp(..) + | ExprKind::AssignField(..) + | ExprKind::AssignIndex(..) + | ExprKind::Block(..) + | ExprKind::Closure(..) + | ExprKind::Fail(..) + | ExprKind::Hole + | ExprKind::If(..) + | ExprKind::Lit(..) + | ExprKind::Return(..) + | ExprKind::String(..) + | ExprKind::Var(..) + | ExprKind::While(..) => expr_references_local(pkg, expr_id), + } +} + +/// Reports whether `expr_id` transitively references any `Var(Res::Local(_))`. +/// +/// A sound deep scan over every expression kind (via the FIR `Visitor`), used +/// by [`compound_literal_has_residual_leak`] on the verbatim-cloned leaves so +/// no producer local can slip past the decline guard. +fn expr_references_local(pkg: &Package, expr_id: ExprId) -> bool { + let mut finder = LocalVarFinder { + package: pkg, + found: false, + }; + finder.visit_expr(expr_id); + finder.found +} + +/// FIR `Visitor` that short-circuits on the first `Var(Res::Local(_))` reached +/// from a starting expression. +struct LocalVarFinder<'a> { + package: &'a Package, + found: bool, +} + +impl<'a> Visitor<'a> for LocalVarFinder<'a> { + fn visit_expr(&mut self, expr: ExprId) { + if self.found { + return; + } + if let ExprKind::Var(Res::Local(_), _) = &self.package.get_expr(expr).kind { + self.found = true; + return; + } + visit::walk_expr(self, expr); + } + + fn get_block(&self, id: BlockId) -> &'a Block { + self.package.get_block(id) + } + + fn get_expr(&self, id: ExprId) -> &'a Expr { + self.package.get_expr(id) + } + + fn get_pat(&self, id: PatId) -> &'a Pat { + self.package.get_pat(id) + } + + fn get_stmt(&self, id: StmtId) -> &'a Stmt { + self.package.get_stmt(id) + } +} + /// Seeds the callable-flow lattice for a HOF with the concrete callables /// bound to its arrow parameters at a specific call site, enabling /// reaching-def analysis to track parameter-forwarding chains. @@ -1800,31 +2426,32 @@ fn resolve_indexed_callable_candidates( let mut candidates = Vec::new(); for elem_expr_id in element_expr_ids { + let elem_allow_scoped_capture_exprs = allow_scoped_capture_exprs + || matches!( + pkg.get_expr(elem_expr_id).kind, + ExprKind::Block(_) | ExprKind::If(_, _, _) + ); let resolved = resolve_callee( pkg, store, locals, elem_expr_id, depth + 1, - allow_scoped_capture_exprs, + elem_allow_scoped_capture_exprs, scoped_capture_vars, package_id, ); match resolved { CalleeLattice::Single(callable) => { - if !candidates.contains(&callable) { - candidates.push(callable); - } + candidates.push(callable); } CalleeLattice::Multi(entries) => { for (callable, condition) in entries { if !condition.is_empty() { return None; } - if !candidates.contains(&callable) { - candidates.push(callable); - } + candidates.push(callable); } } CalleeLattice::Bottom | CalleeLattice::Dynamic => return None, @@ -1988,12 +2615,57 @@ pub(super) fn resolve_captures( .iter() .map(|&var| { let ty = find_local_var_type(pkg, locals, var)?; - let expr = resolve_scoped_capture_expr(pkg, locals, var, scoped_capture_vars); - Some(CapturedVar { var, ty, expr }) + let expr = resolve_known_callable_capture_expr(pkg, locals, var) + .or_else(|| resolve_scoped_capture_expr(pkg, locals, var, scoped_capture_vars)); + Some(CapturedVar { + var, + ty, + expr, + caller_substitutions: Vec::new(), + }) }) .collect() } +/// Returns the initializer expression bound to `var` when it resolves to a +/// statically-known callable value (see [`is_known_callable_capture_expr`]), +/// used to recognize a capture that can be baked into a closure target. +fn resolve_known_callable_capture_expr( + pkg: &Package, + locals: &LocalState, + var: LocalVarId, +) -> Option { + let expr_id = *locals.exprs.get(&var)?; + is_known_callable_capture_expr(pkg, locals, expr_id, 0).then_some(expr_id) +} + +/// Tests whether an expression is a statically-known callable value: a +/// non-generic item reference, a capture-free closure, or a local that forwards +/// (through `LocalState`) to one of those. +/// +/// Recursion is bounded by `MAX_RESOLVE_DEPTH` and guards against a local that +/// refers back to itself, so a forwarding chain cannot loop. +fn is_known_callable_capture_expr( + pkg: &Package, + locals: &LocalState, + expr_id: ExprId, + depth: usize, +) -> bool { + if depth > MAX_RESOLVE_DEPTH { + return false; + } + let (base_id, _) = peel_body_functors(pkg, expr_id); + match &pkg.get_expr(base_id).kind { + ExprKind::Var(Res::Item(_), generic_args) => generic_args.is_empty(), + ExprKind::Closure(captures, _) => captures.is_empty(), + ExprKind::Var(Res::Local(next), _) => locals.exprs.get(next).is_some_and(|&next_expr| { + next_expr != expr_id + && is_known_callable_capture_expr(pkg, locals, next_expr, depth + 1) + }), + _ => false, + } +} + /// Resolves a capture expression by walking the enclosing block scope and /// its visible local bindings, used when a direct `LocalState.exprs` lookup /// cannot see the binding. @@ -2132,7 +2804,7 @@ fn collect_binding_types_from_pat( map } -/// Recursively records `LocalVarId` → `Ty` for every binding in a pattern. +/// Recursively records `LocalVarId` => `Ty` for every binding in a pattern. fn collect_binding_types_from_pat_into( pkg: &Package, pat_id: qsc_fir::fir::PatId, @@ -2382,6 +3054,9 @@ fn bind_callable_pat( } } +/// Walks a binding pattern and records, in the analysis state, the reaching +/// callables for each arrow-typed sub-binding by indexing into the initializer +/// along the accumulated field `path`. fn bind_callable_pat_projections( pkg: &Package, store: &PackageStore, @@ -2507,6 +3182,16 @@ fn analyze_expr_flow( mut recorder: Option<&mut CallRecorder>, ) { let expr = pkg.get_expr(expr_id); + // Any write to a local invalidates conditional callables whose dispatch + // guard reads that local. Rewrite re-evaluates guards at the apply site, so + // reassigning a guard variable after a conditional callable's guarded value + // was formed would make the apply-site read observe the new value and + // dispatch to the wrong branch. Degrade those callables to `Dynamic` before + // processing the write so the stale dispatch is rejected with a clear + // diagnostic rather than silently miscompiled. + if let Some(written) = assignment_written_local(pkg, expr) { + invalidate_guard_dependents(pkg, state, written); + } match &expr.kind { ExprKind::Assign(lhs_id, rhs_id) => { // Recurse into the RHS first (in evaluation order) so any nested @@ -2856,8 +3541,77 @@ fn collect_assigned_vars_expr(pkg: &Package, expr_id: ExprId, vars: &mut Vec Option { + match &pkg.get_expr(lhs_id).kind { + ExprKind::Var(Res::Local(var), _) => Some(*var), + ExprKind::Field(base, _) | ExprKind::Index(base, _) => assign_lhs_base_local(pkg, *base), + _ => None, + } +} + +/// Reports whether `expr` transitively reads the local `var`. +fn expr_reads_local(pkg: &Package, expr_id: ExprId, var: LocalVarId) -> bool { + let mut found = false; + crate::walk_utils::for_each_expr(pkg, expr_id, &mut |_id, expr| { + if let ExprKind::Var(Res::Local(v), _) = &expr.kind + && *v == var + { + found = true; + } + }); + found +} + +/// Degrades to `Dynamic` any conditional callable in `state` whose dispatch +/// guard reads `written`, invoked when `written` is reassigned during the +/// forward flow. +/// +/// Rewrite reconstructs a conditional callable's dispatch by re-evaluating its +/// guards at the *apply* site, not the *binding* site. Once a guard variable is +/// reassigned after the callable's guarded value was formed, the guard read at +/// the apply site would observe the new value and select the wrong branch (see +/// the `reaching_def_conditional_callable_reassigned_guard_dynamic` regression). +/// Marking such callables `Dynamic` surfaces a clear "could not be resolved +/// statically" diagnostic instead of emitting incorrect dispatch. Guards formed +/// *after* this write are unaffected, so a normalization accumulator assigned +/// before the branch decision (e.g. `cond_normalize`'s `__cond`) stays +/// resolvable. +fn invalidate_guard_dependents(pkg: &Package, state: &mut LocalState, written: LocalVarId) { + for lattice in state.callable.values_mut() { + if let CalleeLattice::Multi(entries) = lattice { + let depends = entries.iter().any(|(_, guards)| { + guards + .iter() + .any(|&guard| expr_reads_local(pkg, guard, written)) + }); + if depends { + *lattice = CalleeLattice::Dynamic; + } + } + } +} + +/// Resolves the base local written by an assignment expression, descending +/// through field and index projections (`x::field = ...`, `arr[i] = ...`) to +/// the underlying `Var(Local)`. Returns `None` when the expression is not an +/// assignment rooted in a local. +fn assignment_written_local(pkg: &Package, expr: &Expr) -> Option { + let lhs_id = match &expr.kind { + ExprKind::Assign(lhs, _) + | ExprKind::AssignOp(_, lhs, _) + | ExprKind::AssignField(lhs, _, _) + | ExprKind::AssignIndex(lhs, _, _) => *lhs, + _ => return None, + }; + assign_lhs_base_local(pkg, lhs_id) +} + /// Extracts bindings from a pattern. For `Bind(ident)` patterns, records -/// `ident.id → init_expr_id`. For `Tuple` patterns, we cannot easily +/// `ident.id => init_expr_id`. For `Tuple` patterns, we cannot easily /// split the init expression, so we skip those. fn collect_bindings_from_pat( pkg: &Package, diff --git a/source/compiler/qsc_fir_transforms/src/defunctionalize/prepass.rs b/source/compiler/qsc_fir_transforms/src/defunctionalize/prepass.rs index bbce1423cc2..51ada058138 100644 --- a/source/compiler/qsc_fir_transforms/src/defunctionalize/prepass.rs +++ b/source/compiler/qsc_fir_transforms/src/defunctionalize/prepass.rs @@ -67,6 +67,14 @@ fn promote_adjacent_aggregate_callable_aliases(store: &mut PackageStore, package } } +/// Iterates the block until no further promotions apply, removing alias +/// statements whose single-use binding feeds a subsequent tuple destructure. +/// +/// Each pass scans adjacent statement pairs: when the first is an immutable +/// `let` binding whose init is a callable-bearing aggregate and the second +/// destructures that binding with exactly one use, the alias statement is +/// elided and the destructure is repointed directly at the original init. +/// The loop re-runs because removing one alias may expose the next. fn promote_adjacent_aggregate_callable_aliases_in_block(pkg: &mut Package, block_id: BlockId) { loop { let stmt_ids = pkg.get_block(block_id).stmts.clone(); @@ -112,6 +120,17 @@ fn promote_adjacent_aggregate_callable_aliases_in_block(pkg: &mut Package, block } } +/// Returns the initializer `ExprId` of `alias_stmt_id` when it forms a +/// promotable adjacent-aggregate pair with `use_stmt_id`. +/// +/// The pair is promotable when: +/// 1. `alias_stmt_id` is an immutable `let` binding whose type contains an +/// arrow (callable-bearing aggregate). +/// 2. `use_stmt_id` destructures that exact binding via a tuple pattern. +/// 3. The alias local has exactly one use in the enclosing block, which is +/// the `use_stmt_id` reference. +/// +/// Returns `None` when any condition fails. fn aggregate_alias_promotion_init( pkg: &Package, block_id: BlockId, @@ -150,6 +169,9 @@ fn aggregate_alias_promotion_init( } } +/// Reports whether `local_id` has exactly one use in `block_id` and that +/// use is the expression `expected_use_expr_id`. Both direct `Var` references +/// and closure captures count as uses. fn local_has_exactly_one_use_in_block( pkg: &Package, block_id: BlockId, @@ -176,6 +198,15 @@ fn local_has_exactly_one_use_in_block( use_count == 1 && saw_expected_use } +/// Reports whether `ty` is or transitively contains an arrow type (callable). +/// Recurses through tuple types but does not expand UDTs — expanding a UDT +/// requires a `PackageStore` lookup (to read the type definition's underlying +/// structure), and this helper intentionally avoids that dependency because +/// the pre-pass promotions are best-effort simplifications, not correctness +/// requirements. A missed callable hidden behind a UDT wrapper is still +/// handled correctly by the full analysis phase, which uses the heavier +/// [`super::specialize::ty_contains_arrow_through_udts`] variant with store +/// access. fn ty_contains_arrow(ty: &Ty) -> bool { match ty { Ty::Arrow(_) => true, diff --git a/source/compiler/qsc_fir_transforms/src/defunctionalize/rewrite.rs b/source/compiler/qsc_fir_transforms/src/defunctionalize/rewrite.rs index 3c9faa63aa0..1bdcf7f5267 100644 --- a/source/compiler/qsc_fir_transforms/src/defunctionalize/rewrite.rs +++ b/source/compiler/qsc_fir_transforms/src/defunctionalize/rewrite.rs @@ -40,18 +40,25 @@ use super::types::{ AnalysisResult, CallSite, CallableParam, CapturedVar, ConcreteCallable, DirectCallSite, SpecKey, peel_body_functors, }; -use super::{build_spec_key, ty_contains_arrow}; -use crate::EMPTY_EXEC_RANGE; +use super::{ + build_combined_spec_key, build_combined_spec_key_for_group, build_spec_key, + is_combined_eligible, partition_mixed_branch_split, ty_contains_arrow, +}; +use crate::fir_builder::{ + alloc_bin_op_expr, alloc_call_expr, alloc_expr, alloc_functor_wrapped_expr, alloc_int_lit, + alloc_local_var_expr, +}; use crate::walk_utils::{DirectChild, UseClass, classify_block_use, for_each_direct_child}; use qsc_data_structures::functors::FunctorApp; use qsc_data_structures::span::Span; use qsc_fir::assigner::Assigner; use qsc_fir::fir::{ - BinOp, BlockId, CallableImpl, Expr, ExprId, ExprKind, Field, Functor, ItemId, ItemKind, Lit, - LocalItemId, LocalVarId, Mutability, Package, PackageId, PackageLookup, PatId, PatKind, Res, - StmtKind, StoreItemId, UnOp, + BinOp, Block, BlockId, CallableImpl, CallableKind, Expr, ExprId, ExprKind, Field, FieldAssign, + FieldPath, ItemId, ItemKind, LocalItemId, LocalVarId, Mutability, Package, PackageId, + PackageLookup, Pat, PatId, PatKind, Res, Stmt, StmtId, StmtKind, StoreItemId, }; use qsc_fir::ty::{Arrow, Prim, Ty}; +use qsc_fir::visit::{self, Visitor}; use rustc_hash::{FxHashMap, FxHashSet}; /// A resolved HOF dispatch target: the `(call site, specialization item, param)` @@ -74,7 +81,6 @@ type ConditionedHofTarget<'a> = (HofDispatchTarget<'a>, Vec); /// - If the callable argument was a closure, its captured variables are /// appended as extra arguments. /// - The callee expression's type is updated to reflect the new signature. -#[allow(clippy::too_many_lines)] pub(super) fn rewrite( package: &mut Package, package_id: PackageId, @@ -85,7 +91,20 @@ pub(super) fn rewrite( let expr_owner_lookup = build_expr_owner_lookup(package); let mut rewritten_callable_arg_locals = FxHashSet::default(); - // Build a lookup from each HOF's StoreItemId → CallableParam. + // Source-array locals for closure callable-arrays that a higher-order call + // forwards (directly, through a struct-literal field, or through `let` + // aliases) and fully consumes. The bare-`Var` recorder above cannot see the + // underlying array when it is wrapped in a struct field or aliased, and the + // array element type keeps `remove_dead_callable_local_from_callable` from + // pruning it. Tracing the forwarded value back to its source-array local + // lets the closure-bearing cleanup remove the now-dead binding instead of + // leaving an array of blanked (unit) closure elements — an arrow-typed + // block with a unit tail — stranded in a reachable caller. + let mut hof_consumed_source_arrays = FxHashSet::default(); + + // Lowest-index lookup serves the per-row and branch-split paths, where + // every row of a group resolves the same parameter; it cannot distinguish + // between separate arrow parameters. let param_lookup: FxHashMap = { let mut map = FxHashMap::default(); for p in &analysis.callable_params { @@ -94,79 +113,419 @@ pub(super) fn rewrite( map }; - // Group resolved call sites by call_expr_id so that multi-callee sites - // (from branch-split analysis) are handled together. - let mut grouped: FxHashMap> = FxHashMap::default(); + // Precise lookup keyed by parameter position recovers the exact parameter + // for each distinct slot of a combined multi-argument call. + let param_by_position: FxHashMap<(StoreItemId, usize, Vec), &CallableParam> = { + let mut map = FxHashMap::default(); + for p in &analysis.callable_params { + map.insert((p.callable_id, p.top_level_param, p.field_path.clone()), p); + } + map + }; + // Group this package's static call sites by call expression. A combined + // multi-argument call contributes one row per arrow parameter; those rows + // rewrite together so the single call shape matches the one combined + // specialization. Rows that share an expression but resolve the same + // parameter, which are the branch-split candidate sets, keep their dispatch + // path. + let mut grouped: FxHashMap> = FxHashMap::default(); for call_site in &analysis.call_sites { // This pass rewrites one package at a time; skip call sites that live // in a different package's body. if call_site.call_pkg_id != package_id { continue; } - // Skip dynamic callables — they have no specialization. if matches!(call_site.callable_arg, ConcreteCallable::Dynamic) { continue; } + grouped + .entry(call_site.call_expr_id) + .or_default() + .push(call_site); + } + + for (call_expr_id, group) in &grouped { + // Combined multi-argument rewrite: specialize and rewrite consult the + // same predicate so they agree on which call sites are combined. + if is_combined_eligible(package, group) { + rewrite_combined_group( + package, + *call_expr_id, + group, + spec_map, + ¶m_by_position, + &expr_owner_lookup, + &mut rewritten_callable_arg_locals, + &mut hof_consumed_source_arrays, + assigner, + ); + continue; + } + + // Per-leaf producer-closure inline. The specialize side built one + // combined spec per dispatch candidate, formed as `[candidate] + + // single-valued siblings`, for this mixed branch-split group. Route the + // synthesized dispatch leaves through those combined specs so each leaf + // inlines the single-valued producer closure, consumed in the same pass + // before any later-iteration producer-body clearing. + if rewrite_mixed_branch_split_group( + package, + package_id, + *call_expr_id, + group, + spec_map, + ¶m_by_position, + &expr_owner_lookup, + &mut rewritten_callable_arg_locals, + &mut hof_consumed_source_arrays, + assigner, + ) { + continue; + } + + rewrite_per_row_group( + package, + package_id, + *call_expr_id, + group, + spec_map, + ¶m_lookup, + ¶m_by_position, + &expr_owner_lookup, + &mut rewritten_callable_arg_locals, + assigner, + ); + } + + rewrite_direct_call_sites( + package, + package_id, + analysis, + &expr_owner_lookup, + &mut rewritten_callable_arg_locals, + assigner, + ); + + prune_dead_callable_arg_locals( + package, + &rewritten_callable_arg_locals, + &hof_consumed_source_arrays, + ); +} + +/// Rewrites a combined multi-argument HOF call whose arrow parameters all +/// specialize together under a single combined key. +/// +/// Recovers the exact [`CallableParam`] for each row via `param_by_position`, +/// orders the members ascending by parameter position so the rewritten argument +/// tuple lines up with the specialize-side combined input pattern, records the +/// consumed callable-arg locals and source arrays, then dispatches to either +/// the callable-array or the plain multi-argument rewrite. Returns without +/// rewriting when the combined spec is missing or any row's parameter cannot be +/// resolved. +#[allow(clippy::too_many_arguments)] +fn rewrite_combined_group( + package: &mut Package, + call_expr_id: ExprId, + group: &[&CallSite], + spec_map: &FxHashMap, + param_by_position: &FxHashMap<(StoreItemId, usize, Vec), &CallableParam>, + expr_owner_lookup: &FxHashMap, + rewritten_callable_arg_locals: &mut FxHashSet<(LocalItemId, LocalVarId)>, + hof_consumed_source_arrays: &mut FxHashSet<(LocalItemId, LocalVarId)>, + assigner: &mut Assigner, +) { + // Every row in this group came from the same higher-order call, so they all + // point at the same HOF. Grab it and look up the one specialization that was + // generated for this exact combination of callable arguments. If we never + // built that specialization, there is nothing to rewrite to, so bail out. + let hof_item_id = group[0].hof_item_id; + let spec_key = build_combined_spec_key_for_group(hof_item_id, group); + let Some(&spec_store_id) = spec_map.get(&spec_key) else { + return; + }; + let hof_store_id = StoreItemId::from((hof_item_id.package, hof_item_id.item)); + + // Recover the exact parameter per row, then order the members + // ascending by parameter position so the rewritten argument tuple + // lines up with the specialize-side combined input pattern. + // + // Each row knows which slot of the call it filled (its top-level position + // plus the path into any nested tuple/struct). Use that to find the exact + // parameter it corresponds to on the HOF. If any row's parameter can't be + // matched, we can't safely rewrite the whole call, so give up entirely. + let mut members: Vec<(&CallSite, &CallableParam)> = Vec::with_capacity(group.len()); + for call_site in group { + let position_key = ( + hof_store_id, + call_site.top_level_param, + call_site.field_path.clone(), + ); + if let Some(¶m) = param_by_position.get(&position_key) { + members.push((call_site, param)); + } else { + return; + } + } + // Put the rows back in the order the parameters appear in the HOF signature, + // so the new argument list we build matches the specialization's expected + // input shape one-for-one. + members.sort_by(|a, b| { + a.1.top_level_param + .cmp(&b.1.top_level_param) + .then_with(|| a.1.field_path.cmp(&b.1.field_path)) + }); + + // Before we change the call, note the callable arguments that are about to + // be baked into the specialization. Recording them (and any backing arrays + // that get fully consumed) lets a later cleanup pass delete the now-unused + // local variables that used to hold those callables. + for (call_site, _) in &members { + collect_rewritten_callable_arg_local( + package, + expr_owner_lookup, + call_site.call_expr_id, + call_site.arg_expr_id, + rewritten_callable_arg_locals, + ); + collect_hof_consumed_source_array( + package, + expr_owner_lookup, + call_site.call_expr_id, + call_site.arg_expr_id, + hof_consumed_source_arrays, + ); + } + // Finally, actually rewrite the call. If one of the arguments is an array of + // callables that itself needs to be rebuilt element-by-element, take the + // specialized array path; otherwise do the plain multi-argument rewrite that + // just drops the callable args and calls the specialization directly. + if callable_array_member_position(&members).is_some() + && callable_array_member_needs_nested_rewrite(&members) + { + rewrite_callable_array_multi( + package, + call_expr_id, + &members, + spec_store_id, + expr_owner_lookup, + assigner, + ); + } else { + rewrite_multi( + package, + call_expr_id, + &members, + spec_store_id, + expr_owner_lookup, + assigner, + ); + } +} + +/// Attempts the per-leaf producer-closure inline for a mixed branch-split +/// group, routing each synthesized dispatch leaf through the per-candidate +/// combined spec (`[candidate] + single-valued siblings`) so each leaf inlines +/// the single-valued producer closure in the same pass. +/// +/// Returns `true` when the group was rewritten. Returns `false` when the group +/// is not a mixed branch-split, or when a required combined spec or parameter +/// cannot be resolved, so the caller falls through to the per-row path, which +/// surfaces an honest `DynamicCallable` diagnostic instead of wrong QIR. +#[allow(clippy::too_many_arguments)] +fn rewrite_mixed_branch_split_group( + package: &mut Package, + package_id: PackageId, + call_expr_id: ExprId, + group: &[&CallSite], + spec_map: &FxHashMap, + param_by_position: &FxHashMap<(StoreItemId, usize, Vec), &CallableParam>, + expr_owner_lookup: &FxHashMap, + rewritten_callable_arg_locals: &mut FxHashSet<(LocalItemId, LocalVarId)>, + hof_consumed_source_arrays: &mut FxHashSet<(LocalItemId, LocalVarId)>, + assigner: &mut Assigner, +) -> bool { + let Some((dispatch, constants)) = partition_mixed_branch_split(group) else { + return false; + }; + let hof_item_id = group[0].hof_item_id; + let hof_store_id = StoreItemId::from((hof_item_id.package, hof_item_id.item)); + + // Resolve constant sibling params; operand order is handled in the + // leaf builder `create_combined_branch_call`. + let mut const_members: Vec<(&CallSite, &CallableParam)> = Vec::with_capacity(constants.len()); + let mut resolved = true; + for cs in &constants { + let position_key = (hof_store_id, cs.top_level_param, cs.field_path.clone()); + if let Some(¶m) = param_by_position.get(&position_key) { + const_members.push((*cs, param)); + } else { + resolved = false; + break; + } + } + + // Build dispatch entries keyed by the per-candidate combined spec. + let mut entries: Vec = Vec::with_capacity(dispatch.len()); + if resolved { + for candidate in &dispatch { + let mut members_cs: Vec<&CallSite> = Vec::with_capacity(constants.len() + 1); + members_cs.push(*candidate); + members_cs.extend(constants.iter().copied()); + let spec_key = build_combined_spec_key(hof_item_id, &members_cs); + let position_key = ( + hof_store_id, + candidate.top_level_param, + candidate.field_path.clone(), + ); + if let (Some(&spec_store_id), Some(¶m)) = ( + spec_map.get(&spec_key), + param_by_position.get(&position_key), + ) { + entries.push((*candidate, spec_store_id, param)); + } else { + resolved = false; + break; + } + } + } + + if resolved && !entries.is_empty() { + for call_site in group { + collect_rewritten_callable_arg_local( + package, + expr_owner_lookup, + call_site.call_expr_id, + call_site.arg_expr_id, + rewritten_callable_arg_locals, + ); + collect_hof_consumed_source_array( + package, + expr_owner_lookup, + call_site.call_expr_id, + call_site.arg_expr_id, + hof_consumed_source_arrays, + ); + } + branch_split_rewrite( + package, + package_id, + call_expr_id, + &entries, + &const_members, + expr_owner_lookup, + assigner, + ); + return true; + } + // Combined specs not found — fall through to the per-row path. + false +} +/// Rewrites a HOF call group on the per-row / branch-split path: resolves each +/// row under its single-argument spec key and exact parameter position, then +/// dispatches by candidate count. +/// +/// A single resolved entry uses the direct [`rewrite_one`] rewrite; multiple +/// entries synthesize a condition-indexed dispatch via [`branch_split_rewrite`]. +/// Rows whose spec or parameter cannot be resolved are skipped. +#[allow(clippy::too_many_arguments)] +fn rewrite_per_row_group( + package: &mut Package, + package_id: PackageId, + call_expr_id: ExprId, + group: &[&CallSite], + spec_map: &FxHashMap, + param_lookup: &FxHashMap, + param_by_position: &FxHashMap<(StoreItemId, usize, Vec), &CallableParam>, + expr_owner_lookup: &FxHashMap, + rewritten_callable_arg_locals: &mut FxHashSet<(LocalItemId, LocalVarId)>, + assigner: &mut Assigner, +) { + let mut entries: Vec = Vec::with_capacity(group.len()); + for call_site in group { let spec_key = build_spec_key(call_site); let Some(&spec_store_id) = spec_map.get(&spec_key) else { continue; }; - let hof_store_id = StoreItemId::from((call_site.hof_item_id.package, call_site.hof_item_id.item)); - let Some(¶m) = param_lookup.get(&hof_store_id) else { + // Resolve the exact parameter for this row's position so a removed + // sibling drops its own slot rather than the lowest-index slot. + let position_key = ( + hof_store_id, + call_site.top_level_param, + call_site.field_path.clone(), + ); + let Some(¶m) = param_by_position + .get(&position_key) + .or_else(|| param_lookup.get(&hof_store_id)) + else { continue; }; + entries.push((call_site, spec_store_id, param)); + } - grouped - .entry(call_site.call_expr_id) - .or_default() - .push((call_site, spec_store_id, param)); + if entries.is_empty() { + return; } - for (call_expr_id, entries) in &grouped { - if entries.len() == 1 { - let (call_site, spec_store_id, param) = entries[0]; + if entries.len() == 1 { + let (call_site, spec_store_id, param) = entries[0]; + collect_rewritten_callable_arg_local( + package, + expr_owner_lookup, + call_site.call_expr_id, + call_site.arg_expr_id, + rewritten_callable_arg_locals, + ); + rewrite_one( + package, + package_id, + call_site, + param, + spec_store_id, + expr_owner_lookup, + assigner, + ); + } else { + for (call_site, _, _) in &entries { collect_rewritten_callable_arg_local( package, - &expr_owner_lookup, + expr_owner_lookup, call_site.call_expr_id, call_site.arg_expr_id, - &mut rewritten_callable_arg_locals, - ); - rewrite_one( - package, - package_id, - call_site, - param, - spec_store_id, - &expr_owner_lookup, - assigner, - ); - } else { - for (call_site, _, _) in entries { - collect_rewritten_callable_arg_local( - package, - &expr_owner_lookup, - call_site.call_expr_id, - call_site.arg_expr_id, - &mut rewritten_callable_arg_locals, - ); - } - branch_split_rewrite( - package, - package_id, - *call_expr_id, - entries, - &expr_owner_lookup, - assigner, + rewritten_callable_arg_locals, ); } + branch_split_rewrite( + package, + package_id, + call_expr_id, + &entries, + &[], + expr_owner_lookup, + assigner, + ); } +} +/// Rewrites every direct call site in this package's body, grouping them by +/// call expression. +/// +/// A lone unconditional site is rewritten in place by [`rewrite_direct_call`]; +/// a group with multiple sites (or a conditional lone site) is lowered to a +/// condition-indexed dispatch via [`branch_split_direct_call_rewrite`]. +fn rewrite_direct_call_sites( + package: &mut Package, + package_id: PackageId, + analysis: &AnalysisResult, + expr_owner_lookup: &FxHashMap, + rewritten_callable_arg_locals: &mut FxHashSet<(LocalItemId, LocalVarId)>, + assigner: &mut Assigner, +) { let mut grouped_direct: FxHashMap> = FxHashMap::default(); for direct_call_site in &analysis.direct_call_sites { // Rewrite only the direct call sites that live in this package's body. @@ -185,8 +544,8 @@ pub(super) fn rewrite( package, package_id, entries[0], - &expr_owner_lookup, - &mut rewritten_callable_arg_locals, + expr_owner_lookup, + rewritten_callable_arg_locals, assigner, ); } else { @@ -198,23 +557,21 @@ pub(super) fn rewrite( collect_rewritten_callable_arg_local( package, - &expr_owner_lookup, + expr_owner_lookup, call_expr_id, callee_id, - &mut rewritten_callable_arg_locals, + rewritten_callable_arg_locals, ); branch_split_direct_call_rewrite( package, package_id, call_expr_id, entries, - &expr_owner_lookup, + expr_owner_lookup, assigner, ); } } - - prune_dead_callable_arg_locals(package, &rewritten_callable_arg_locals); } /// Rewrites a `DirectCallSite` whose callee was resolved to a specific @@ -282,10 +639,32 @@ fn rewrite_direct_call( controlled_layers, assigner, ); - if matches!(direct_call_site.callable, ConcreteCallable::Closure { .. }) - || package_direct_lambda + // The target callable's declared input, used to shape the call arguments. + // For a closure this is the (already capture-flattened) target signature; + // for a directly-called lambda whose parameters live in a one-element tuple + // it is the packaged one-tuple input. + let target_input = match &direct_call_site.callable { + ConcreteCallable::Closure { target, .. } => match &package.get_item(*target).kind { + ItemKind::Callable(decl) => Some(package.get_pat(decl.input).ty.clone()), + ItemKind::Ty(..) => None, + }, + ConcreteCallable::Global { item_id, .. } if item_id.package == package_id => { + direct_lambda_packaged_input(package, item_id.item) + } + _ => None, + }; + if let Some(target_input) = target_input + && (matches!(direct_call_site.callable, ConcreteCallable::Closure { .. }) + || package_direct_lambda) { - rewrite_direct_closure_args(package, args_id, &captures, controlled_layers, assigner); + rewrite_direct_closure_args( + package, + args_id, + &captures, + &target_input, + controlled_layers, + assigner, + ); } } @@ -415,6 +794,79 @@ fn collect_rewritten_callable_arg_local( } } +/// Records the source-array local for a closure callable-array that a +/// higher-order call forwards and fully consumes. +/// +/// The forwarded argument is not always a bare `Var`: a call may pass the array +/// through a struct-literal field (`f(new Config { Ops = ops })`) or through a +/// chain of `let` aliases (`let a = ops; f(a)`). In every case the underlying +/// value is the same `Var(Res::Local)` source-array local. Recording it lets +/// the closure-bearing cleanup remove the now-dead binding after the call is +/// rewritten, instead of leaving an array of blanked (unit) closure elements — +/// an arrow-typed block with a unit tail — in a reachable caller. +fn collect_hof_consumed_source_array( + package: &Package, + expr_owner_lookup: &FxHashMap, + call_expr_id: ExprId, + arg_expr_id: ExprId, + hof_consumed_source_arrays: &mut FxHashSet<(LocalItemId, LocalVarId)>, +) { + let Some(&callable_id) = expr_owner_lookup.get(&call_expr_id) else { + return; + }; + trace_forwarded_callable_array_source_locals(package, callable_id, arg_expr_id, &mut |src| { + hof_consumed_source_arrays.insert((callable_id, src)); + }); +} + +/// Traces a forwarded argument expression to the callable-array source locals +/// it ultimately references, invoking `record` for each. +/// +/// Follows `let`-alias chains (`let a = ops`) to the originating array local and +/// descends into struct-literal fields (`new Config { Ops = ops }`) so a +/// callable array wrapped in a struct is still reached. Only locals whose type +/// is a callable array are recorded; the closure-bearing gate in +/// [`prune_dead_callable_arg_locals`] still decides whether removal is safe, so +/// plain callable-reference arrays are left in place. +fn trace_forwarded_callable_array_source_locals( + package: &Package, + owner: LocalItemId, + expr_id: ExprId, + record: &mut impl FnMut(LocalVarId), +) { + match &package.get_expr(expr_id).kind { + ExprKind::Var(Res::Local(var), _) => { + let var = *var; + if !ty_is_callable_array(package, &package.get_expr(expr_id).ty) { + return; + } + // Follow a `let` alias to the original source array; a genuine + // array-literal (or other non-alias) binding is itself the source. + if let Some(init) = find_local_init_expr_in_callable(package, owner, var) + && matches!(package.get_expr(init).kind, ExprKind::Var(Res::Local(_), _)) + { + trace_forwarded_callable_array_source_locals(package, owner, init, record); + } else { + record(var); + } + } + ExprKind::Struct(_, copy, fields) => { + if let Some(copy) = copy { + trace_forwarded_callable_array_source_locals(package, owner, *copy, record); + } + for field in fields { + trace_forwarded_callable_array_source_locals(package, owner, field.value, record); + } + } + _ => {} + } +} + +/// Returns `true` when `ty` is an array whose element type contains an arrow. +fn ty_is_callable_array(package: &Package, ty: &Ty) -> bool { + matches!(resolve_udt_ty(package, ty), Ty::Array(elem) if ty_contains_arrow(&elem)) +} + /// Synthesizes an index-dispatch `if`/`else` chain for a HOF call site that /// resolves to multiple callables via branch-split analysis. fn synthesize_callsite_index_dispatch( @@ -550,6 +1002,13 @@ fn resolve_index_dispatch_source( owner_expr_id: ExprId, dispatch_expr_id: ExprId, ) -> Option<(ExprId, Vec)> { + let direct_callables = resolve_array_expr_to_callables( + package, + expr_owner_lookup, + owner_expr_id, + dispatch_expr_id, + ); + let source_expr_id = resolve_dispatch_source_expr(package, expr_owner_lookup, owner_expr_id, dispatch_expr_id)?; let ExprKind::Index(array_expr_id, index_expr_id) = package.get_expr(source_expr_id).kind @@ -565,6 +1024,12 @@ fn resolve_index_dispatch_source( return Some((index_expr_id, indexed_callables)); } + if let Some(indexed_callables) = direct_callables + && indexed_callables.len() >= 2 + { + return Some((index_expr_id, indexed_callables)); + } + // Direct resolution failed: array elements may be tuples. // Check if the dispatch expression was a local variable bound from a // tuple pattern, and try extracting the appropriate field from each @@ -745,186 +1210,227 @@ fn resolve_expr_to_concrete_callable( let expr = package.get_expr(base_id); match expr.kind { ExprKind::Var(Res::Item(item_id), _) => Some(ConcreteCallable::Global { item_id, functor }), + ExprKind::Closure(ref captured_vars, target) => Some(ConcreteCallable::Closure { + target, + captures: resolve_concrete_closure_captures( + package, + expr_owner_lookup, + owner_expr_id, + captured_vars, + )?, + functor, + }), _ => None, } } -/// Allocates a `BinOp(Eq, index_expr, Int(index_value))` expression used as -/// the condition guard for index-dispatch branches. Inserts two new `Expr` -/// nodes (literal and comparison) through `assigner`. -fn alloc_index_eq_expr( - package: &mut Package, - index_expr_id: ExprId, - index_value: usize, - span: Span, - assigner: &mut Assigner, -) -> ExprId { - let lit_id = assigner.next_expr(); - let index_value = i64::try_from(index_value).expect("dispatch index should fit in i64"); - package.exprs.insert( - lit_id, - Expr { - id: lit_id, - span, - ty: Ty::Prim(Prim::Int), - kind: ExprKind::Lit(Lit::Int(index_value)), - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); +/// Resolves each captured variable of a concrete closure to a [`CapturedVar`], +/// recovering its type and initializer expression from the owning callable. +/// +/// Returns `None` when the owner is unknown or a capture's type cannot be +/// found. +fn resolve_concrete_closure_captures( + package: &Package, + expr_owner_lookup: &FxHashMap, + owner_expr_id: ExprId, + captured_vars: &[LocalVarId], +) -> Option> { + let owner_callable = *expr_owner_lookup.get(&owner_expr_id)?; + captured_vars + .iter() + .map(|&var| { + let expr = find_local_init_expr_in_callable(package, owner_callable, var); + let ty = expr + .map(|expr_id| package.get_expr(expr_id).ty.clone()) + .or_else(|| find_var_type_in_callable(package, owner_callable, var))?; + Some(CapturedVar { + var, + ty, + expr, + caller_substitutions: Vec::new(), + }) + }) + .collect() +} - let cond_id = assigner.next_expr(); - package.exprs.insert( - cond_id, - Expr { - id: cond_id, - span, - ty: Ty::Prim(Prim::Bool), - kind: ExprKind::BinOp(BinOp::Eq, index_expr_id, lit_id), - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); - cond_id +/// Drives a [`Visitor`] over a callable implementation's body blocks — the +/// `body` block plus any `adj`/`ctl`/`ctl_adj` specialization blocks. +/// +/// Specialization input patterns are intentionally skipped: the `find_*` +/// searches only look at the callable's own input pattern and the statements +/// inside each specialization body. +fn walk_callable_impl_bodies<'a>(vis: &mut impl Visitor<'a>, callable_impl: &CallableImpl) { + match callable_impl { + CallableImpl::Intrinsic => {} + CallableImpl::SimulatableIntrinsic(spec_decl) => vis.visit_block(spec_decl.block), + CallableImpl::Spec(spec_impl) => { + vis.visit_block(spec_impl.body.block); + for spec in [ + spec_impl.adj.as_ref(), + spec_impl.ctl.as_ref(), + spec_impl.ctl_adj.as_ref(), + ] + .into_iter() + .flatten() + { + vis.visit_block(spec.block); + } + } + } } -/// Locates the initializer expression for a given local variable inside a -/// reachable callable body. -fn find_local_init_expr_in_callable( +/// FIR [`Visitor`] that records the declared type of a target local the first +/// time it reaches the `Bind` pattern that introduces it. Recursion stops once +/// the type is found. +struct VarTypeFinder<'a> { + package: &'a Package, + local_var: LocalVarId, + result: Option, +} + +impl<'a> Visitor<'a> for VarTypeFinder<'a> { + fn visit_pat(&mut self, pat: PatId) { + if self.result.is_some() { + return; + } + let p = self.package.get_pat(pat); + match &p.kind { + PatKind::Bind(ident) if ident.id == self.local_var => { + self.result = Some(p.ty.clone()); + } + _ => visit::walk_pat(self, pat), + } + } + + fn visit_expr(&mut self, expr: ExprId) { + if self.result.is_some() { + return; + } + visit::walk_expr(self, expr); + } + + fn get_block(&self, id: BlockId) -> &'a Block { + self.package.get_block(id) + } + fn get_expr(&self, id: ExprId) -> &'a Expr { + self.package.get_expr(id) + } + fn get_pat(&self, id: PatId) -> &'a Pat { + self.package.get_pat(id) + } + fn get_stmt(&self, id: StmtId) -> &'a Stmt { + self.package.get_stmt(id) + } +} + +/// Searches a callable's body and input pattern for the declared type of +/// `local_var`, returning `None` when it is not found. +fn find_var_type_in_callable( package: &Package, callable_id: LocalItemId, local_var: LocalVarId, -) -> Option { +) -> Option { let Some(ItemKind::Callable(decl)) = package.items.get(callable_id).map(|item| &item.kind) else { return None; }; + let mut finder = VarTypeFinder { + package, + local_var, + result: None, + }; + finder.visit_pat(decl.input); + if finder.result.is_none() { + walk_callable_impl_bodies(&mut finder, &decl.implementation); + } + finder.result +} - find_local_init_expr_in_callable_impl(package, &decl.implementation, local_var) +/// Allocates a `BinOp(Eq, index_expr, Int(index_value))` expression used as +/// the condition guard for index-dispatch branches. Inserts two new `Expr` +/// nodes (literal and comparison) through `assigner`. +fn alloc_index_eq_expr( + package: &mut Package, + index_expr_id: ExprId, + index_value: usize, + span: Span, + assigner: &mut Assigner, +) -> ExprId { + let index_value = i64::try_from(index_value).expect("dispatch index should fit in i64"); + let lit_id = alloc_int_lit(package, assigner, index_value, span); + alloc_bin_op_expr( + package, + assigner, + BinOp::Eq, + index_expr_id, + lit_id, + Ty::Prim(Prim::Bool), + span, + ) } -/// Recurses over a `CallableImpl` variant to locate a local variable's -/// initializer expression. -fn find_local_init_expr_in_callable_impl( - package: &Package, - callable_impl: &qsc_fir::fir::CallableImpl, +/// FIR [`Visitor`] that records the initializer expression of the `Local` +/// binding for a target local the first time it is reached. Recursion stops +/// once the initializer is found. +struct LocalInitFinder<'a> { + package: &'a Package, local_var: LocalVarId, -) -> Option { - match callable_impl { - qsc_fir::fir::CallableImpl::Intrinsic => None, - qsc_fir::fir::CallableImpl::SimulatableIntrinsic(spec_decl) => { - find_local_init_expr_in_block(package, spec_decl.block, local_var) - } - qsc_fir::fir::CallableImpl::Spec(spec_impl) => { - find_local_init_expr_in_block(package, spec_impl.body.block, local_var).or_else(|| { - [ - spec_impl.adj.as_ref(), - spec_impl.ctl.as_ref(), - spec_impl.ctl_adj.as_ref(), - ] - .into_iter() - .flatten() - .find_map(|spec| find_local_init_expr_in_block(package, spec.block, local_var)) - }) - } - } + result: Option, } -/// Walks a block's statements looking for the `Local` binding of the -/// requested local variable. -fn find_local_init_expr_in_block( - package: &Package, - block_id: qsc_fir::fir::BlockId, - local_var: LocalVarId, -) -> Option { - let block = package.get_block(block_id); - for &stmt_id in &block.stmts { - let stmt = package.get_stmt(stmt_id); - if let StmtKind::Local(_, pat_id, init_expr_id) = stmt.kind - && pat_binds_local_var(package, pat_id, local_var) +impl<'a> Visitor<'a> for LocalInitFinder<'a> { + fn visit_stmt(&mut self, stmt: StmtId) { + if self.result.is_some() { + return; + } + if let StmtKind::Local(_, pat_id, init_expr_id) = self.package.get_stmt(stmt).kind + && pat_binds_local_var(self.package, pat_id, self.local_var) { - return Some(init_expr_id); + self.result = Some(init_expr_id); + return; } + visit::walk_stmt(self, stmt); + } - let nested = match stmt.kind { - StmtKind::Expr(expr_id) | StmtKind::Semi(expr_id) | StmtKind::Local(_, _, expr_id) => { - find_local_init_expr_in_expr(package, expr_id, local_var) - } - StmtKind::Item(_) => None, - }; - if nested.is_some() { - return nested; + fn visit_expr(&mut self, expr: ExprId) { + if self.result.is_some() { + return; } + visit::walk_expr(self, expr); } - None + fn get_block(&self, id: BlockId) -> &'a Block { + self.package.get_block(id) + } + fn get_expr(&self, id: ExprId) -> &'a Expr { + self.package.get_expr(id) + } + fn get_pat(&self, id: PatId) -> &'a Pat { + self.package.get_pat(id) + } + fn get_stmt(&self, id: StmtId) -> &'a Stmt { + self.package.get_stmt(id) + } } -/// Descends into nested expressions (blocks, conditionals, loops) while -/// searching for a local variable's initializer. -fn find_local_init_expr_in_expr( +/// Locates the initializer expression for a given local variable inside a +/// reachable callable body. +fn find_local_init_expr_in_callable( package: &Package, - expr_id: ExprId, + callable_id: LocalItemId, local_var: LocalVarId, ) -> Option { - let expr = package.get_expr(expr_id); - match &expr.kind { - ExprKind::Array(exprs) | ExprKind::ArrayLit(exprs) | ExprKind::Tuple(exprs) => exprs - .iter() - .find_map(|&expr_id| find_local_init_expr_in_expr(package, expr_id, local_var)), - ExprKind::ArrayRepeat(lhs, rhs) - | ExprKind::Assign(lhs, rhs) - | ExprKind::AssignOp(_, lhs, rhs) - | ExprKind::BinOp(_, lhs, rhs) - | ExprKind::Call(lhs, rhs) - | ExprKind::Index(lhs, rhs) - | ExprKind::AssignField(lhs, _, rhs) - | ExprKind::UpdateField(lhs, _, rhs) => { - find_local_init_expr_in_expr(package, *lhs, local_var) - .or_else(|| find_local_init_expr_in_expr(package, *rhs, local_var)) - } - ExprKind::AssignIndex(a, b, c) | ExprKind::UpdateIndex(a, b, c) => { - find_local_init_expr_in_expr(package, *a, local_var) - .or_else(|| find_local_init_expr_in_expr(package, *b, local_var)) - .or_else(|| find_local_init_expr_in_expr(package, *c, local_var)) - } - ExprKind::Block(block_id) => find_local_init_expr_in_block(package, *block_id, local_var), - ExprKind::Fail(inner) - | ExprKind::Field(inner, _) - | ExprKind::Return(inner) - | ExprKind::UnOp(_, inner) => find_local_init_expr_in_expr(package, *inner, local_var), - ExprKind::If(cond, body, otherwise) => { - find_local_init_expr_in_expr(package, *cond, local_var) - .or_else(|| find_local_init_expr_in_expr(package, *body, local_var)) - .or_else(|| { - otherwise.and_then(|expr_id| { - find_local_init_expr_in_expr(package, expr_id, local_var) - }) - }) - } - ExprKind::Range(start, step, end) => start - .and_then(|expr_id| find_local_init_expr_in_expr(package, expr_id, local_var)) - .or_else(|| { - step.and_then(|expr_id| find_local_init_expr_in_expr(package, expr_id, local_var)) - }) - .or_else(|| { - end.and_then(|expr_id| find_local_init_expr_in_expr(package, expr_id, local_var)) - }), - ExprKind::String(components) => components.iter().find_map(|component| match component { - qsc_fir::fir::StringComponent::Expr(expr_id) => { - find_local_init_expr_in_expr(package, *expr_id, local_var) - } - qsc_fir::fir::StringComponent::Lit(_) => None, - }), - ExprKind::Struct(_, copy, fields) => copy - .and_then(|expr_id| find_local_init_expr_in_expr(package, expr_id, local_var)) - .or_else(|| { - fields - .iter() - .find_map(|field| find_local_init_expr_in_expr(package, field.value, local_var)) - }), - ExprKind::While(cond, block_id) => find_local_init_expr_in_expr(package, *cond, local_var) - .or_else(|| find_local_init_expr_in_block(package, *block_id, local_var)), - ExprKind::Closure(_, _) | ExprKind::Hole | ExprKind::Lit(_) | ExprKind::Var(_, _) => None, - } + let Some(ItemKind::Callable(decl)) = package.items.get(callable_id).map(|item| &item.kind) + else { + return None; + }; + let mut finder = LocalInitFinder { + package, + local_var, + result: None, + }; + walk_callable_impl_bodies(&mut finder, &decl.implementation); + finder.result } /// Removes callable-typed argument locals whose only remaining uses were @@ -936,16 +1442,61 @@ fn find_local_init_expr_in_expr( fn prune_dead_callable_arg_locals( package: &mut Package, rewritten_callable_arg_locals: &FxHashSet<(LocalItemId, LocalVarId)>, + hof_consumed_source_arrays: &FxHashSet<(LocalItemId, LocalVarId)>, ) { + let mut source_arrays: FxHashSet<(LocalItemId, LocalVarId)> = FxHashSet::default(); + + // Closure callable-arrays forwarded and fully consumed by a higher-order + // call are dead source arrays that the direct-path index-read tracer never + // sees. Seed them here so the same closure-bearing removal below prunes the + // now-dead binding rather than leaving blanked closure elements behind. + source_arrays.extend(hof_consumed_source_arrays.iter().copied()); + for &(callable_id, local_var) in rewritten_callable_arg_locals { if !local_var_is_used_in_callable(package, callable_id, local_var) { + // A direct-dispatch callee bound from an indexed read + // (`let op = ops[i]`) leaves its source array potentially dead once + // this local is gone. Record it so the source array can be pruned + // afterward if nothing else reads it. + if let Some(src) = local_index_source_array_local(package, callable_id, local_var) { + source_arrays.insert((callable_id, src)); + } remove_dead_callable_local_from_callable(package, callable_id, local_var); + } else if !local_var_is_read_in_callable(package, callable_id, local_var) + && local_var_has_closure_valued_binding(package, callable_id, local_var) + { + // The local is still mentioned, but only as an assignment target: + // every read was consumed when the call site was rewritten into a + // direct dispatch. When such a write-only local is initialized from + // a partial application (`mutable op = Rx(0.0, _)`), its binding + // holds a closure-tailed block. Left in place, closure cleanup + // blanks that tail and strands an arrow-typed block with no + // producing value. Removing the dead binding and its assignments + // avoids that. Locals bound only to plain callable references + // (`op = H`) carry no closure tail, so they are left untouched to + // preserve existing dead-code behavior. + remove_write_only_callable_local_from_callable(package, callable_id, local_var); + } + } + + // Prune callable-array locals that only fed removed direct-dispatch index + // reads. Restricting removal to a closure-bearing array (`[X, Rx(0.0, _)]`) + // avoids stranding a blanked closure element as an arrow-typed block with + // no producing tail, while plain callable-reference arrays are left in + // place. + for (callable_id, src_var) in source_arrays { + if !local_var_is_read_in_callable(package, callable_id, src_var) + && local_var_has_closure_valued_binding(package, callable_id, src_var) + { + remove_write_only_callable_local_from_callable(package, callable_id, src_var); } } prune_dead_top_level_callable_locals(package); } +/// Builds a map from each expression id to the local callable that owns it, so +/// a rewrite can find the scope an expression belongs to. fn build_expr_owner_lookup(package: &Package) -> FxHashMap { let mut expr_owner_lookup = FxHashMap::default(); @@ -1072,6 +1623,7 @@ pub(crate) fn hoist_expr_into_let( Some((local_var, var_expr)) } +/// Reports whether `local_var` is referenced anywhere in the callable's body. fn local_var_is_used_in_callable( package: &Package, callable_id: LocalItemId, @@ -1095,6 +1647,292 @@ fn local_var_is_used_in_callable( used } +/// Returns `true` when `local_var` is read anywhere in the callable body. +/// +/// A read is any `Var(Res::Local(local_var))` reference other than the direct +/// left-hand side of an assignment (`local_var = ...`). This distinguishes a +/// still-referenced-but-write-only local, whose only remaining mentions are +/// assignment targets, from one that is genuinely observed. +fn local_var_is_read_in_callable( + package: &Package, + callable_id: LocalItemId, + local_var: LocalVarId, +) -> bool { + let Some(ItemKind::Callable(decl)) = package.items.get(callable_id).map(|item| &item.kind) + else { + return false; + }; + + // Assignment left-hand sides are writes, not reads, so gather them first + // and exclude those expression positions from the read scan below. + let mut assign_lhs: FxHashSet = FxHashSet::default(); + crate::walk_utils::for_each_expr_in_callable_impl( + package, + &decl.implementation, + &mut |_expr_id, expr| { + if let ExprKind::Assign(lhs, _) = expr.kind { + assign_lhs.insert(lhs); + } + }, + ); + + let mut read = false; + crate::walk_utils::for_each_expr_in_callable_impl( + package, + &decl.implementation, + &mut |expr_id, expr| { + if matches!(expr.kind, ExprKind::Var(Res::Local(var), _) if var == local_var) + && !assign_lhs.contains(&expr_id) + { + read = true; + } + }, + ); + read +} + +/// Returns `true` when `local_var` is bound or assigned from an expression +/// whose subtree contains a `Closure`. +/// +/// Partial applications lower to a block whose tail is a `Closure`, so this +/// distinguishes a callable local initialized from a partial application from +/// one bound only to plain callable references (`op = H`), which carry no +/// closure and need no dead-binding removal. +fn local_var_has_closure_valued_binding( + package: &Package, + callable_id: LocalItemId, + local_var: LocalVarId, +) -> bool { + let Some(ItemKind::Callable(decl)) = package.items.get(callable_id).map(|item| &item.kind) + else { + return false; + }; + + // The initializer of the `Local` binding. + if let Some(init_expr_id) = find_local_init_expr_in_callable(package, callable_id, local_var) + && expr_subtree_contains_closure(package, init_expr_id) + { + return true; + } + + // Any `local_var = ` assignment whose right-hand side holds a closure. + let mut found = false; + crate::walk_utils::for_each_expr_in_callable_impl( + package, + &decl.implementation, + &mut |_expr_id, expr| { + if let ExprKind::Assign(lhs, rhs) = expr.kind + && matches!(package.get_expr(lhs).kind, ExprKind::Var(Res::Local(var), _) if var == local_var) + && expr_subtree_contains_closure(package, rhs) + { + found = true; + } + }, + ); + found +} + +/// Returns `true` when the expression subtree rooted at `expr_id` contains a +/// `Closure` expression. +fn expr_subtree_contains_closure(package: &Package, expr_id: ExprId) -> bool { + let mut found = false; + crate::walk_utils::for_each_expr(package, expr_id, &mut |_expr_id, expr| { + if matches!(expr.kind, ExprKind::Closure(_, _)) { + found = true; + } + }); + found +} + +/// Returns the local array variable that a callable local is bound from when +/// its initializer is an indexed read (`let op = ops[i]`). +/// +/// Returns `None` when the local has no initializer, or its initializer is not +/// an index into a bare local variable. +fn local_index_source_array_local( + package: &Package, + callable_id: LocalItemId, + local_var: LocalVarId, +) -> Option { + let init_expr_id = find_local_init_expr_in_callable(package, callable_id, local_var)?; + let ExprKind::Index(base, _) = package.get_expr(init_expr_id).kind else { + return None; + }; + if let ExprKind::Var(Res::Local(src), _) = package.get_expr(base).kind { + Some(src) + } else { + None + } +} + +/// Removes a write-only callable local from the given callable's body by +/// deleting its binding and every assignment to it, recursing into nested +/// blocks via [`remove_write_only_callable_local_from_block`]. +/// +/// Unlike [`remove_dead_callable_local_from_callable`], this handles `mutable` +/// bindings and assignment statements, which arise when a callable local is +/// initialized (and possibly reassigned) from partial applications but is only +/// ever consumed through a call site that direct-dispatch rewriting has already +/// replaced. +fn remove_write_only_callable_local_from_callable( + package: &mut Package, + callable_id: LocalItemId, + local_var: LocalVarId, +) { + let Some(ItemKind::Callable(decl)) = package.items.get(callable_id).map(|item| &item.kind) + else { + return; + }; + + let implementation = decl.implementation.clone(); + match implementation { + qsc_fir::fir::CallableImpl::Intrinsic => {} + qsc_fir::fir::CallableImpl::SimulatableIntrinsic(spec_decl) => { + remove_write_only_callable_local_from_block(package, spec_decl.block, local_var); + } + qsc_fir::fir::CallableImpl::Spec(spec_impl) => { + remove_write_only_callable_local_from_block(package, spec_impl.body.block, local_var); + for spec in [spec_impl.adj, spec_impl.ctl, spec_impl.ctl_adj] + .into_iter() + .flatten() + { + remove_write_only_callable_local_from_block(package, spec.block, local_var); + } + } + } +} + +/// Removes the binding and every assignment of a write-only callable local +/// within a block, recursing into nested blocks. +/// +/// Drops `Local` bindings (of any mutability) whose pattern is a simple bind +/// of `local_var`, and drops `local_var = ...` assignment statements. Other +/// statements are retained and their nested blocks are walked so assignments in +/// conditional branches or loop bodies are removed as well. +fn remove_write_only_callable_local_from_block( + package: &mut Package, + block_id: qsc_fir::fir::BlockId, + local_var: LocalVarId, +) { + let stmt_ids = package.get_block(block_id).stmts.clone(); + let mut retained = Vec::with_capacity(stmt_ids.len()); + + for stmt_id in stmt_ids { + let stmt = package.get_stmt(stmt_id); + let remove_stmt = match &stmt.kind { + StmtKind::Local(_, pat_id, _) => { + matches!(&package.get_pat(*pat_id).kind, PatKind::Bind(ident) if ident.id == local_var) + } + StmtKind::Semi(expr_id) | StmtKind::Expr(expr_id) => { + expr_is_assign_to_local(package, *expr_id, local_var) + } + StmtKind::Item(_) => false, + }; + if !remove_stmt { + retained.push(stmt_id); + } + } + + let retained_for_walk = retained.clone(); + package + .blocks + .get_mut(block_id) + .expect("block should exist") + .stmts = retained; + + for stmt_id in retained_for_walk { + if let StmtKind::Expr(expr_id) | StmtKind::Semi(expr_id) | StmtKind::Local(_, _, expr_id) = + package.get_stmt(stmt_id).kind + { + remove_write_only_callable_local_from_expr(package, expr_id, local_var); + } + } +} + +/// Recurses through an expression subtree removing write-only local assignments +/// found inside nested `Block` and `While` bodies. +fn remove_write_only_callable_local_from_expr( + package: &mut Package, + expr_id: ExprId, + local_var: LocalVarId, +) { + let expr_kind = package.get_expr(expr_id).kind.clone(); + match expr_kind { + ExprKind::Block(block_id) => { + remove_write_only_callable_local_from_block(package, block_id, local_var); + } + ExprKind::While(cond, block_id) => { + remove_write_only_callable_local_from_expr(package, cond, local_var); + remove_write_only_callable_local_from_block(package, block_id, local_var); + } + ExprKind::If(cond, body, otherwise) => { + remove_write_only_callable_local_from_expr(package, cond, local_var); + remove_write_only_callable_local_from_expr(package, body, local_var); + if let Some(otherwise) = otherwise { + remove_write_only_callable_local_from_expr(package, otherwise, local_var); + } + } + ExprKind::Array(exprs) | ExprKind::ArrayLit(exprs) | ExprKind::Tuple(exprs) => { + for child in exprs { + remove_write_only_callable_local_from_expr(package, child, local_var); + } + } + ExprKind::ArrayRepeat(a, b) + | ExprKind::AssignOp(_, a, b) + | ExprKind::BinOp(_, a, b) + | ExprKind::Call(a, b) + | ExprKind::Index(a, b) + | ExprKind::AssignField(a, _, b) + | ExprKind::UpdateField(a, _, b) + | ExprKind::Assign(a, b) => { + remove_write_only_callable_local_from_expr(package, a, local_var); + remove_write_only_callable_local_from_expr(package, b, local_var); + } + ExprKind::AssignIndex(a, b, c) | ExprKind::UpdateIndex(a, b, c) => { + remove_write_only_callable_local_from_expr(package, a, local_var); + remove_write_only_callable_local_from_expr(package, b, local_var); + remove_write_only_callable_local_from_expr(package, c, local_var); + } + ExprKind::Fail(inner) + | ExprKind::Field(inner, _) + | ExprKind::Return(inner) + | ExprKind::UnOp(_, inner) => { + remove_write_only_callable_local_from_expr(package, inner, local_var); + } + ExprKind::Range(start, step, end) => { + for child in [start, step, end].into_iter().flatten() { + remove_write_only_callable_local_from_expr(package, child, local_var); + } + } + ExprKind::String(components) => { + for component in components { + if let qsc_fir::fir::StringComponent::Expr(child) = component { + remove_write_only_callable_local_from_expr(package, child, local_var); + } + } + } + ExprKind::Struct(_, copy, fields) => { + if let Some(copy) = copy { + remove_write_only_callable_local_from_expr(package, copy, local_var); + } + for field in fields { + remove_write_only_callable_local_from_expr(package, field.value, local_var); + } + } + ExprKind::Closure(_, _) | ExprKind::Hole | ExprKind::Lit(_) | ExprKind::Var(_, _) => {} + } +} + +/// Returns `true` when `expr_id` is an assignment whose left-hand side is a +/// bare read of `local_var`. +fn expr_is_assign_to_local(package: &Package, expr_id: ExprId, local_var: LocalVarId) -> bool { + if let ExprKind::Assign(lhs, _) = package.get_expr(expr_id).kind { + matches!(package.get_expr(lhs).kind, ExprKind::Var(Res::Local(var), _) if var == local_var) + } else { + false + } +} + /// Removes a specific dead callable local from the given callable's body by /// deleting its `Local` binding and any references that remain, recursing /// into nested blocks via [`remove_dead_callable_local_from_block`]. @@ -1432,6 +2270,7 @@ fn remove_dead_callable_local_from_expr( } } +/// Collects the local variables bound by a pattern into `bound_vars`. fn collect_bound_pat_vars(package: &Package, pat_id: PatId, bound_vars: &mut Vec) { let pat = package.get_pat(pat_id); match &pat.kind { @@ -1445,6 +2284,7 @@ fn collect_bound_pat_vars(package: &Package, pat_id: PatId, bound_vars: &mut Vec } } +/// Reports whether the pattern binds `local_var`. fn pat_binds_local_var(package: &Package, pat_id: PatId, local_var: LocalVarId) -> bool { let mut bound_vars = Vec::new(); collect_bound_pat_vars(package, pat_id, &mut bound_vars); @@ -1453,141 +2293,71 @@ fn pat_binds_local_var(package: &Package, pat_id: PatId, local_var: LocalVarId) .any(|bound_var| bound_var == local_var) } -/// For a local variable bound inside a tuple pattern (e.g., -/// `let (_, callee, _) = tuple_expr`), returns the field position -/// path (e.g., `[1]` for position 1). -fn find_var_tuple_field_path_in_callable( - package: &Package, - callable_id: LocalItemId, +/// [`Visitor`] that records the tuple-field path of a target local the +/// first time it reaches a tuple `Local` binding that introduces it. Direct +/// (non-tuple) bindings are ignored because their path is empty. Recursion +/// stops once a path is found. +struct TupleFieldPathFinder<'a> { + package: &'a Package, local_var: LocalVarId, -) -> Option> { - let item = package.items.get(callable_id)?; - let ItemKind::Callable(decl) = &item.kind else { - return None; - }; - match &decl.implementation { - qsc_fir::fir::CallableImpl::Intrinsic => None, - qsc_fir::fir::CallableImpl::SimulatableIntrinsic(spec_decl) => { - find_var_tuple_field_path_in_block(package, spec_decl.block, local_var) - } - qsc_fir::fir::CallableImpl::Spec(spec_impl) => find_var_tuple_field_path_in_block( - package, - spec_impl.body.block, - local_var, - ) - .or_else(|| { - [ - spec_impl.adj.as_ref(), - spec_impl.ctl.as_ref(), - spec_impl.ctl_adj.as_ref(), - ] - .into_iter() - .flatten() - .find_map(|spec| find_var_tuple_field_path_in_block(package, spec.block, local_var)) - }), - } + result: Option>, } -/// Walks a block's statements looking for a `PatKind::Tuple` binding that -/// contains the requested local variable. -fn find_var_tuple_field_path_in_block( - package: &Package, - block_id: qsc_fir::fir::BlockId, - local_var: LocalVarId, -) -> Option> { - let block = package.get_block(block_id); - for &stmt_id in &block.stmts { - let stmt = package.get_stmt(stmt_id); - if let StmtKind::Local(_, pat_id, _) = stmt.kind - && let Some(path) = find_var_field_path_in_pat(package, pat_id, local_var) +impl<'a> Visitor<'a> for TupleFieldPathFinder<'a> { + fn visit_stmt(&mut self, stmt: StmtId) { + if self.result.is_some() { + return; + } + if let StmtKind::Local(_, pat_id, _) = self.package.get_stmt(stmt).kind + && let Some(path) = find_var_field_path_in_pat(self.package, pat_id, self.local_var) && !path.is_empty() { - return Some(path); + self.result = Some(path); + return; } - // Also descend into nested blocks and control flow - let nested = match stmt.kind { - StmtKind::Expr(expr_id) | StmtKind::Semi(expr_id) | StmtKind::Local(_, _, expr_id) => { - find_var_tuple_field_path_in_expr(package, expr_id, local_var) - } - StmtKind::Item(_) => None, - }; - if nested.is_some() { - return nested; + visit::walk_stmt(self, stmt); + } + + fn visit_expr(&mut self, expr: ExprId) { + if self.result.is_some() { + return; } + visit::walk_expr(self, expr); + } + + fn get_block(&self, id: BlockId) -> &'a Block { + self.package.get_block(id) + } + fn get_expr(&self, id: ExprId) -> &'a Expr { + self.package.get_expr(id) + } + fn get_pat(&self, id: PatId) -> &'a Pat { + self.package.get_pat(id) + } + fn get_stmt(&self, id: StmtId) -> &'a Stmt { + self.package.get_stmt(id) } - None } -/// Descends into nested expressions to find the tuple field path of a local -/// variable binding. -/// -/// Exhaustive over every `ExprKind` that holds nested expressions (mirroring -/// [`find_local_init_expr_in_expr`]) so a tuple-pattern dispatch binding in an -/// operand-position block (for example `let z = { let (_, callee, _) = arr[i]; -/// callee(q) } + 1`) is still found, rather than dropped by a wildcard arm. -fn find_var_tuple_field_path_in_expr( +/// For a local variable bound inside a tuple pattern (e.g., +/// `let (_, callee, _) = tuple_expr`), returns the field position +/// path (e.g., `[1]` for position 1). +fn find_var_tuple_field_path_in_callable( package: &Package, - expr_id: ExprId, + callable_id: LocalItemId, local_var: LocalVarId, -) -> Option> { - let expr = package.get_expr(expr_id); - match &expr.kind { - ExprKind::Array(exprs) | ExprKind::ArrayLit(exprs) | ExprKind::Tuple(exprs) => exprs - .iter() - .find_map(|&expr_id| find_var_tuple_field_path_in_expr(package, expr_id, local_var)), - ExprKind::ArrayRepeat(lhs, rhs) - | ExprKind::Assign(lhs, rhs) - | ExprKind::AssignOp(_, lhs, rhs) - | ExprKind::BinOp(_, lhs, rhs) - | ExprKind::Call(lhs, rhs) - | ExprKind::Index(lhs, rhs) - | ExprKind::AssignField(lhs, _, rhs) - | ExprKind::UpdateField(lhs, _, rhs) => { - find_var_tuple_field_path_in_expr(package, *lhs, local_var) - .or_else(|| find_var_tuple_field_path_in_expr(package, *rhs, local_var)) - } - ExprKind::AssignIndex(a, b, c) | ExprKind::UpdateIndex(a, b, c) => { - find_var_tuple_field_path_in_expr(package, *a, local_var) - .or_else(|| find_var_tuple_field_path_in_expr(package, *b, local_var)) - .or_else(|| find_var_tuple_field_path_in_expr(package, *c, local_var)) - } - ExprKind::Block(block_id) => { - find_var_tuple_field_path_in_block(package, *block_id, local_var) - } - ExprKind::Fail(inner) - | ExprKind::Field(inner, _) - | ExprKind::Return(inner) - | ExprKind::UnOp(_, inner) => find_var_tuple_field_path_in_expr(package, *inner, local_var), - ExprKind::If(cond, body, otherwise) => { - find_var_tuple_field_path_in_expr(package, *cond, local_var) - .or_else(|| find_var_tuple_field_path_in_expr(package, *body, local_var)) - .or_else(|| { - otherwise.and_then(|e| find_var_tuple_field_path_in_expr(package, e, local_var)) - }) - } - ExprKind::Range(start, step, end) => start - .and_then(|e| find_var_tuple_field_path_in_expr(package, e, local_var)) - .or_else(|| step.and_then(|e| find_var_tuple_field_path_in_expr(package, e, local_var))) - .or_else(|| end.and_then(|e| find_var_tuple_field_path_in_expr(package, e, local_var))), - ExprKind::String(components) => components.iter().find_map(|component| match component { - qsc_fir::fir::StringComponent::Expr(expr_id) => { - find_var_tuple_field_path_in_expr(package, *expr_id, local_var) - } - qsc_fir::fir::StringComponent::Lit(_) => None, - }), - ExprKind::Struct(_, copy, fields) => copy - .and_then(|e| find_var_tuple_field_path_in_expr(package, e, local_var)) - .or_else(|| { - fields.iter().find_map(|field| { - find_var_tuple_field_path_in_expr(package, field.value, local_var) - }) - }), - ExprKind::While(cond, block_id) => { - find_var_tuple_field_path_in_expr(package, *cond, local_var) - .or_else(|| find_var_tuple_field_path_in_block(package, *block_id, local_var)) - } - ExprKind::Closure(_, _) | ExprKind::Hole | ExprKind::Lit(_) | ExprKind::Var(_, _) => None, - } +) -> Option> { + let Some(ItemKind::Callable(decl)) = package.items.get(callable_id).map(|item| &item.kind) + else { + return None; + }; + let mut finder = TupleFieldPathFinder { + package, + local_var, + result: None, + }; + walk_callable_impl_bodies(&mut finder, &decl.implementation); + finder.result } /// Recursively finds the tuple field path for a local variable within a @@ -1684,15 +2454,25 @@ fn rewrite_direct_callee( /// (capture_0, ..., capture_n, original_args) : (CaptureTys..., OriginalInputTy) /// ``` /// +/// When the current arguments already match the target callable's declared +/// input (`target_input`) they are left unchanged. This guards the degenerate +/// case where the closure has no runtime captures the target's +/// input was flattened to the bare parameter tuple, so grouping the arguments +/// into a one-element tuple would produce an over-wrapped `((a, b),)` that no +/// longer matches the callee. +/// /// # Mutations /// - Rewrites `args_id`'s `ExprKind` and `Ty` in place to a `Tuple` /// containing capture expressions followed by the original args. /// - Allocates capture `Expr` nodes through `assigner`. -/// - For controlled operations, recurses through control-qubit layers. +/// - For controlled operations, recurses through control-qubit layers, +/// threading the full `target_input` so the base-input match is checked at +/// the innermost, uncontrolled layer. fn rewrite_direct_closure_args( package: &mut Package, args_id: ExprId, captures: &[CapturedVar], + target_input: &Ty, controlled_layers: usize, assigner: &mut Assigner, ) { @@ -1701,7 +2481,14 @@ fn rewrite_direct_closure_args( ExprKind::Tuple(ref elements) if elements.len() > 1 => elements[1], _ => return, }; - rewrite_direct_closure_args(package, inner_id, captures, controlled_layers - 1, assigner); + rewrite_direct_closure_args( + package, + inner_id, + captures, + target_input, + controlled_layers - 1, + assigner, + ); let inner_ty = package.get_expr(inner_id).ty.clone(); let args_mut = package.exprs.get_mut(args_id).expect("args expr not found"); if let Ty::Tuple(ref mut tys) = args_mut.ty @@ -1713,19 +2500,24 @@ fn rewrite_direct_closure_args( } let args_expr = package.get_expr(args_id).clone(); + + // The arguments already match the target's declared input, so there is + // nothing to splice. This covers the no-capture case where wrapping the + // arguments in a one-element tuple would over-wrap a shape the callee + // already expects flat. + if args_expr.ty == *target_input { + return; + } + let capture_ids = allocate_capture_exprs(package, args_expr.span, captures, assigner); let capture_tys: Vec = captures.iter().map(|capture| capture.ty.clone()).collect(); - let preserved_args_id = assigner.next_expr(); - package.exprs.insert( - preserved_args_id, - Expr { - id: preserved_args_id, - span: args_expr.span, - ty: args_expr.ty.clone(), - kind: args_expr.kind, - exec_graph_range: EMPTY_EXEC_RANGE, - }, + let preserved_args_id = alloc_expr( + package, + assigner, + args_expr.ty.clone(), + args_expr.kind, + args_expr.span, ); let mut new_elements = capture_ids; @@ -1902,31 +2694,16 @@ fn create_direct_branch_call( package_direct_lambda, assigner, ); - let args_id = assigner.next_expr(); - package.exprs.insert( - args_id, - Expr { - id: args_id, - span, - ty: args_ty, - kind: args_kind, - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); - - let call_id = assigner.next_expr(); - package.exprs.insert( - call_id, - Expr { - id: call_id, - span, - ty: result_ty.clone(), - kind: ExprKind::Call(callee_id, args_id), - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); + let args_id = alloc_expr(package, assigner, args_ty, args_kind, span); - call_id + alloc_call_expr( + package, + assigner, + callee_id, + args_id, + result_ty.clone(), + span, + ) } /// Assembles the argument-tuple expressions for a direct-call branch, @@ -1981,16 +2758,12 @@ fn build_direct_branch_args_data( assigner, ); - let inner_id = assigner.next_expr(); - package.exprs.insert( - inner_id, - Expr { - id: inner_id, - span: inner_orig.span, - ty: inner_ty.clone(), - kind: inner_kind, - exec_graph_range: EMPTY_EXEC_RANGE, - }, + let inner_id = alloc_expr( + package, + assigner, + inner_ty.clone(), + inner_kind, + inner_orig.span, ); return ( @@ -2006,16 +2779,12 @@ fn build_direct_branch_args_data( let capture_ids = allocate_capture_exprs(package, orig_args.span, captures, assigner); let capture_tys: Vec = captures.iter().map(|capture| capture.ty.clone()).collect(); - let preserved_args_id = assigner.next_expr(); - package.exprs.insert( - preserved_args_id, - Expr { - id: preserved_args_id, - span: orig_args.span, - ty: orig_args.ty.clone(), - kind: orig_args.kind.clone(), - exec_graph_range: EMPTY_EXEC_RANGE, - }, + let preserved_args_id = alloc_expr( + package, + assigner, + orig_args.ty.clone(), + orig_args.kind.clone(), + orig_args.span, ); let mut tuple_items = capture_ids; @@ -2023,68 +2792,527 @@ fn build_direct_branch_args_data( let mut tuple_tys = capture_tys; tuple_tys.push(orig_args.ty.clone()); - (ExprKind::Tuple(tuple_items), Ty::Tuple(tuple_tys)) + (ExprKind::Tuple(tuple_items), Ty::Tuple(tuple_tys)) +} + +/// Rewrites a single call site to use the specialized callable. +/// +/// # Before +/// ```text +/// Call(Var(hof_item), (callable_arg, other_args)) +/// ``` +/// # After +/// ```text +/// Call(Var(specialized_item), (other_args, captures...)) +/// ``` +/// +/// # Mutations +/// - Rewrites the callee via [`rewrite_specialized_callee`]. +/// - Rewrites args via [`rewrite_args`], removing the callable parameter +/// and appending closure captures. +fn rewrite_one( + package: &mut Package, + _package_id: PackageId, + call_site: &CallSite, + param: &CallableParam, + spec_store_id: StoreItemId, + expr_owner_lookup: &FxHashMap, + assigner: &mut Assigner, +) { + let call_expr = package.get_expr(call_site.call_expr_id).clone(); + + let ExprKind::Call(callee_id, args_id) = call_expr.kind else { + return; + }; + + // Replace callee with the specialized callable reference + let spec_item_id = ItemId { + package: spec_store_id.package, + item: spec_store_id.item, + }; + + // Build the new callee type: remove the callable param from the arrow input. + let input_path = callable_param_input_path(package, callee_id, param); + // Count the outer controlled functor layers wrapping this call so the arg + // rewrite can nest closure captures INSIDE the control-level input tuple + // instead of appending them as top-level siblings of the control qubits. + let (_, outer_functor) = peel_body_functors(package, callee_id); + let controlled_layers = usize::from(outer_functor.controlled); + let captures = match &call_site.callable_arg { + ConcreteCallable::Closure { captures, .. } => filter_threaded_rewrite_captures( + package, + resolve_rewrite_captures(package, call_site.arg_expr_id, captures), + ), + _ => Vec::new(), + }; + let new_callee_ty = if !param.hof_input_is_tuple && !param.field_path.is_empty() { + build_specialized_nested_payload_callee_ty(package, callee_id, &input_path, &captures) + } else { + build_specialized_callee_ty(package, callee_id, &input_path, &call_site.callable_arg) + }; + rewrite_specialized_callee(package, callee_id, spec_item_id, new_callee_ty, assigner); + + // Remove the callable argument from the args tuple + // Insert closure captures as extra arguments. + // + // Only the shallow (single field) case short-circuits here: it removes the + // callable directly from the top-level payload. Deeper paths (len >= 2) must + // fall through to `rewrite_args`, which routes to the nested deep-removal + // path (`rewrite_single_arg_nested`) that can strip a callable buried inside + // a nested aggregate and inline a `Var`-bound local initializer. + if !param.hof_input_is_tuple && param.field_path.len() == 1 { + let mut remove_indices = FxHashSet::default(); + if let Some(&field_index) = param.field_path.first() { + remove_indices.insert(field_index); + } + if rewrite_nested_arg_expr_remove_fields_as_payload( + package, + expr_owner_lookup.get(&call_site.call_expr_id).copied(), + args_id, + &remove_indices, + &captures, + assigner, + ) { + return; + } + } + rewrite_args( + package, + call_site.call_expr_id, + args_id, + &input_path, + controlled_layers, + &captures, + expr_owner_lookup, + assigner, + ); +} + +/// Rewrites a single multi-argument higher-order call so it invokes the +/// combined specialization produced on the specialize side. +/// +/// Every arrow argument slot is removed from the call's argument tuple in one +/// pass, and each closure argument's captures are appended in ascending +/// parameter order. The resulting argument tuple and callee type mirror the +/// combined specialization's input pattern built by `remove_callable_params` on +/// the specialize side: surviving arguments keep their order, all captures +/// follow in ascending parameter order, and the tuple flattens to a scalar only +/// when a single argument survives and no captures are appended. +/// +/// `members` must be ordered ascending by parameter position so the appended +/// captures line up with the specialized input pattern. +fn rewrite_multi( + package: &mut Package, + call_expr_id: ExprId, + members: &[(&CallSite, &CallableParam)], + spec_store_id: StoreItemId, + expr_owner_lookup: &FxHashMap, + assigner: &mut Assigner, +) { + let call_expr = package.get_expr(call_expr_id).clone(); + let ExprKind::Call(callee_id, args_id) = call_expr.kind else { + return; + }; + + let spec_item_id = ItemId { + package: spec_store_id.package, + item: spec_store_id.item, + }; + + // Collect the slots to remove and resolve every closure's captures in + // ascending parameter order. + // + // For a multi-parameter HOF the call argument is a tuple of parameters, so + // each member's top-level parameter index selects the slot to drop. For a + // single tuple-valued parameter the whole call argument is that tuple, so + // each member's immediate field index selects the element to drop instead; + // the gate guarantees a single-level field path that covers the tuple. + let uses_tuple_input = members + .first() + .is_none_or(|(call_site, _)| call_site.hof_input_is_tuple); + let mut remove_indices: Vec = Vec::with_capacity(members.len()); + let mut captures: Vec = Vec::new(); + for (call_site, _param) in members { + let remove_idx = if uses_tuple_input { + call_site.top_level_param + } else { + *call_site + .field_path + .first() + .unwrap_or(&call_site.top_level_param) + }; + remove_indices.push(remove_idx); + if let ConcreteCallable::Closure { + captures: member_captures, + .. + } = &call_site.callable_arg + { + captures.extend(member_captures.iter().map(|capture| { + let mut resolved = capture.clone(); + if resolved.expr.is_none() { + resolved.expr = + resolve_capture_expr_from_arg(package, call_site.arg_expr_id, capture.var); + } + resolved + })); + } + } + + // Retarget the callee to the combined specialization with the rebuilt type. + let new_callee_ty = + build_specialized_multi_callee_ty(package, callee_id, &remove_indices, &captures); + rewrite_specialized_callee(package, callee_id, spec_item_id, new_callee_ty, assigner); + + // Rebuild the argument tuple to match the combined input pattern. The + // owner callable lets a non-inline tuple argument be projected through its + // local initializer. + let owner_callable = expr_owner_lookup.get(&call_expr_id).copied(); + rewrite_args_remove_tuple_elements( + package, + args_id, + owner_callable, + &remove_indices, + &captures, + assigner, + ); +} + +/// Finds the single parameter position shared by the members of a forwarded +/// callable array, if there is exactly one repeated, array-typed position. +/// +/// Returns `None` when no position repeats, more than one does, or the repeated +/// position is not array-typed. +fn callable_array_member_position( + members: &[(&CallSite, &CallableParam)], +) -> Option<(usize, Vec)> { + let mut positions: FxHashMap<(usize, Vec), usize> = FxHashMap::default(); + for (_, param) in members { + *positions + .entry((param.top_level_param, param.field_path.clone())) + .or_default() += 1; + } + let repeated = positions + .into_iter() + .filter(|(_, count)| *count >= 2) + .map(|(position, _)| position) + .collect::>(); + let [position] = repeated.as_slice() else { + return None; + }; + members + .iter() + .find(|(_, param)| (param.top_level_param, param.field_path.clone()) == *position) + .and_then(|(_, param)| matches!(param.param_ty, Ty::Array(_)).then(|| position.clone())) +} + +/// Reports whether the forwarded callable array is nested inside a tuple +/// parameter, in which case the nested-field rewrite path is needed instead of +/// the simpler top-level path. +fn callable_array_member_needs_nested_rewrite(members: &[(&CallSite, &CallableParam)]) -> bool { + let Some(position) = callable_array_member_position(members) else { + return false; + }; + members + .iter() + .find(|(_, param)| (param.top_level_param, param.field_path.clone()) == position) + .is_some_and(|(call_site, param)| { + !call_site.hof_input_is_tuple || !param.field_path.is_empty() + }) +} + +/// Rewrites a call that forwards an array of callables so it targets the +/// specialized clone and no longer passes the callable array. +/// +/// Handles the case where several call sites (`members`) share one forwarded +/// callable-array parameter. The callee expression is repointed at +/// `spec_store_id`, the callable-array argument is removed, and any captured +/// values are threaded through as extra arguments. +fn rewrite_callable_array_multi( + package: &mut Package, + call_expr_id: ExprId, + members: &[(&CallSite, &CallableParam)], + spec_store_id: StoreItemId, + expr_owner_lookup: &FxHashMap, + assigner: &mut Assigner, +) { + let call_expr = package.get_expr(call_expr_id).clone(); + let ExprKind::Call(callee_id, args_id) = call_expr.kind else { + return; + }; + + let spec_item_id = ItemId { + package: spec_store_id.package, + item: spec_store_id.item, + }; + + let Some(array_position) = callable_array_member_position(members) else { + return; + }; + let Some((first_call_site, param)) = members + .iter() + .find(|(_, param)| (param.top_level_param, param.field_path.clone()) == array_position) + .copied() + else { + return; + }; + let mut captures = Vec::new(); + let mut remove_indices = FxHashSet::default(); + let mut remove_expr_ids = FxHashSet::default(); + for (call_site, _) in members { + if let Some(&field_index) = call_site.field_path.first() { + remove_indices.insert(field_index); + } + remove_expr_ids.insert(call_site.arg_expr_id); + if let ConcreteCallable::Closure { + captures: member_captures, + .. + } = &call_site.callable_arg + { + captures.extend(resolve_rewrite_captures( + package, + call_site.arg_expr_id, + member_captures, + )); + } + } + + let new_callee_ty = build_nested_callable_array_callee_ty( + package, + callee_id, + first_call_site.hof_input_is_tuple, + param.top_level_param, + &remove_indices, + &captures, + ); + rewrite_specialized_callee(package, callee_id, spec_item_id, new_callee_ty, assigner); + + rewrite_args_remove_nested_callable_fields( + package, + call_expr_id, + args_id, + first_call_site.hof_input_is_tuple, + param.top_level_param, + &remove_indices, + &remove_expr_ids, + &captures, + expr_owner_lookup, + assigner, + ); +} + +/// Computes the callee arrow type for a combined multi-argument rewrite by +/// removing every arrow input slot in `remove_indices` and appending all +/// capture types in parameter order. +fn build_specialized_multi_callee_ty( + package: &Package, + callee_id: ExprId, + remove_indices: &[usize], + captures: &[CapturedVar], +) -> Option { + let callee_expr = package.get_expr(callee_id); + let Ty::Arrow(ref arrow) = callee_expr.ty else { + return None; + }; + let new_input = remove_tys_at_indices(package, &arrow.input, remove_indices, captures); + Some(Ty::Arrow(Box::new(Arrow { + kind: arrow.kind, + input: Box::new(new_input), + output: arrow.output.clone(), + functors: arrow.functors, + }))) +} + +/// Builds the arrow type of the specialized callee after a forwarded callable +/// array nested inside a tuple parameter is removed and capture types are +/// appended. +/// +/// Returns `None` when the callee is not arrow-typed. +fn build_nested_callable_array_callee_ty( + package: &Package, + callee_id: ExprId, + uses_tuple_input: bool, + top_level_param: usize, + remove_indices: &FxHashSet, + captures: &[CapturedVar], +) -> Option { + let callee_expr = package.get_expr(callee_id); + let Ty::Arrow(ref arrow) = callee_expr.ty else { + return None; + }; + + let input_ty = resolve_udt_ty(package, &arrow.input); + let new_input = if uses_tuple_input { + let Ty::Tuple(mut top_level_tys) = input_ty else { + return None; + }; + top_level_tys[top_level_param] = remove_nested_top_level_fields_from_ty( + package, + &top_level_tys[top_level_param], + remove_indices, + ); + top_level_tys.extend(captures.iter().map(|capture| capture.ty.clone())); + Ty::Tuple(top_level_tys) + } else { + let mut ty = remove_nested_top_level_fields_from_ty(package, &input_ty, remove_indices); + if !captures.is_empty() { + let mut tys = vec![ty]; + tys.extend(captures.iter().map(|capture| capture.ty.clone())); + ty = Ty::Tuple(tys); + } + ty + }; + + Some(Ty::Arrow(Box::new(Arrow { + kind: arrow.kind, + input: Box::new(new_input), + output: arrow.output.clone(), + functors: arrow.functors, + }))) +} + +/// Removes the tuple element types at `remove_indices` from `ty`, resolving +/// through any UDT wrappers first. +/// +/// Collapses the result to `Unit` when nothing remains, and to the sole element +/// when exactly one remains, rather than leaving a one-tuple. +fn remove_nested_top_level_fields_from_ty( + package: &Package, + ty: &Ty, + remove_indices: &FxHashSet, +) -> Ty { + let ty = resolve_udt_ty(package, ty); + let Ty::Tuple(tys) = ty else { + return ty; + }; + let remaining: Vec = tys + .into_iter() + .enumerate() + .filter(|(idx, _)| !remove_indices.contains(idx)) + .map(|(_, ty)| ty) + .collect(); + match remaining.as_slice() { + [] => Ty::UNIT, + [single] => single.clone(), + _ => Ty::Tuple(remaining), + } +} + +/// Removes the tuple element types at `remove_indices` and appends the capture +/// types, flattening to a scalar only when a single element survives and no +/// captures are appended, matching the specialize-side input pattern flatten +/// rule in `remove_callable_params`. +fn remove_tys_at_indices( + package: &Package, + ty: &Ty, + remove_indices: &[usize], + captures: &[CapturedVar], +) -> Ty { + let capture_tys: Vec = captures.iter().map(|c| c.ty.clone()).collect(); + let ty = resolve_udt_ty(package, ty); + let Ty::Tuple(tys) = &ty else { + // A multi-argument HOF always has a tuple input. + return ty.clone(); + }; + let remove: FxHashSet = remove_indices.iter().copied().collect(); + let mut remaining: Vec = tys + .iter() + .enumerate() + .filter(|(i, _)| !remove.contains(i)) + .map(|(_, t)| t.clone()) + .collect(); + remaining.extend(capture_tys); + if remaining.len() == 1 && captures.is_empty() { + remaining + .into_iter() + .next() + .expect("single element should exist") + } else { + Ty::Tuple(remaining) + } } -/// Rewrites a single call site to use the specialized callable. -/// -/// # Before -/// ```text -/// Call(Var(hof_item), (callable_arg, other_args)) -/// ``` -/// # After -/// ```text -/// Call(Var(specialized_item), (other_args, captures...)) -/// ``` +/// Removes the top-level tuple elements at `remove_indices` from a call's +/// argument expression and appends closure captures. /// -/// # Mutations -/// - Rewrites the callee via [`rewrite_specialized_callee`]. -/// - Rewrites args via [`rewrite_args`], removing the callable parameter -/// and appending closure captures. -fn rewrite_one( +/// The rebuilt tuple matches the combined specialization's input pattern: +/// surviving arguments keep their order, captures follow in ascending parameter +/// order, and the tuple flattens to a scalar only when a single argument +/// survives and no captures are appended. +fn rewrite_args_remove_tuple_elements( package: &mut Package, - _package_id: PackageId, - call_site: &CallSite, - param: &CallableParam, - spec_store_id: StoreItemId, - expr_owner_lookup: &FxHashMap, + args_id: ExprId, + owner_callable: Option, + remove_indices: &[usize], + captures: &[CapturedVar], assigner: &mut Assigner, ) { - let call_expr = package.get_expr(call_site.call_expr_id).clone(); + let args_expr = package + .exprs + .get(args_id) + .expect("args expr not found") + .clone(); - let ExprKind::Call(callee_id, args_id) = call_expr.kind else { - return; - }; + let remove: FxHashSet = remove_indices.iter().copied().collect(); - // Replace callee with the specialized callable reference - let spec_item_id = ItemId { - package: spec_store_id.package, - item: spec_store_id.item, - }; + if let ExprKind::Tuple(elements) = &args_expr.kind { + let mut new_elements: Vec = elements + .iter() + .enumerate() + .filter(|(i, _)| !remove.contains(i)) + .map(|(_, &id)| id) + .collect(); - // Build the new callee type: remove the callable param from the arrow input. - let input_path = callable_param_input_path(package, callee_id, param); - let new_callee_ty = - build_specialized_callee_ty(package, callee_id, &input_path, &call_site.callable_arg); - rewrite_specialized_callee(package, callee_id, spec_item_id, new_callee_ty, assigner); + let capture_ids = allocate_capture_exprs(package, args_expr.span, captures, assigner); + new_elements.extend(capture_ids); - // Remove the callable argument from the args tuple - // Insert closure captures as extra arguments - let captures = match &call_site.callable_arg { - ConcreteCallable::Closure { captures, .. } => { - resolve_rewrite_captures(package, call_site.arg_expr_id, captures) + let new_ty = remove_tys_at_indices(package, &args_expr.ty, remove_indices, captures); + + if new_elements.len() == 1 && captures.is_empty() { + let single_id = new_elements[0]; + let single_expr = package + .exprs + .get(single_id) + .expect("expr not found") + .clone(); + let args_mut = package.exprs.get_mut(args_id).expect("args expr not found"); + args_mut.kind = single_expr.kind; + args_mut.ty = single_expr.ty; + } else { + let args_mut = package.exprs.get_mut(args_id).expect("args expr not found"); + args_mut.kind = ExprKind::Tuple(new_elements); + args_mut.ty = new_ty; } - _ => Vec::new(), - }; - rewrite_args( - package, - call_site.call_expr_id, - args_id, - &input_path, - &captures, - expr_owner_lookup, - assigner, - ); + return; + } + + // Non-inline argument: a local bound to a tuple or struct literal of + // callables. The callee was already retargeted to the reduced combined + // spec, so the arguments must be reduced to match. Resolve the local's + // initializer and project the surviving slots through the same removal + // helper the per-row nested path uses, then overwrite the argument + // expression in place with the rebuilt aggregate. The now-dead initializer + // binding is pruned later by the dead-callable-local cleanup. + if let ExprKind::Var(Res::Local(local_var), _) = args_expr.kind + && let Some(owner_callable) = owner_callable + && let Some(init_expr_id) = + find_local_init_expr_in_callable(package, owner_callable, local_var) + && let Some((kind, ty)) = remove_top_level_field_from_expr_data( + package, + init_expr_id, + &remove, + captures, + assigner, + ) + { + let args_mut = package.exprs.get_mut(args_id).expect("args expr not found"); + args_mut.kind = kind; + args_mut.ty = ty; + } + + // Any other shape, for example a function-returning-tuple result, is + // outside the combined rewrite's scope and is left untouched; the + // defunctionalization fixpoint surfaces an honest dynamic-callable + // diagnostic for shapes the analysis cannot project. } /// Removes the callable argument selected by `param` from the call arguments @@ -2102,11 +3330,13 @@ fn rewrite_one( /// # Mutations /// - Rewrites `args_id`'s `ExprKind` and `Ty` in place. /// - Allocates capture `Expr` nodes through `assigner`. +#[allow(clippy::too_many_arguments)] fn rewrite_args( package: &mut Package, call_expr_id: ExprId, args_id: ExprId, input_path: &[usize], + controlled_layers: usize, captures: &[CapturedVar], expr_owner_lookup: &FxHashMap, assigner: &mut Assigner, @@ -2130,6 +3360,7 @@ fn rewrite_args( args_id, input_path[0], &input_path[1..], + controlled_layers, captures, assigner, ); @@ -2218,7 +3449,9 @@ fn rewrite_args_remove_tuple_element( } /// Rewrites args for a nested callable inside a top-level tuple input slot. -/// Captures are appended to the top-level args tuple. +/// +/// For an uncontrolled call (`controlled_layers == 0`) any closure captures are +/// appended to the top-level args tuple as siblings of the surviving elements. /// /// # Before /// ```text @@ -2229,16 +3462,30 @@ fn rewrite_args_remove_tuple_element( /// (ctrl_qubits, (inner_arg), capture0, ...) // nested element removed /// ``` /// +/// For a controlled call (`controlled_layers > 0`) each `Controlled` functor +/// wraps the whole input as `([ctls], base_input)` without splitting the base +/// input tuple. Appending captures at the top level would produce a malformed +/// tuple such as `([ctls], (inner_arg), capture0)` whose control-level element +/// is no longer a 2-tuple, which `split_controls_and_input` in `qsc_rca` +/// rejects. Instead the captures are nested INSIDE the deepest input tuple via +/// [`append_captures_beneath_control_layers`], yielding +/// `([ctls], (inner_arg, capture0, ...))`. This lockstep with +/// `rewrite_closure_dispatch_branch_args` in [`super::specialize`] keeps the +/// caller arg shape aligned with the specialized callee's uncontrolled input +/// pattern. +/// /// # Mutations /// - Rewrites the inner element via [`rewrite_local_single_arg_nested`] or /// [`remove_element_at_path`], then updates the outer tuple's type. /// - Allocates capture `Expr` nodes through `assigner`. +#[allow(clippy::too_many_arguments)] fn rewrite_args_nested_tuple_input( package: &mut Package, owner_callable: Option, args_id: ExprId, top_level_param: usize, field_path: &[usize], + controlled_layers: usize, captures: &[CapturedVar], assigner: &mut Assigner, ) { @@ -2262,6 +3509,21 @@ fn rewrite_args_nested_tuple_input( remove_element_at_path(package, inner_id, field_path); } + // Under one or more control layers, nest the captures inside the base + // input tuple beneath the control qubits rather than appending them as + // top-level siblings, refreshing each control tuple's input-slot type on + // the way out. + if !captures.is_empty() && controlled_layers > 0 { + append_captures_beneath_control_layers( + package, + args_id, + controlled_layers, + captures, + assigner, + ); + return; + } + // Read the updated inner type before mutably borrowing the outer. let inner_ty = package .exprs @@ -2292,6 +3554,76 @@ fn rewrite_args_nested_tuple_input( } } +/// Appends closure capture expressions and types into the input tuple nested +/// beneath `controlled_layers` control functor layers. +/// +/// Each `Controlled` functor wraps the whole input as `([ctls], base_input)` +/// (it never splits the base input tuple), so descending into `elements[1]` +/// once per layer reaches the callable's uncontrolled input tuple. The captures +/// are appended LAST inside that base tuple, and each enclosing control tuple's +/// input-slot type (`tys[1]`) is refreshed on the way out so the control-level +/// argument stays a valid 2-tuple. +/// +/// # Before (single control layer) +/// ```text +/// ([ctls], (inner_arg)) +/// ``` +/// # After (single control layer) +/// ```text +/// ([ctls], (inner_arg, capture0, ...)) +/// ``` +/// +/// # Mutations +/// - Rewrites the deepest input tuple's `ExprKind` and `Ty` in place. +/// - Refreshes each enclosing control tuple's input-slot `Ty`. +/// - Allocates capture `Expr` nodes through `assigner`. +fn append_captures_beneath_control_layers( + package: &mut Package, + tuple_id: ExprId, + controlled_layers: usize, + captures: &[CapturedVar], + assigner: &mut Assigner, +) { + if controlled_layers == 0 { + let span = package.get_expr(tuple_id).span; + let capture_ids = allocate_capture_exprs(package, span, captures, assigner); + let capture_tys: Vec = captures.iter().map(|c| c.ty.clone()).collect(); + let tuple_mut = package + .exprs + .get_mut(tuple_id) + .expect("args expr not found"); + if let ExprKind::Tuple(ref mut elems) = tuple_mut.kind { + elems.extend(capture_ids); + } + if let Ty::Tuple(ref mut tys) = tuple_mut.ty { + tys.extend(capture_tys); + } + return; + } + + let inner_id = match package.get_expr(tuple_id).kind { + ExprKind::Tuple(ref elements) if elements.len() > 1 => elements[1], + _ => return, + }; + append_captures_beneath_control_layers( + package, + inner_id, + controlled_layers - 1, + captures, + assigner, + ); + let inner_ty = package.get_expr(inner_id).ty.clone(); + let tuple_mut = package + .exprs + .get_mut(tuple_id) + .expect("args expr not found"); + if let Ty::Tuple(ref mut tys) = tuple_mut.ty + && tys.len() > 1 + { + tys[1] = inner_ty; + } +} + /// Rewrites args when the callable is nested inside the single argument value. /// /// # Before @@ -2328,14 +3660,58 @@ fn rewrite_single_arg_nested( return; } + if field_path.len() == 1 { + let mut remove_indices = FxHashSet::default(); + remove_indices.insert(field_path[0]); + // A top-level callable field can sit inside a struct or tuple literal. + // Rebuild that aggregate so the remaining fields keep the same order as + // the specialized callee's reduced input pattern. + if rewrite_nested_arg_expr_remove_fields_as_payload( + package, + expr_owner_lookup.get(&call_expr_id).copied(), + args_id, + &remove_indices, + captures, + assigner, + ) { + return; + } + if let Some((kind, ty)) = remove_top_level_field_from_expr_data( + package, + args_id, + &remove_indices, + captures, + assigner, + ) { + let args_expr = package.exprs.get_mut(args_id).expect("args expr not found"); + args_expr.kind = kind; + args_expr.ty = ty; + return; + } + } + remove_element_at_path(package, args_id, field_path); if !captures.is_empty() { let span = package.get_expr(args_id).span; let capture_ids = allocate_capture_exprs(package, span, captures, assigner); let modified_expr = package.exprs.get(args_id).expect("expr not found").clone(); - let mut new_elements = match &modified_expr.kind { - ExprKind::Tuple(elems) => elems.clone(), - _ => vec![args_id], + let mut new_elements = if let ExprKind::Tuple(elems) = &modified_expr.kind { + elems.clone() + } else { + // Non-`Tuple` arg (e.g. an unresolved `Var`): copy the current + // payload into a FRESH expr and reference that instead of + // `args_id`. Referencing `args_id` here would make the rewritten + // `Tuple([args_id, ...])` contain itself, producing a + // self-referential expr cycle that overflows any later + // expression-tree walk. + let payload_id = alloc_expr( + package, + assigner, + modified_expr.ty.clone(), + modified_expr.kind.clone(), + span, + ); + vec![payload_id] }; new_elements.extend(capture_ids); let capture_tys: Vec = captures.iter().map(|c| c.ty.clone()).collect(); @@ -2350,6 +3726,230 @@ fn rewrite_single_arg_nested( } } +/// Removes the forwarded callable-array fields from a call's argument tuple and +/// threads any closure captures through, for a call routed to a combined +/// callable-array specialization by [`rewrite_callable_array_multi`]. +/// +/// The callable array sits in one of two argument shapes, selected by +/// `uses_tuple_input`: +/// - A multi-parameter HOF whose input is a tuple of parameters, where the +/// array lives in the `top_level_param` slot of that tuple. +/// - A single tuple-valued parameter whose whole argument *is* that tuple. +/// +/// Fields to drop are identified positionally by `remove_indices` and by the +/// exact forwarded element expression via `remove_expr_ids`, matching the +/// callee-type reduction already applied by +/// [`build_nested_callable_array_callee_ty`]. +/// +/// # Before (`uses_tuple_input`, `top_level_param = 1`) +/// ```text +/// (other_arg, (callable0, callable1, keep), more_args) +/// ``` +/// # After +/// ```text +/// (other_arg, (keep), more_args, capture0, ...) // array fields removed, captures appended +/// ``` +/// +/// # Mutations +/// - Rewrites the inner tuple slot (or `args_id` itself) in place via +/// [`rewrite_nested_arg_expr_remove_fields`]. +/// - Refreshes the outer tuple's slot type and appends closure captures. +/// - Allocates capture `Expr` nodes through `assigner`. +#[allow(clippy::too_many_arguments)] +fn rewrite_args_remove_nested_callable_fields( + package: &mut Package, + call_expr_id: ExprId, + args_id: ExprId, + uses_tuple_input: bool, + top_level_param: usize, + remove_indices: &FxHashSet, + remove_expr_ids: &FxHashSet, + captures: &[CapturedVar], + expr_owner_lookup: &FxHashMap, + assigner: &mut Assigner, +) { + let args_expr = package.get_expr(args_id).clone(); + let owner_callable = expr_owner_lookup.get(&call_expr_id).copied(); + + if uses_tuple_input { + // Multi-parameter HOF: the callable array occupies one slot of the + // top-level argument tuple. Descend into `elements[top_level_param]`, + // strip the callable fields there, then refresh that slot's type and + // append any captures as top-level siblings of the surviving arguments. + if let ExprKind::Tuple(elements) = args_expr.kind { + let inner_id = elements[top_level_param]; + if rewrite_nested_arg_expr_remove_fields( + package, + owner_callable, + inner_id, + remove_indices, + remove_expr_ids, + // No captures here: they belong at the top level (siblings of the + // other parameters), not inside this one parameter's slot, so they + // are appended to the outer `args_id` tuple below instead. + &[], + assigner, + ) { + // Refresh the outer tuple's type for the now-reduced inner slot. + let inner_ty = package.get_expr(inner_id).ty.clone(); + let args_mut = package.exprs.get_mut(args_id).expect("args expr not found"); + if let Ty::Tuple(ref mut tys) = args_mut.ty { + tys[top_level_param] = inner_ty; + } + if !captures.is_empty() { + // Append closure captures (and their types) as top-level + // siblings, matching the combined callee's input pattern. + let capture_ids = + allocate_capture_exprs(package, args_expr.span, captures, assigner); + let capture_tys: Vec = + captures.iter().map(|capture| capture.ty.clone()).collect(); + let args_mut = package.exprs.get_mut(args_id).expect("args expr not found"); + if let ExprKind::Tuple(ref mut elems) = args_mut.kind { + elems.extend(capture_ids); + } + if let Ty::Tuple(ref mut tys) = args_mut.ty { + tys.extend(capture_tys); + } + } + } + } + return; + } + + // Single tuple-valued parameter: the whole argument is the tuple holding the + // callable array, so strip the fields directly from `args_id` and thread the + // captures in the same pass. The returned success flag is unused because + // there is no enclosing slot type to refresh afterward. + let _ = rewrite_nested_arg_expr_remove_fields( + package, + owner_callable, + args_id, + remove_indices, + remove_expr_ids, + captures, + assigner, + ); +} + +/// Rewrites a call's argument expression to drop the removed callable field and +/// thread captured values through, keeping the surviving fields as a single +/// payload element. +/// +/// Returns `true` when the argument was rewritten. When removing the field +/// empties the payload, only the captures are emitted so the argument arity +/// matches the specialized callee's input pattern. +fn rewrite_nested_arg_expr_remove_fields_as_payload( + package: &mut Package, + owner_callable: Option, + args_id: ExprId, + remove_indices: &FxHashSet, + captures: &[CapturedVar], + assigner: &mut Assigner, +) -> bool { + let args_expr = package.get_expr(args_id).clone(); + let source_id = if let ExprKind::Var(Res::Local(local_var), _) = args_expr.kind + && let Some(owner_callable) = owner_callable + && let Some(init_expr_id) = + find_local_init_expr_in_callable(package, owner_callable, local_var) + { + init_expr_id + } else { + args_id + }; + + let Some((payload_kind, payload_ty)) = remove_top_level_field_from_expr_data_with_exprs( + package, + owner_callable, + source_id, + remove_indices, + &FxHashSet::default(), + &[], + assigner, + ) else { + return false; + }; + + if captures.is_empty() { + let args_mut = package.exprs.get_mut(args_id).expect("args expr not found"); + args_mut.kind = payload_kind; + args_mut.ty = payload_ty; + return true; + } + + // Removing the sole tuple field leaves an empty payload. The specialized + // callee drops that emptied slot and keeps only the threaded captures, so + // prepending the empty payload here would produce a longer argument tuple + // than the callee's input pattern. Emit only the captures in that case so + // both sides agree on arity; otherwise keep the surviving payload ahead of + // the captures. + let payload_is_empty = matches!(&payload_kind, ExprKind::Tuple(fields) if fields.is_empty()); + + let capture_ids = allocate_capture_exprs(package, args_expr.span, captures, assigner); + let capture_tys: Vec = captures.iter().map(|capture| capture.ty.clone()).collect(); + + let (mut elements, mut tys) = if payload_is_empty { + (Vec::new(), Vec::new()) + } else { + let payload_id = alloc_expr( + package, + assigner, + payload_ty.clone(), + payload_kind, + args_expr.span, + ); + (vec![payload_id], vec![payload_ty]) + }; + elements.extend(capture_ids); + tys.extend(capture_tys); + + let args_mut = package.exprs.get_mut(args_id).expect("args expr not found"); + args_mut.kind = ExprKind::Tuple(elements); + args_mut.ty = Ty::Tuple(tys); + true +} + +/// Rewrites a call's argument expression to drop the removed callable fields, +/// identified by index or by expression id, and thread captured values through +/// as sibling elements. +/// +/// Returns `true` when the argument was rewritten. +fn rewrite_nested_arg_expr_remove_fields( + package: &mut Package, + owner_callable: Option, + args_id: ExprId, + remove_indices: &FxHashSet, + remove_expr_ids: &FxHashSet, + captures: &[CapturedVar], + assigner: &mut Assigner, +) -> bool { + let source_id = if let ExprKind::Var(Res::Local(local_var), _) = package.get_expr(args_id).kind + && let Some(owner_callable) = owner_callable + && let Some(init_expr_id) = + find_local_init_expr_in_callable(package, owner_callable, local_var) + { + init_expr_id + } else { + args_id + }; + + let Some((kind, ty)) = remove_top_level_field_from_expr_data_with_exprs( + package, + owner_callable, + source_id, + remove_indices, + remove_expr_ids, + captures, + assigner, + ) else { + return false; + }; + + let args_expr = package.exprs.get_mut(args_id).expect("args expr not found"); + args_expr.kind = kind; + args_expr.ty = ty; + true +} + /// Rewrites a single local UDT/tuple argument by replacing the argument use with /// the local initializer after removing the specialized callable field. /// @@ -2373,58 +3973,120 @@ fn rewrite_local_single_arg_nested( captures: &[CapturedVar], assigner: &mut Assigner, ) -> bool { - if field_path.len() != 1 { - return false; + if field_path.len() == 1 { + let mut remove_indices = FxHashSet::default(); + remove_indices.insert(field_path[0]); + return rewrite_nested_arg_expr_remove_fields_as_payload( + package, + owner_callable, + args_id, + &remove_indices, + captures, + assigner, + ); } - let ExprKind::Var(Res::Local(local_var), _) = package.get_expr(args_id).kind else { - return false; - }; - let Some(owner_callable) = owner_callable else { + // Deep path (`field_path.len() >= 2`): the callable lives inside a nested + // aggregate reached through a `Var`-bound local. Resolve the local's + // initializer and deep-strip the callable field, then inline the + // deep-stripped value at the call site. The original local binding becomes + // dead and is removed by later cleanup, so it no longer retains an + // arrow-typed field. + let args_expr = package.get_expr(args_id).clone(); + let source_id = if let ExprKind::Var(Res::Local(local_var), _) = args_expr.kind + && let Some(owner_callable) = owner_callable + && let Some(init_expr_id) = + find_local_init_expr_in_callable(package, owner_callable, local_var) + { + init_expr_id + } else { + // Non-`Var` / unresolved args fall back to the caller's existing + // behavior (the inline-literal deep path is handled there). return false; }; - let Some(init_expr_id) = find_local_init_expr_in_callable(package, owner_callable, local_var) + + let Some((kind, ty)) = build_removed_nested_expr_data(package, source_id, field_path, assigner) else { return false; }; - let Some((kind, ty)) = remove_top_level_field_from_expr_data( - package, - init_expr_id, - field_path[0], - captures, - assigner, - ) else { - return false; - }; - let args_expr = package.exprs.get_mut(args_id).expect("args expr not found"); - args_expr.kind = kind; - args_expr.ty = ty; + if captures.is_empty() { + let args_mut = package.exprs.get_mut(args_id).expect("args expr not found"); + args_mut.kind = kind; + args_mut.ty = ty; + return true; + } + + // Capture-carrying deep local: wrap the deep-stripped payload and append the + // closure captures (capture-LAST flatten), mirroring the len == 1 handling in + // `rewrite_nested_arg_expr_remove_fields_as_payload`. The payload is a FRESH + // expr id, so the rewritten arg never references itself; that avoids the + // self-referential `Tuple([args_id, ...])` cycle the plain fallback in + // `rewrite_single_arg_nested` would otherwise build for a non-`Tuple` `Var` + // arg carrying captures. + let payload_id = alloc_expr(package, assigner, ty.clone(), kind, args_expr.span); + let capture_ids = allocate_capture_exprs(package, args_expr.span, captures, assigner); + let capture_tys: Vec = captures.iter().map(|capture| capture.ty.clone()).collect(); + let mut elements = vec![payload_id]; + elements.extend(capture_ids); + let mut tys = vec![ty]; + tys.extend(capture_tys); + let args_mut = package.exprs.get_mut(args_id).expect("args expr not found"); + args_mut.kind = ExprKind::Tuple(elements); + args_mut.ty = Ty::Tuple(tys); true } /// Builds replacement expression data for a call-argument aggregate after the -/// top-level callable field has been removed. +/// top-level callable fields have been removed. /// /// Before, the tuple or struct represented by `expr_id` still contains the -/// callable-valued field selected by `field_index`. After, the returned -/// `ExprKind`/`Ty` pair describes the same aggregate with that field removed, +/// callable-valued fields selected by `remove_indices`. After, the returned +/// `ExprKind`/`Ty` pair describes the same aggregate with those fields removed, /// collapsed when only one element remains, and widened with any closure /// captures that must become explicit call arguments. fn remove_top_level_field_from_expr_data( package: &mut Package, expr_id: ExprId, - field_index: usize, + remove_indices: &FxHashSet, + captures: &[CapturedVar], + assigner: &mut Assigner, +) -> Option<(ExprKind, Ty)> { + remove_top_level_field_from_expr_data_with_exprs( + package, + None, + expr_id, + remove_indices, + &FxHashSet::default(), + captures, + assigner, + ) +} + +/// Builds replacement expression data for a call-argument aggregate after +/// removing the callable fields at `remove_indices` or `remove_expr_ids` and +/// appending capture expressions. +/// +/// Recurses through a `Call` to its argument tuple, and handles both tuple and +/// struct aggregates. Returns `None` for a shape it does not rewrite. +fn remove_top_level_field_from_expr_data_with_exprs( + package: &mut Package, + owner_callable: Option, + expr_id: ExprId, + remove_indices: &FxHashSet, + remove_expr_ids: &FxHashSet, captures: &[CapturedVar], assigner: &mut Assigner, ) -> Option<(ExprKind, Ty)> { let expr = package.get_expr(expr_id).clone(); let mut remaining = match &expr.kind { ExprKind::Call(_, args_id) => { - return remove_top_level_field_from_expr_data( + return remove_top_level_field_from_expr_data_with_exprs( package, + owner_callable, *args_id, - field_index, + remove_indices, + remove_expr_ids, captures, assigner, ); @@ -2432,13 +4094,31 @@ fn remove_top_level_field_from_expr_data( ExprKind::Tuple(elements) => elements .iter() .enumerate() - .filter(|(idx, _)| *idx != field_index) + .filter(|(idx, expr_id)| { + !remove_indices.contains(idx) && !remove_expr_ids.contains(expr_id) + }) .map(|(_, &expr_id)| expr_id) .collect::>(), - ExprKind::Struct(_, _, fields) => fields + ExprKind::Struct(_, Some(copy_id), fields) => collect_struct_fields_after_removal( + package, + owner_callable, + &expr, + *copy_id, + fields, + remove_indices, + remove_expr_ids, + assigner, + )?, + ExprKind::Struct(_, None, fields) => fields .iter() .filter_map(|field| match &field.field { - Field::Path(path) if path.indices.first() != Some(&field_index) => { + Field::Path(path) + if path + .indices + .first() + .is_none_or(|idx| !remove_indices.contains(idx)) + && !remove_expr_ids.contains(&field.value) => + { Some(field.value) } _ => None, @@ -2454,6 +4134,160 @@ fn remove_top_level_field_from_expr_data( Some(build_expr_data_from_elements(package, remaining)) } +/// Rebuilds the surviving field values of a `Struct` (with-copy) expression +/// after removing selected fields, so a callable field spliced out of a struct +/// literal leaves a well-formed argument aggregate. +/// +/// Explicitly-assigned fields are taken from `fields`; every other field is +/// materialized from the base copy expression via +/// [`materialize_struct_copy_field`]. Fields at `remove_indices`, and any value +/// in `remove_expr_ids`, are dropped. Returns `None` for a shape it cannot +/// decompose. +#[allow(clippy::too_many_arguments)] +fn collect_struct_fields_after_removal( + package: &mut Package, + owner_callable: Option, + expr: &Expr, + copy_id: ExprId, + fields: &[FieldAssign], + remove_indices: &FxHashSet, + remove_expr_ids: &FxHashSet, + assigner: &mut Assigner, +) -> Option> { + let field_tys = struct_top_level_field_tys(package, &expr.ty)?; + let mut explicit_fields: FxHashMap = FxHashMap::default(); + for field in fields { + let Field::Path(path) = &field.field else { + return None; + }; + let Some(&top_level_index) = path.indices.first() else { + continue; + }; + if path.indices.len() != 1 { + return None; + } + explicit_fields.insert(top_level_index, field.value); + } + + let mut remaining = Vec::new(); + for (index, ty) in field_tys.into_iter().enumerate() { + if remove_indices.contains(&index) { + continue; + } + if let Some(&field_id) = explicit_fields.get(&index) { + if !remove_expr_ids.contains(&field_id) { + remaining.push(field_id); + } + continue; + } + let field_id = materialize_struct_copy_field( + package, + owner_callable, + copy_id, + index, + ty, + expr.span, + assigner, + )?; + if !remove_expr_ids.contains(&field_id) { + remaining.push(field_id); + } + } + Some(remaining) +} + +/// Returns the top-level field types of a struct/UDT type, resolving UDT +/// wrappers to their underlying tuple; `None` when the type is not a tuple. +fn struct_top_level_field_tys(package: &Package, ty: &Ty) -> Option> { + match resolve_udt_ty(package, ty) { + Ty::Tuple(tys) => Some(tys), + _ => None, + } +} + +/// Produces an expression for field `field_index` of a struct copy source. +/// +/// Follows the copy expression to its underlying value — a local's initializer +/// (within `owner_callable`), a tuple element, a call's argument tuple, or a +/// nested struct field — returning the concrete sub-expression when one is +/// found, and otherwise synthesizing a `Field` projection of the copy source. +fn materialize_struct_copy_field( + package: &mut Package, + owner_callable: Option, + copy_id: ExprId, + field_index: usize, + field_ty: Ty, + span: Span, + assigner: &mut Assigner, +) -> Option { + let copy_expr = package.get_expr(copy_id).clone(); + match copy_expr.kind { + ExprKind::Var(Res::Local(local_var), _) + if let Some(owner_callable) = owner_callable + && let Some(init_expr_id) = + find_local_init_expr_in_callable(package, owner_callable, local_var) => + { + materialize_struct_copy_field( + package, + Some(owner_callable), + init_expr_id, + field_index, + field_ty, + span, + assigner, + ) + } + ExprKind::Tuple(elements) => elements.get(field_index).copied(), + ExprKind::Call(_, args_id) => materialize_struct_copy_field( + package, + owner_callable, + args_id, + field_index, + field_ty, + span, + assigner, + ), + ExprKind::Struct(_, nested_copy, fields) => { + for field in fields { + let Field::Path(path) = &field.field else { + return None; + }; + if path.indices.as_slice() == [field_index] { + return Some(field.value); + } + } + if let Some(nested_copy) = nested_copy { + materialize_struct_copy_field( + package, + owner_callable, + nested_copy, + field_index, + field_ty, + span, + assigner, + ) + } else { + None + } + } + _ => Some(alloc_expr( + package, + assigner, + field_ty, + ExprKind::Field( + copy_id, + Field::Path(FieldPath { + indices: vec![field_index], + }), + ), + span, + )), + } +} + +/// Builds the `ExprKind` and `Ty` for a tuple of the given elements, collapsing +/// an empty list to `Unit` and a single element to itself rather than a +/// one-tuple. fn build_expr_data_from_elements(package: &Package, elements: Vec) -> (ExprKind, Ty) { match elements.as_slice() { [] => (ExprKind::Tuple(Vec::new()), Ty::UNIT), @@ -2471,6 +4305,86 @@ fn build_expr_data_from_elements(package: &Package, elements: Vec) -> (E } } +/// Builds replacement expression data for a call-argument aggregate after +/// removing the callable-valued field reachable at `field_path`, without +/// mutating any existing expression node. +/// +/// This is the deep (`field_path.len() >= 1`) analogue of +/// [`remove_top_level_field_from_expr_data`]. For each path segment it unwraps a +/// UDT-constructor `Call(ctor, args)` to its argument aggregate, descends into +/// the selected tuple element, and rebuilds each intermediate tuple with a +/// freshly allocated `Expr` for the stripped child while sharing the untouched +/// sibling expression ids unchanged. +/// +/// # Before (`field_path = [0, 0]`, `expr = Config(OpBox(callable, 1), 5)`) +/// ```text +/// Config(OpBox(callable, 1), 5) +/// ``` +/// # After +/// ```text +/// ((1), 5) // the callable at Inner[0] removed, OpBox collapsed to its Id +/// ``` +/// +/// Returns `None` for a `Struct` literal or an out-of-range index. A +/// `Struct`-literal-bound deep local is intentionally left to decline: analysis +/// cannot resolve a concrete callable through a `Struct` literal, so such a call +/// site is already reported as `Qsc.Defunctionalize.DynamicCallable` upstream of +/// this rewrite and never reaches here. Extending this helper to recurse through +/// `Struct` fields would therefore be dead code unless analysis is separately +/// taught to resolve concrete callables through struct literals. +fn build_removed_nested_expr_data( + package: &mut Package, + expr_id: ExprId, + field_path: &[usize], + assigner: &mut Assigner, +) -> Option<(ExprKind, Ty)> { + let expr = package.get_expr(expr_id).clone(); + + // Unwrap a UDT-constructor `Call(ctor, args)` to its argument aggregate. + if let ExprKind::Call(_, inner_args_id) = &expr.kind { + return build_removed_nested_expr_data(package, *inner_args_id, field_path, assigner); + } + + let (&index, rest) = field_path.split_first()?; + + let ExprKind::Tuple(elements) = &expr.kind else { + // `Struct` literals (and any other aggregate form) decline here; see the + // doc comment above for why this is a correct, bounded decline. + return None; + }; + if index >= elements.len() { + return None; + } + let elements = elements.clone(); + + if rest.is_empty() { + // Terminal segment: drop the selected element, collapsing/retyping via + // the shared element builder. + let remaining: Vec = elements + .iter() + .enumerate() + .filter(|(idx, _)| *idx != index) + .map(|(_, &id)| id) + .collect(); + return Some(build_expr_data_from_elements(package, remaining)); + } + + // Deeper segment: recursively strip the nested aggregate at `index`, then + // splice a FRESH `Expr` for it back into a rebuilt tuple so the original + // nested nodes are left untouched. + let (child_kind, child_ty) = + build_removed_nested_expr_data(package, elements[index], rest, assigner)?; + let child_span = package.get_expr(elements[index]).span; + let child_id = alloc_expr(package, assigner, child_ty, child_kind, child_span); + let mut new_elements = elements; + new_elements[index] = child_id; + let new_tys: Vec = new_elements + .iter() + .map(|&id| package.get_expr(id).ty.clone()) + .collect(); + Some((ExprKind::Tuple(new_elements), Ty::Tuple(new_tys))) +} + /// Rewrites a single-parameter call's args expression after the callable /// argument has been removed. /// @@ -2599,25 +4513,225 @@ fn allocate_capture_exprs( for capture in captures { if let Some(expr_id) = capture.expr { - ids.push(expr_id); + if capture.caller_substitutions.is_empty() { + ids.push(expr_id); + } else { + // The capture's initializer is a producer-scope compound + // literal whose inner leaves reference the producing function's + // parameters. Deep-clone it into caller scope, rebinding each + // recorded producer leaf to its caller-scope argument, so no + // unbound producer local is spliced into the caller. + let substitutions: FxHashMap = + capture.caller_substitutions.iter().copied().collect(); + let rebound = clone_capture_literal_with_substitutions( + package, + expr_id, + &substitutions, + assigner, + ); + ids.push(rebound); + } continue; } - let new_id = assigner.next_expr(); - let new_expr = Expr { - id: new_id, - span, - ty: capture.ty.clone(), - kind: ExprKind::Var(Res::Local(capture.var), Vec::new()), - exec_graph_range: EMPTY_EXEC_RANGE, - }; - package.exprs.insert(new_id, new_expr); + let new_id = alloc_local_var_expr(package, assigner, capture.var, capture.ty.clone(), span); ids.push(new_id); } ids } +/// Deep-clones a producer-scope compound-literal capture into caller scope, +/// rebinding its inner producer-parameter leaves to caller-scope arguments. +/// +/// Before, the literal's inner `Var(Res::Local(_))` leaves reference the +/// producing function's parameters, which are unbound in the caller. After, +/// every safe, referentially-transparent node (struct/tuple/array constructors, +/// pure `function` calls, binary/unary operators, field and index accessors, +/// index/field updates, and ranges) is re-allocated with a fresh `ExprId`, each +/// producer leaf recorded in `substitutions` is replaced by the caller-scope +/// argument expression bound to that parameter at the call site (reused as-is, +/// not re-cloned), and every other leaf is cloned verbatim, so the +/// reconstructed literal is rooted entirely in caller-scope values. The set of +/// kinds recursed here must stay harmonized with +/// `collect_compound_capture_substitutions` and the residual-leak decline guard +/// in analysis; a non-pure `Call` (operation callee) is intentionally excluded +/// so its call is not relocated. +#[allow(clippy::too_many_lines)] +fn clone_capture_literal_with_substitutions( + package: &mut Package, + expr_id: ExprId, + substitutions: &FxHashMap, + assigner: &mut Assigner, +) -> ExprId { + let expr = package.get_expr(expr_id).clone(); + + // A substituted producer-parameter leaf resolves directly to its already + // caller-scope argument expression, which is reused unchanged. + if let ExprKind::Var(Res::Local(var), _) = &expr.kind + && let Some(&caller_expr) = substitutions.get(var) + { + return caller_expr; + } + + let new_kind = match &expr.kind { + ExprKind::Tuple(elements) => { + let mut cloned = Vec::with_capacity(elements.len()); + for &elem in elements { + cloned.push(clone_capture_literal_with_substitutions( + package, + elem, + substitutions, + assigner, + )); + } + ExprKind::Tuple(cloned) + } + ExprKind::Array(elements) => { + let mut cloned = Vec::with_capacity(elements.len()); + for &elem in elements { + cloned.push(clone_capture_literal_with_substitutions( + package, + elem, + substitutions, + assigner, + )); + } + ExprKind::Array(cloned) + } + ExprKind::ArrayLit(elements) => { + let mut cloned = Vec::with_capacity(elements.len()); + for &elem in elements { + cloned.push(clone_capture_literal_with_substitutions( + package, + elem, + substitutions, + assigner, + )); + } + ExprKind::ArrayLit(cloned) + } + ExprKind::ArrayRepeat(value, size) => { + let value = + clone_capture_literal_with_substitutions(package, *value, substitutions, assigner); + let size = + clone_capture_literal_with_substitutions(package, *size, substitutions, assigner); + ExprKind::ArrayRepeat(value, size) + } + ExprKind::Struct(name, copy, fields) => { + let copy = (*copy).map(|copy_id| { + clone_capture_literal_with_substitutions(package, copy_id, substitutions, assigner) + }); + let mut cloned_fields = Vec::with_capacity(fields.len()); + for field in fields { + let value = clone_capture_literal_with_substitutions( + package, + field.value, + substitutions, + assigner, + ); + cloned_fields.push(FieldAssign { + span: field.span, + field: field.field.clone(), + value, + }); + } + ExprKind::Struct(*name, copy, cloned_fields) + } + // A `Call` is rebuilt only when its callee is a pure `function`; a + // non-pure operation call falls to the verbatim `other => other` arm so + // its call is never relocated (analysis declines such a capture to a + // dynamic call site before rewrite runs, so this arm is effectively + // unreached for operation callees). + ExprKind::Call(callee, arg) if callee_is_pure_function(package, *callee) => { + let callee = + clone_capture_literal_with_substitutions(package, *callee, substitutions, assigner); + let arg = + clone_capture_literal_with_substitutions(package, *arg, substitutions, assigner); + ExprKind::Call(callee, arg) + } + ExprKind::BinOp(op, lhs, rhs) => { + let lhs = + clone_capture_literal_with_substitutions(package, *lhs, substitutions, assigner); + let rhs = + clone_capture_literal_with_substitutions(package, *rhs, substitutions, assigner); + ExprKind::BinOp(*op, lhs, rhs) + } + ExprKind::UnOp(op, operand) => { + let operand = clone_capture_literal_with_substitutions( + package, + *operand, + substitutions, + assigner, + ); + ExprKind::UnOp(*op, operand) + } + ExprKind::Field(base, field) => { + let base = + clone_capture_literal_with_substitutions(package, *base, substitutions, assigner); + ExprKind::Field(base, field.clone()) + } + ExprKind::Index(base, index) => { + let base = + clone_capture_literal_with_substitutions(package, *base, substitutions, assigner); + let index = + clone_capture_literal_with_substitutions(package, *index, substitutions, assigner); + ExprKind::Index(base, index) + } + ExprKind::UpdateIndex(container, index, value) => { + let container = clone_capture_literal_with_substitutions( + package, + *container, + substitutions, + assigner, + ); + let index = + clone_capture_literal_with_substitutions(package, *index, substitutions, assigner); + let value = + clone_capture_literal_with_substitutions(package, *value, substitutions, assigner); + ExprKind::UpdateIndex(container, index, value) + } + ExprKind::UpdateField(record, field, value) => { + let record = + clone_capture_literal_with_substitutions(package, *record, substitutions, assigner); + let value = + clone_capture_literal_with_substitutions(package, *value, substitutions, assigner); + ExprKind::UpdateField(record, field.clone(), value) + } + ExprKind::Range(start, step, end) => { + let clone_opt = |package: &mut Package, + part: Option, + assigner: &mut Assigner| { + part.map(|part| { + clone_capture_literal_with_substitutions(package, part, substitutions, assigner) + }) + }; + let start = clone_opt(package, *start, assigner); + let step = clone_opt(package, *step, assigner); + let end = clone_opt(package, *end, assigner); + ExprKind::Range(start, step, end) + } + _ => expr.kind.clone(), + }; + + alloc_expr(package, assigner, expr.ty.clone(), new_kind, expr.span) +} + +/// Reports whether a `Call`'s callee resolves to a pure `function`. +/// +/// A Q# `function` is guaranteed side-effect free and its arrow type cannot +/// bear functors, so it is referentially transparent and its call may be +/// relocated or duplicated into caller-scope argument construction. An +/// `operation` may have observable side effects and ordering, so its call must +/// not be relocated. This mirrors the identical gate applied in analysis so the +/// collect / clone / decline-guard sites recurse the same set of `Call` nodes. +fn callee_is_pure_function(package: &Package, callee: ExprId) -> bool { + matches!( + &package.get_expr(callee).ty, + Ty::Arrow(arrow) if arrow.kind == CallableKind::Function + ) +} + /// Computes the callee arrow type that corresponds to a rewritten direct call. /// /// Before, the callee type still includes the callable-valued parameter from @@ -2649,6 +4763,38 @@ fn build_specialized_callee_ty( }))) } +/// Builds the arrow type of the specialized callee after the callable value at +/// `input_path` is removed from the input and capture types are appended. +/// +/// Returns `None` when the callee is not arrow-typed. +fn build_specialized_nested_payload_callee_ty( + package: &Package, + callee_id: ExprId, + input_path: &[usize], + captures: &[CapturedVar], +) -> Option { + let callee_expr = package.get_expr(callee_id); + let Ty::Arrow(ref arrow) = callee_expr.ty else { + return None; + }; + + let payload = remove_ty_at_path(package, &arrow.input, input_path, &[]); + let new_input = if captures.is_empty() { + payload + } else { + let mut tys = vec![payload]; + tys.extend(captures.iter().map(|capture| capture.ty.clone())); + Ty::Tuple(tys) + }; + + Some(Ty::Arrow(Box::new(Arrow { + kind: arrow.kind, + input: Box::new(new_input), + output: arrow.output.clone(), + functors: arrow.functors, + }))) +} + /// Removes the type at a given path from a tuple type and appends capture types. /// For single-element paths, removes the element at that index from the tuple. /// For multi-element paths, navigates into nested tuples to remove the element. @@ -2732,10 +4878,15 @@ fn build_tuple_ty_without_path( remove_ty_at_path(package, ty, param_path, captures) } +/// Reports whether `ty` contains an arrow type after resolving through any UDT +/// wrappers. fn local_ty_contains_arrow_through_udts(package: &Package, ty: &Ty) -> bool { ty_contains_arrow(&resolve_udt_ty(package, ty)) } +/// Resolves a type through user-defined-type wrappers to its underlying +/// structural type, recursing into tuples, arrays, and arrow inputs and +/// outputs. fn resolve_udt_ty(package: &Package, ty: &Ty) -> Ty { match ty { Ty::Udt(Res::Item(item_id)) => { @@ -2764,6 +4915,8 @@ fn resolve_udt_ty(package: &Package, ty: &Ty) -> Ty { } } +/// Computes the argument-tuple path that locates `param` at the given call +/// site, accounting for any functor shell around the callee. fn callable_param_input_path( package: &Package, callee_id: ExprId, @@ -2825,65 +4978,26 @@ fn rewrite_item_callee_with_functor( functor: FunctorApp, assigner: &mut Assigner, ) { - let callee_expr = package.get_expr(callee_id).clone(); + let span = package.get_expr(callee_id).span; + let base_kind = ExprKind::Var(Res::Item(item_id), Vec::new()); if !functor.adjoint && functor.controlled == 0 { let expr = package .exprs .get_mut(callee_id) .expect("callee expr not found"); - expr.kind = ExprKind::Var(Res::Item(item_id), Vec::new()); + expr.kind = base_kind; expr.ty = callee_ty; return; } // Rebuild the functor wrapper chain from the inside out, then copy the // outermost node back into the original callee slot. - let mut current_id = assigner.next_expr(); - package.exprs.insert( - current_id, - Expr { - id: current_id, - span: callee_expr.span, - ty: callee_ty.clone(), - kind: ExprKind::Var(Res::Item(item_id), Vec::new()), - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); - - if functor.adjoint { - let adj_id = assigner.next_expr(); - package.exprs.insert( - adj_id, - Expr { - id: adj_id, - span: callee_expr.span, - ty: callee_ty.clone(), - kind: ExprKind::UnOp(UnOp::Functor(Functor::Adj), current_id), - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); - current_id = adj_id; - } - - for _ in 0..functor.controlled { - let ctl_id = assigner.next_expr(); - package.exprs.insert( - ctl_id, - Expr { - id: ctl_id, - span: callee_expr.span, - ty: callee_ty.clone(), - kind: ExprKind::UnOp(UnOp::Functor(Functor::Ctl), current_id), - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); - current_id = ctl_id; - } - + let outer_id = + alloc_functor_wrapped_expr(package, assigner, base_kind, functor, &callee_ty, span); let outermost_kind = package .exprs - .get(current_id) + .get(outer_id) .expect("specialized callee wrapper should exist") .kind .clone(); @@ -2895,6 +5009,48 @@ fn rewrite_item_callee_with_functor( expr.ty = callee_ty; } +/// Restricts a mixed branch-split candidate set to the single parameter +/// position whose callable is selected by the loop index. +/// +/// A per-row group can mix a dispatched parameter, the same position carrying +/// two or more candidates such as `[H, X]` at slot 0, with siblings at other +/// positions such as a global `Y` at slot 1. Only the dispatched parameter +/// should drive the index dispatch; the siblings stay in the original call +/// arguments and are threaded as runtime values by each specialized leaf. When +/// exactly one position has two or more candidates and at least one sibling +/// exists, the result keeps only that position's entries. The input is returned +/// unchanged when no position is dispatched, or when two or more positions form +/// a genuine product of dispatched parameters. +fn restrict_to_dispatched_parameter<'a>( + entries: &[HofDispatchTarget<'a>], +) -> Vec> { + let mut candidates_per_position: FxHashMap<(usize, Vec), usize> = FxHashMap::default(); + for entry in entries { + *candidates_per_position + .entry((entry.0.top_level_param, entry.0.field_path.clone())) + .or_default() += 1; + } + let dispatched_positions: Vec<(usize, Vec)> = candidates_per_position + .iter() + .filter(|(_, count)| **count >= 2) + .map(|(position, _)| position.clone()) + .collect(); + if dispatched_positions.len() != 1 { + return entries.to_vec(); + } + let kept_position = &dispatched_positions[0]; + let filtered: Vec = entries + .iter() + .filter(|entry| (entry.0.top_level_param, entry.0.field_path.clone()) == *kept_position) + .copied() + .collect(); + if filtered.len() == entries.len() { + entries.to_vec() + } else { + filtered + } +} + /// Rewrites a call site that has multiple callee candidates (from branch-split /// analysis) into an if/elif/else dispatch chain where each branch calls the /// appropriate specialization. @@ -2914,12 +5070,12 @@ fn rewrite_item_callee_with_functor( /// - Replaces `call_expr_id`'s `ExprKind` with the dispatch chain. /// - Allocates per-branch `Call`, callee, args, and `If` `Expr` nodes /// through `assigner`. -#[allow(clippy::too_many_lines)] fn branch_split_rewrite( package: &mut Package, package_id: PackageId, call_expr_id: ExprId, entries: &[HofDispatchTarget], + constants: &[(&CallSite, &CallableParam)], expr_owner_lookup: &FxHashMap, assigner: &mut Assigner, ) { @@ -2930,6 +5086,90 @@ fn branch_split_rewrite( let span = orig_call.span; let result_ty = orig_call.ty.clone(); + // A per-row mixed group, built when defunctionalization cannot prove a + // combined specialization, can mix a dispatched parameter, the same + // position carrying two or more empty-condition candidates such as `[H, X]` + // at slot 0, with a sibling at a different position such as a global `Y` at + // slot 1. The sibling is not selected by the loop index, so it must not + // enter the index-dispatch candidate set; if it does, + // `synthesize_callsite_index_dispatch` cannot locate it among the indexed + // callables, aborts, and the call collapses to a single default, dropping + // the real candidates. + // + // Restrict the candidate set to the single dispatched parameter. The + // sibling at the other position stays in the original arguments, so each + // specialized leaf threads it as a runtime argument in its original + // position through `create_branch_call`, which removes only the dispatch + // slot, preserving call order. + let restricted = restrict_to_dispatched_parameter(entries); + let entries: &[HofDispatchTarget] = &restricted; + + let Some((conditioned, default_entry)) = partition_branch_split_targets( + package, + expr_owner_lookup, + call_expr_id, + entries, + span, + assigner, + ) else { + return; + }; + + if conditioned.is_empty() { + // Single effective entry. For a combined group the entry's spec id is a + // per-candidate combined spec whose input pattern removes every member + // slot, so the single-slot `create_branch_call` would mis-shape the + // call. Bail instead so the fixpoint surfaces an honest diagnostic + // rather than emitting wrong QIR. + if !constants.is_empty() { + return; + } + // Single effective entry — use normal rewrite. + rewrite_one( + package, + package_id, + default_entry.0, + default_entry.2, + default_entry.1, + expr_owner_lookup, + assigner, + ); + return; + } + + install_branch_split_dispatch( + package, + package_id, + call_expr_id, + orig_callee_id, + orig_args_id, + span, + &result_ty, + conditioned, + default_entry, + constants, + assigner, + ); +} + +/// Partitions branch-split dispatch entries into conditioned targets and a +/// single default target for the `else` arm. +/// +/// Entries carrying an explicit condition become conditioned targets; the first +/// empty-condition entry becomes the default. When no entry carries a condition +/// but more than one candidate exists, a synthetic index dispatch is derived +/// from the callee's runtime-selected index via +/// [`synthesize_callsite_index_dispatch`]. If no default is found, the last +/// conditioned target is promoted to serve as the `else` arm. Returns `None` +/// when there is no entry to dispatch at all. +fn partition_branch_split_targets<'a>( + package: &mut Package, + expr_owner_lookup: &FxHashMap, + call_expr_id: ExprId, + entries: &[HofDispatchTarget<'a>], + span: Span, + assigner: &mut Assigner, +) -> Option<(Vec>, HofDispatchTarget<'a>)> { let mut conditioned: Vec = Vec::new(); let mut default: Option = None; for &entry in entries { @@ -2964,48 +5204,70 @@ fn branch_split_rewrite( let default_entry = if let Some(d) = default { d } else { - if conditioned.is_empty() { - return; - } - conditioned.pop().expect("non-empty conditioned").0 + conditioned.pop()?.0 }; + Some((conditioned, default_entry)) +} - if conditioned.is_empty() { - // Single effective entry — use normal rewrite. - rewrite_one( - package, - package_id, - default_entry.0, - default_entry.2, - default_entry.1, - expr_owner_lookup, - assigner, - ); - return; - } - +/// Builds the if/elif/else dispatch chain for a branch-split rewrite and +/// installs it in place of the original call expression. +/// +/// Each conditioned target and the default target are lowered to a specialized +/// call — via [`create_branch_call`] for a per-row group or +/// [`create_combined_branch_call`] when constant sibling parameters are present +/// — and assembled into a nested `if` tree by [`build_branch_tree`]. The +/// resulting dispatch expression's kind and type overwrite `call_expr_id`. +#[allow(clippy::too_many_arguments)] +fn install_branch_split_dispatch<'a>( + package: &mut Package, + package_id: PackageId, + call_expr_id: ExprId, + orig_callee_id: ExprId, + orig_args_id: ExprId, + span: Span, + result_ty: &Ty, + conditioned: Vec>, + default_entry: HofDispatchTarget<'a>, + constants: &[(&CallSite, &CallableParam)], + assigner: &mut Assigner, +) { // Clone original callee and args expressions before modifications. let orig_callee = package.get_expr(orig_callee_id).clone(); let orig_args = package.get_expr(orig_args_id).clone(); let mut build_call = |package: &mut Package, assigner: &mut Assigner, (cs, spec_id, param)| { - create_branch_call( - package, - package_id, - &orig_callee, - &orig_args, - span, - &result_ty, - cs, - param, - spec_id, - assigner, - ) + if constants.is_empty() { + create_branch_call( + package, + package_id, + &orig_callee, + &orig_args, + span, + result_ty, + cs, + param, + spec_id, + assigner, + ) + } else { + create_combined_branch_call( + package, + &orig_callee, + &orig_args, + span, + result_ty, + cs, + param, + constants, + spec_id, + assigner, + ) + } }; let dispatch_id = build_branch_tree( package, span, - &result_ty, + result_ty, conditioned, default_entry, assigner, @@ -3085,32 +5347,238 @@ fn create_branch_call( let (args_kind, args_ty) = build_branch_args_data(package, orig_args, &input_path, &captures, span, assigner); - let args_id = assigner.next_expr(); - package.exprs.insert( + let args_id = alloc_expr(package, assigner, args_ty, args_kind, span); + + // Call expression. + alloc_call_expr( + package, + assigner, + callee_id, args_id, - Expr { - id: args_id, - span, - ty: args_ty, - kind: args_kind, - exec_graph_range: EMPTY_EXEC_RANGE, - }, + result_ty.clone(), + span, + ) +} + +/// Creates a single dispatch leaf for the combined branch-split path, returning +/// its [`ExprId`]. The leaf calls one per-candidate combined specialization +/// formed as `[dispatch candidate] + single-valued siblings`. Every member's +/// argument slot is removed from the call's argument tuple in one pass and each +/// closure member's captures are appended in ascending parameter order, +/// mirroring [`rewrite_multi`] so the leaf's argument shape matches the combined +/// spec's input pattern. The single-valued producer closures are therefore +/// inlined into the leaf in this pass, consumed before any later-iteration body +/// clearing. +/// +/// # Before +/// ```text +/// // one branch's slice of the original HOF call, dispatched candidate `H` at +/// // slot 0 plus a producer-closure sibling at slot 1: +/// Call(Var(hof), (H, makeOp(), other_args)) : result_ty +/// ``` +/// # After +/// ```text +/// // both callable slots removed, the closure's captures appended: +/// Call(Var(combined_spec), (other_args, captures...)) : result_ty +/// ``` +/// +/// # Mutations +/// - Allocates callee, args, and call `Expr` nodes through `assigner`. +#[allow(clippy::too_many_arguments)] +fn create_combined_branch_call( + package: &mut Package, + orig_callee: &Expr, + orig_args: &Expr, + span: Span, + result_ty: &Ty, + candidate: &CallSite, + candidate_param: &CallableParam, + constants: &[(&CallSite, &CallableParam)], + spec_store_id: StoreItemId, + assigner: &mut Assigner, +) -> ExprId { + let spec_item_id = ItemId { + package: spec_store_id.package, + item: spec_store_id.item, + }; + + // Gather this leaf's members: the dispatched candidate plus every + // single-valued sibling. Order them ascending by parameter position so the + // removed slots and the appended captures line up with the combined spec's + // input pattern. + let mut members: Vec<(&CallSite, &CallableParam)> = Vec::with_capacity(constants.len() + 1); + members.push((candidate, candidate_param)); + members.extend(constants.iter().copied()); + members.sort_by(|a, b| { + a.1.top_level_param + .cmp(&b.1.top_level_param) + .then_with(|| a.1.field_path.cmp(&b.1.field_path)) + }); + + // Walk the members to record which argument-tuple slots to drop and, for + // each closure member, resolve the capture expressions to append (in the + // same ascending order), mirroring `rewrite_multi`. + let uses_tuple_input = members.first().is_none_or(|(cs, _)| cs.hof_input_is_tuple); + let mut remove_indices: Vec = Vec::with_capacity(members.len()); + let mut captures: Vec = Vec::new(); + for (cs, _param) in &members { + // A tuple-input HOF removes the whole top-level slot; a single + // tuple-valued parameter removes the immediate field instead. + let remove_idx = if uses_tuple_input { + cs.top_level_param + } else { + *cs.field_path.first().unwrap_or(&cs.top_level_param) + }; + remove_indices.push(remove_idx); + if let ConcreteCallable::Closure { + captures: member_captures, + .. + } = &cs.callable_arg + { + captures.extend(resolve_rewrite_captures( + package, + cs.arg_expr_id, + member_captures, + )); + } + } + + // Build the specialized callee: its arrow type drops the removed slots and + // appends the capture types, and the callee expression names the combined + // spec item. Fall back to the original callee type if the new one cannot be + // computed. + let new_callee_ty = + build_specialized_multi_callee_ty(package, orig_callee.id, &remove_indices, &captures); + let callee_id = alloc_specialized_callee_expr( + package, + orig_callee, + spec_item_id, + &new_callee_ty.unwrap_or_else(|| orig_callee.ty.clone()), + assigner, ); - // Call expression. - let call_id = assigner.next_expr(); - package.exprs.insert( - call_id, - Expr { - id: call_id, + // Build the leaf argument tuple: drop every member slot and append captures. + // The tuple-input and single-tuple-parameter shapes are handled separately. + let (args_kind, args_ty) = if uses_tuple_input { + build_combined_branch_args_data( + package, + orig_args, + &remove_indices, + &captures, span, - ty: result_ty.clone(), - kind: ExprKind::Call(callee_id, args_id), - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); + assigner, + ) + } else { + build_combined_nested_branch_args_data( + package, + orig_args, + &remove_indices, + &captures, + span, + assigner, + ) + }; + // Allocate the new args expression node. + let args_id = alloc_expr(package, assigner, args_ty, args_kind, span); + + // Allocate the call expression that invokes the combined spec with the + // rewritten args, and hand back its id as this dispatch leaf. + alloc_call_expr( + package, + assigner, + callee_id, + args_id, + result_ty.clone(), + span, + ) +} + +/// Builds the `(ExprKind, Ty)` for a combined dispatch leaf's argument tuple: +/// every element at `remove_indices` is dropped and the resolved capture +/// expressions are appended, flattening to a scalar only when a single element +/// survives and no captures are appended, matching the combined spec's input +/// pattern in [`remove_tys_at_indices`]. Surviving element expressions are +/// reused, mirroring the single-slot [`build_branch_args_data`] branch-split +/// behavior. +fn build_combined_branch_args_data( + package: &mut Package, + orig_args: &Expr, + remove_indices: &[usize], + captures: &[CapturedVar], + span: Span, + assigner: &mut Assigner, +) -> (ExprKind, Ty) { + let new_ty = remove_tys_at_indices(package, &orig_args.ty, remove_indices, captures); + match &orig_args.kind { + ExprKind::Tuple(elements) => { + let remove: FxHashSet = remove_indices.iter().copied().collect(); + let mut new_elements: Vec = elements + .iter() + .enumerate() + .filter(|(i, _)| !remove.contains(i)) + .map(|(_, &id)| id) + .collect(); + let capture_ids = allocate_capture_exprs(package, span, captures, assigner); + new_elements.extend(capture_ids); + if new_elements.len() == 1 && captures.is_empty() { + let single_id = new_elements[0]; + let single_expr = package.exprs.get(single_id).expect("expr not found"); + (single_expr.kind.clone(), single_expr.ty.clone()) + } else { + (ExprKind::Tuple(new_elements), new_ty) + } + } + // A combined multi-argument HOF always has a tuple argument; fall back + // to the original kind defensively. + _ => (orig_args.kind.clone(), new_ty), + } +} + +/// Builds the argument expression data for one branch of a combined +/// multi-argument dispatch, removing the callable fields at `remove_indices` and +/// appending capture expressions as a trailing group. +fn build_combined_nested_branch_args_data( + package: &mut Package, + orig_args: &Expr, + remove_indices: &[usize], + captures: &[CapturedVar], + span: Span, + assigner: &mut Assigner, +) -> (ExprKind, Ty) { + let remove: FxHashSet = remove_indices.iter().copied().collect(); + if let Some((payload_kind, payload_ty)) = + remove_top_level_field_from_expr_data(package, orig_args.id, &remove, &[], assigner) + { + if captures.is_empty() { + return (payload_kind, payload_ty); + } + + let payload_id = alloc_expr(package, assigner, payload_ty.clone(), payload_kind, span); - call_id + let capture_ids = allocate_capture_exprs(package, span, captures, assigner); + let capture_tys: Vec = captures.iter().map(|capture| capture.ty.clone()).collect(); + let mut elements = vec![payload_id]; + elements.extend(capture_ids); + let mut tys = vec![payload_ty]; + tys.extend(capture_tys); + return (ExprKind::Tuple(elements), Ty::Tuple(tys)); + } + + let new_ty = remove_nested_top_level_fields_from_ty(package, &orig_args.ty, &remove); + if captures.is_empty() { + (orig_args.kind.clone(), new_ty) + } else { + let capture_ids = allocate_capture_exprs(package, span, captures, assigner); + let capture_tys: Vec = captures.iter().map(|capture| capture.ty.clone()).collect(); + let mut elements = match &orig_args.kind { + ExprKind::Tuple(elements) => elements.clone(), + _ => vec![orig_args.id], + }; + elements.extend(capture_ids); + let mut tys = vec![new_ty]; + tys.extend(capture_tys); + (ExprKind::Tuple(elements), Ty::Tuple(tys)) + } } /// Resolves the defining expressions for the captures referenced in a @@ -3133,6 +5601,56 @@ fn resolve_rewrite_captures( .collect() } +/// Drops a closure's sole capture from the runtime-threaded set when that +/// capture is a statically-known callable. +/// +/// Such a capture is baked directly into the closure target during +/// specialization rather than passed as a runtime operand, so it must not also +/// be threaded as a call argument. Multi-capture closures are returned +/// unchanged. +fn filter_threaded_rewrite_captures( + package: &Package, + captures: Vec, +) -> Vec { + if captures.len() != 1 { + return captures; + } + captures + .into_iter() + .filter(|capture| { + !capture + .expr + .is_some_and(|expr_id| is_known_callable_capture_expr(package, expr_id)) + }) + .collect() +} + +/// Tests whether a capture initializer is a statically-known callable value: a +/// non-generic item reference or a capture-free closure whose own input does +/// not still contain an arrow (see [`callable_input_contains_arrow`]). +fn is_known_callable_capture_expr(package: &Package, expr_id: ExprId) -> bool { + let (base_id, _) = peel_body_functors(package, expr_id); + match &package.get_expr(base_id).kind { + ExprKind::Var(Res::Item(item_id), generic_args) => { + generic_args.is_empty() && !callable_input_contains_arrow(package, item_id.item) + } + ExprKind::Closure(captures, target) => { + captures.is_empty() && !callable_input_contains_arrow(package, *target) + } + _ => false, + } +} + +/// Returns whether the callable's declared input type still contains an arrow +/// (a callable-typed parameter), resolving UDT wrappers first. A missing or +/// non-callable item conservatively reports `true`. +fn callable_input_contains_arrow(package: &Package, callable: LocalItemId) -> bool { + let Some(ItemKind::Callable(decl)) = package.items.get(callable).map(|item| &item.kind) else { + return true; + }; + ty_contains_arrow(&resolve_udt_ty(package, &package.get_pat(decl.input).ty)) +} + /// Resolves a capture expression by inspecting the call's argument tuple, /// used when the capture was passed in directly at the call site. fn resolve_capture_expr_from_arg( @@ -3191,7 +5709,7 @@ fn resolve_capture_expr_from_block( None } -/// Builds a `LocalVarId → ExprId` map from a block's statements, capturing +/// Builds a `LocalVarId => ExprId` map from a block's statements, capturing /// the initializer expressions for every immutable local binding. fn collect_block_local_exprs( package: &Package, @@ -3362,49 +5880,14 @@ fn alloc_item_callee_expr_with_functor( functor: FunctorApp, assigner: &mut Assigner, ) -> ExprId { - let mut current_id = assigner.next_expr(); - package.exprs.insert( - current_id, - Expr { - id: current_id, - span, - ty: callee_ty.clone(), - kind: ExprKind::Var(Res::Item(item_id), Vec::new()), - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); - - if functor.adjoint { - let adj_id = assigner.next_expr(); - package.exprs.insert( - adj_id, - Expr { - id: adj_id, - span, - ty: callee_ty.clone(), - kind: ExprKind::UnOp(UnOp::Functor(Functor::Adj), current_id), - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); - current_id = adj_id; - } - - for _ in 0..functor.controlled { - let ctl_id = assigner.next_expr(); - package.exprs.insert( - ctl_id, - Expr { - id: ctl_id, - span, - ty: callee_ty.clone(), - kind: ExprKind::UnOp(UnOp::Functor(Functor::Ctl), current_id), - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); - current_id = ctl_id; - } - - current_id + alloc_functor_wrapped_expr( + package, + assigner, + ExprKind::Var(Res::Item(item_id), Vec::new()), + functor, + callee_ty, + span, + ) } /// Allocates a new `ExprKind::If` expression and inserts it into the package @@ -3418,18 +5901,15 @@ fn alloc_if_expr( false_id: ExprId, assigner: &mut Assigner, ) -> ExprId { - let if_id = assigner.next_expr(); - package.exprs.insert( - if_id, - Expr { - id: if_id, - span, - ty: result_ty.clone(), - kind: ExprKind::If(cond_id, true_id, Some(false_id)), - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); - if_id + crate::fir_builder::alloc_if_expr( + package, + assigner, + cond_id, + true_id, + Some(false_id), + result_ty.clone(), + span, + ) } /// Builds a nested `if`/`else` tree selecting one of several specialized calls, diff --git a/source/compiler/qsc_fir_transforms/src/defunctionalize/semantic_equivalence_tests.rs b/source/compiler/qsc_fir_transforms/src/defunctionalize/semantic_equivalence_tests.rs index fa3c90c5529..65b94e3fede 100644 --- a/source/compiler/qsc_fir_transforms/src/defunctionalize/semantic_equivalence_tests.rs +++ b/source/compiler/qsc_fir_transforms/src/defunctionalize/semantic_equivalence_tests.rs @@ -4,6 +4,44 @@ use indoc::formatdoc; use proptest::prelude::*; +/// Regression for controlled dispatch of a *capturing* closure passed to a +/// higher-order operation whose callable parameter is **not** the first +/// argument. The HOF applies `Controlled op(ctls, q)`, so rewrite must nest the +/// closure's captures inside the base input tuple beneath the control register +/// (`([ctls], (q, capture0, capture1))`) rather than appending them as trailing +/// top-level siblings of `([ctls], q)`. A mis-placed capture would either crash +/// downstream control/input splitting or diverge from the original semantics. +/// +/// The control qubit is prepared |1> so the controlled rotation actually fires; +/// the captured angles are threaded through a partial application so the closure +/// carries two ordered captures across the control boundary (exercising the +/// multi-capture nesting order, not just placement). +#[test] +fn controlled_capturing_closure_nonzero_param_slot_is_equivalent() { + crate::test_utils::check_semantic_equivalence(indoc::indoc! {r#" + namespace Test { + operation RotOp(a : Double, b : Double, q : Qubit) : Unit is Adj + Ctl { + Rx(a, q); + Rz(b, q); + } + operation ApplyCtl(ctls : Qubit[], op : Qubit => Unit is Ctl, q : Qubit) : Unit { + Controlled op(ctls, q); + } + @EntryPoint() + operation Main() : Result { + use ctl = Qubit(); + use q = Qubit(); + X(ctl); + let a = 3.141592653589793; + let b = 1.5707963267948966; + let op = RotOp(a, b, _); + ApplyCtl([ctl], op, q); + return MResetZ(q); + } + } + "#}); +} + /// Generates syntactically valid Q# programs exercising defunctionalization's /// key code paths: lambda arguments, partial application, and direct callable /// references passed to higher-order functions. @@ -159,3 +197,128 @@ fn multi_multi_shared_callable_across_branches_is_equivalent() { } "#}); } + +/// Regression for the `Single ⊔ Multi` join: a callable-valued local is +/// selected by an outer dynamic `if` whose *true* branch is a single concrete +/// callable (`X`) and whose *false* branch is itself a dynamic conditional that +/// can also yield `X` (under its own guard). +/// +/// The lattice merge must not deduplicate the true-branch `X` against the +/// occurrence already present in the false-branch `Multi` — doing so drops the +/// `outer` dispatch arm and reroutes the `outer == true` path through the +/// false-branch's inner guards instead of unconditionally applying `X`. The +/// fixture pins `outer == true` (`a` is |1>) and the false-branch inner guard +/// `rb == One` false (`b` stays |0>), so the dropped arm is exactly the path +/// taken: the original applies `X(q)` (measuring `One`) while the buggy rewrite +/// falls through to the false-branch default `Z(q)` (measuring `Zero`), +/// diverging in both return value and effect trace. The guards are pure reads +/// of pre-measured `Result` locals so the fixture isolates the lattice merge +/// from condition-hoisting concerns. +#[test] +fn single_multi_shared_callable_across_branches_is_equivalent() { + crate::test_utils::check_semantic_equivalence(indoc::indoc! {r#" + namespace Test { + operation ApplyOp(op : Qubit => Unit is Adj, q : Qubit) : Unit is Adj { + op(q); + } + @EntryPoint() + operation Main() : Result { + use q = Qubit(); + use a = Qubit(); + use b = Qubit(); + // a is |1> so the outer guard is true — op must be the + // true-branch `X`; b stays |0> so the false-branch inner guard + // is false, the arm the identity-dedup would route through. + X(a); + let ra = MResetZ(a); + let rb = MResetZ(b); + let op = if ra == One { + X + } else { + if rb == One { X } else { Z } + }; + ApplyOp(op, q); + return MResetZ(q); + } + } + "#}); +} + +/// Regression for the `Multi ⊔ Multi` join's "unmodified variable" fast path: a +/// callable-valued local is selected by an outer dynamic `if` whose *both* +/// branches are dynamic conditionals that yield the *same set of callables* +/// (`X`/`Z`) but under *different* inner guards (`rb` in the true branch, `rc` +/// in the false branch). +/// +/// The merge must not treat the two branches as an unmodified variable just +/// because the callable identities coincide — the guards differ, so keeping the +/// true-branch chain drops the outer condition and reroutes the `outer == false` +/// path through the true branch's `rb` guard instead of the false branch's `rc` +/// guard. The fixture pins `outer == false` (`a` stays |0>), `rb == One` +/// (`b` is |1>), and `rc == Zero` (`c` stays |0>): the original applies `Z(q)` +/// (measuring `Zero`) while the buggy rewrite applies `X(q)` (measuring `One`), +/// diverging in both return value and effect trace. +#[test] +fn multi_multi_same_callables_different_guards_is_equivalent() { + crate::test_utils::check_semantic_equivalence(indoc::indoc! {r#" + namespace Test { + operation ApplyOp(op : Qubit => Unit is Adj, q : Qubit) : Unit is Adj { + op(q); + } + @EntryPoint() + operation Main() : Result { + use q = Qubit(); + use a = Qubit(); + use b = Qubit(); + use c = Qubit(); + // a stays |0> (outer guard false); b is |1> (rb == One); + // c stays |0> (rc == Zero). + X(b); + let ra = MResetZ(a); + let rb = MResetZ(b); + let rc = MResetZ(c); + let op = if ra == One { + if rb == One { X } else { Z } + } else { + if rc == One { X } else { Z } + }; + ApplyOp(op, q); + return MResetZ(q); + } + } + "#}); +} + +/// Probe: a conditional callable is bound from a guard variable that is then +/// mutated before the callable is applied. The original captures the callable +/// value at binding time (guard true -> `X`); a defunctionalization that +/// re-evaluates the guard at the apply site would read the mutated guard +/// (now false -> `Z`) and diverge. +/// +/// The safe-degradation regression asserting the pipeline rejects this rather +/// than silently miscompiling lives in +/// `defunctionalize::tests::guard_var_reassigned_after_binding_degrades_to_dynamic`. +#[test] +fn guard_var_never_reassigned_after_binding_is_equivalent() { + crate::test_utils::check_semantic_equivalence(indoc::indoc! {r#" + namespace Test { + operation ApplyOp(op : Qubit => Unit is Adj, q : Qubit) : Unit is Adj { + op(q); + } + @EntryPoint() + operation Main() : Result { + use q = Qubit(); + use a = Qubit(); + X(a); + let ra = MResetZ(a); + // `flag` is mutable but never reassigned after the binding, so + // hoisting its read to the apply site is safe and dispatch is + // preserved. + mutable flag = ra == One; + let op = if flag { X } else { Z }; + ApplyOp(op, q); + return MResetZ(q); + } + } + "#}); +} diff --git a/source/compiler/qsc_fir_transforms/src/defunctionalize/specialize.rs b/source/compiler/qsc_fir_transforms/src/defunctionalize/specialize.rs index 79fffb38ac6..048d35c2112 100644 --- a/source/compiler/qsc_fir_transforms/src/defunctionalize/specialize.rs +++ b/source/compiler/qsc_fir_transforms/src/defunctionalize/specialize.rs @@ -22,26 +22,33 @@ //! [`crate::invariants::InvariantLevel::PostDefunc`] invariant that no //! arrow types appear on reachable callable parameters or expressions. -use super::build_spec_key; use super::types::{ AnalysisResult, CallSite, CallableParam, CapturedVar, ConcreteCallable, Error, SpecKey, compose_functors, peel_body_functors, }; -use crate::EMPTY_EXEC_RANGE; +use super::{ + build_combined_spec_key, build_combined_spec_key_for_group, build_spec_key, + has_multiple_forwarded_callable_arrays, is_combined_eligible, partition_mixed_branch_split, +}; use crate::cloner::FirCloner; -use crate::fir_builder::functored_specs; +use crate::fir_builder::{ + alloc_bin_op_expr, alloc_call_expr, alloc_expr, alloc_functor_wrapped_expr, alloc_int_lit, + alloc_item_var_expr, alloc_local_var_expr, functored_specs, +}; use crate::package_assigners::PackageAssigners; use crate::walk_utils::for_each_expr_in_callable_impl; use qsc_data_structures::functors::FunctorApp; use qsc_data_structures::span::Span; use qsc_fir::assigner::Assigner; use qsc_fir::fir::{ - CallableDecl, CallableImpl, Expr, ExprId, ExprKind, Field, FieldPath, Functor, Ident, Item, - ItemId, ItemKind, LocalItemId, LocalVarId, Package, PackageId, PackageLookup, PackageStore, - Pat, PatId, PatKind, Res, StoreItemId, UnOp, Visibility, + BinOp, Block, BlockId, CallableDecl, CallableImpl, Expr, ExprId, ExprKind, Field, FieldPath, + Ident, Item, ItemId, ItemKind, LocalItemId, LocalVarId, Package, PackageId, PackageLookup, + PackageStore, Pat, PatId, PatKind, Res, Stmt, StmtId, StoreItemId, Visibility, }; -use qsc_fir::ty::{Arrow, Ty}; +use qsc_fir::ty::{Arrow, Prim, Ty}; +use qsc_fir::visit::{self, Visitor}; use rustc_hash::{FxHashMap, FxHashSet}; +use std::fmt::Write as _; use std::rc::Rc; /// Maximum number of specializations a single HOF may generate before a @@ -59,6 +66,33 @@ pub(super) const CAPTURE_NAME_PREFIX: &str = "_.capture"; /// destructuring (e.g. `let (op, _) = pair;` makes `op` an alias). type AliasSet = FxHashSet; +/// Closure-capture threading record for one specialized parameter: the closure +/// body's `LocalItemId` paired with the captured variables and their types. +/// `None` marks an argument slot that holds a global callable rather than a +/// closure. +type ClosureInfo = Option; + +/// Per-parameter record produced when a closure argument is specialized: the +/// closure's lifted-lambda target, the runtime captures threaded onto the +/// specialized input as new leading parameters, and any captures that are +/// themselves concrete callables to be baked directly into the target body. +struct ClosureSpecializationInfo { + target: LocalItemId, + capture_bindings: Vec<(LocalVarId, Ty)>, + callable_capture: Option, +} + +/// A captured value that is itself a statically-known callable, recorded so the +/// closure target can be specialized for it — the callable is inlined into the +/// target body and its capture slot removed — rather than threaded as a runtime +/// operand. +struct CapturedCallableSpecialization { + capture_idx: usize, + capture_ty: Ty, + concrete: ConcreteCallable, +} +type SpecializedCaptureKey = (LocalItemId, LocalVarId); + /// Resolves a `ConcreteCallable` to a compact label for inclusion in /// specialized callable names. For globals, produces the callable name /// with a functor prefix when non-body (e.g. `H`, `Adj S`, `Ctl X`). @@ -97,18 +131,277 @@ pub(super) fn specialize( ) -> (FxHashMap, Vec) { let mut dedup: FxHashMap = FxHashMap::default(); let mut errors: Vec = Vec::new(); - let mut recursion_guard: FxHashSet = FxHashSet::default(); - // Build a lookup from each HOF's StoreItemId → CallableParam for quick - // access. Use entry().or_insert() to keep the first (lowest-index) param - // per callable, ensuring deterministic behavior when a callable has - // multiple arrow params. + // Build a lookup from each HOF's StoreItemId => CallableParam. This + // lowest-index entry serves the per-row path, used by single-arrow-param + // HOFs and branch-split candidate sets where every row resolves the same + // parameter; it cannot distinguish between separate arrow parameters. let mut param_lookup: FxHashMap = FxHashMap::default(); for p in &analysis.callable_params { param_lookup.entry(p.callable_id).or_insert(p); } + // Build a precise lookup keyed by parameter position so a multi-argument + // call recovers the exact parameter for each distinct slot instead of + // collapsing every arrow parameter onto the lowest index. + let mut param_by_position: FxHashMap<(StoreItemId, usize, Vec), &CallableParam> = + FxHashMap::default(); + for p in &analysis.callable_params { + param_by_position.insert((p.callable_id, p.top_level_param, p.field_path.clone()), p); + } + + let groups = group_call_sites_by_expression(analysis); + + // Each group is one call expression's rows. The four branches below are + // tried in priority order: the earliest one that claims a group wins, and + // the order matters because the later paths would silently mishandle shapes + // the earlier paths are meant to catch. + for group in &groups { + // 1. Hard-decline the unsupported shape first (two or more callable + // arrays forwarded through one call). Doing this before anything else + // keeps such a group off the per-row path, which would otherwise + // quietly collapse each array down to a single member. + if try_decline_multiple_callable_arrays(store, group, &mut errors) { + continue; + } + + // 2. Combined multi-argument specialization. Specialize and rewrite + // consult the same eligibility predicate so they agree on which call + // sites are combined. The borrow is scoped here so the combined + // branch can re-borrow the store mutably below. + let combined = { + let package = store.get(group[0].call_pkg_id); + is_combined_eligible(package, group) + }; + if combined { + specialize_combined_group(store, group, ¶m_by_position, &mut dedup, assigners); + continue; + } + + // 3. Mixed branch-split: one parameter dispatched over several + // candidates alongside single-valued sibling parameters (at least one + // a producer closure). This must be tried before the per-row path so + // each dispatch leaf inlines the sibling producer closure in the same + // pass; the per-row path would instead emit a lone producer spec that + // the closure-consistency check later rejects. Returns `true` only + // when the group had this shape and could be handled here. + if specialize_mixed_branch_split_group( + store, + group, + ¶m_by_position, + &mut dedup, + assigners, + ) { + continue; + } + + // 4. Fallback: specialize each row independently under its own + // single-argument key. This covers single-arrow-param HOFs and + // branch-split candidate sets, and is the path every group reaches + // when none of the more specific shapes above applied. + specialize_per_row_group( + store, + group, + ¶m_lookup, + ¶m_by_position, + &mut dedup, + &mut errors, + assigners, + ); + } + + report_excessive_specializations(store, &dedup, &mut errors); + + (dedup, errors) +} + +/// Groups analysis call sites by their originating `(package, call expression)`. +/// +/// A multi-argument HOF call contributes one row per arrow parameter; grouping +/// keeps those rows together so a combined specialization can consume them as a +/// unit. Rows that share an expression but resolve the same parameter (the +/// branch-split candidate sets) also land in the same group and are separated +/// later by the per-row path. +fn group_call_sites_by_expression(analysis: &AnalysisResult) -> Vec> { + let mut groups: Vec> = Vec::new(); + let mut group_index: FxHashMap<(PackageId, ExprId), usize> = FxHashMap::default(); for call_site in &analysis.call_sites { + let group_key = (call_site.call_pkg_id, call_site.call_expr_id); + if let Some(&idx) = group_index.get(&group_key) { + groups[idx].push(call_site); + } else { + group_index.insert(group_key, groups.len()); + groups.push(vec![call_site]); + } + } + groups +} + +/// Declines a group that forwards two or more callable arrays through one call, +/// which the pass does not support. +/// +/// Recording the decline here, before the eligibility branch, keeps the group +/// off the per-row path, which would otherwise collapse each array to a single +/// member. The driver revisits skipped groups every fixpoint iteration, so the +/// decline is deduplicated by call-expression span. Returns `true` when the +/// group was declined and the caller should skip it. +fn try_decline_multiple_callable_arrays( + store: &PackageStore, + group: &[&CallSite], + errors: &mut Vec, +) -> bool { + let package = store.get(group[0].call_pkg_id); + if has_multiple_forwarded_callable_arrays(package, group) { + let span = package.get_expr(group[0].call_expr_id).span; + if !errors + .iter() + .any(|e| matches!(e, Error::UnsupportedMultipleCallableArrays(s) if *s == span)) + { + errors.push(Error::UnsupportedMultipleCallableArrays(span)); + } + return true; + } + false +} + +/// Resolves each call site in `members_cs` to its exact [`CallableParam`] by +/// position, ordering the result ascending by parameter position so capture +/// threading and the call-site rewrite agree on operand order. +/// +/// Returns `None` when any member's parameter cannot be resolved. +fn resolve_group_members<'a>( + members_cs: &[&'a CallSite], + hof_store_id: StoreItemId, + param_by_position: &FxHashMap<(StoreItemId, usize, Vec), &'a CallableParam>, +) -> Option> { + let mut members: Vec<(&CallSite, &CallableParam)> = Vec::with_capacity(members_cs.len()); + for cs in members_cs { + let position_key = (hof_store_id, cs.top_level_param, cs.field_path.clone()); + let param = param_by_position.get(&position_key).copied()?; + members.push((*cs, param)); + } + members.sort_by(|a, b| { + a.1.top_level_param + .cmp(&b.1.top_level_param) + .then_with(|| a.1.field_path.cmp(&b.1.field_path)) + }); + Some(members) +} + +/// Specializes a combined multi-argument HOF call keyed by every resolved +/// argument together, inserting the new spec into `dedup`. +/// +/// Recovers the exact parameter for each row via [`resolve_group_members`], then +/// clones the HOF via [`specialize_many`]. Already-specialized keys are skipped. +fn specialize_combined_group( + store: &mut PackageStore, + group: &[&CallSite], + param_by_position: &FxHashMap<(StoreItemId, usize, Vec), &CallableParam>, + dedup: &mut FxHashMap, + assigners: &mut PackageAssigners, +) { + // Combined multi-argument specialization keyed by every resolved argument + // together. + let hof_item_id = group[0].hof_item_id; + let spec_key = build_combined_spec_key_for_group(hof_item_id, group); + + if dedup.contains_key(&spec_key) { + return; + } + + let hof_store_id = StoreItemId::from((hof_item_id.package, hof_item_id.item)); + let Some(members) = resolve_group_members(group, hof_store_id, param_by_position) else { + return; + }; + + let target_pkg_id = group[0].call_pkg_id; + let new_item_id = assigners.with_package(store, target_pkg_id, |store, assigner| { + let mut assigner = assigner; + let result = specialize_many(store, target_pkg_id, &members, &mut assigner); + (assigner, result) + }); + + if let Some(id) = new_item_id { + dedup.insert(spec_key.clone(), id); + } +} + +/// Specializes each dispatch candidate of a mixed branch-split group into a +/// combined per-candidate specialization (`[candidate] + sibling parameters`). +/// +/// A mixed branch-split group has one parameter dispatched over two or more +/// candidates plus one or more single-valued sibling arrow parameters, at least +/// one of which is a producer closure. Specializing each candidate combined with +/// its siblings lets every dispatch leaf inline the single-valued producer +/// closure in the same pass, so the closure is consumed before +/// `track_specialized_closures` or `cleanup_consumed_closures` can clear the +/// producer body on a later iteration. Skipping the per-row path for this group +/// avoids a single-argument producer specialization that the consistency check +/// in `track_specialized_closures` would otherwise reject. +/// +/// Returns `true` when the group is a mixed branch-split that can be claimed by +/// this path (whether or not any candidate produced a spec); `false` leaves the +/// group for the per-row path. +fn specialize_mixed_branch_split_group( + store: &mut PackageStore, + group: &[&CallSite], + param_by_position: &FxHashMap<(StoreItemId, usize, Vec), &CallableParam>, + dedup: &mut FxHashMap, + assigners: &mut PackageAssigners, +) -> bool { + let Some((dispatch, constants)) = partition_mixed_branch_split(group) else { + return false; + }; + let hof_item_id = group[0].hof_item_id; + let hof_store_id = StoreItemId::from((hof_item_id.package, hof_item_id.item)); + for candidate in &dispatch { + let mut members_cs: Vec<&CallSite> = Vec::with_capacity(constants.len() + 1); + members_cs.push(*candidate); + members_cs.extend(constants.iter().copied()); + let spec_key = build_combined_spec_key(hof_item_id, &members_cs); + if dedup.contains_key(&spec_key) { + continue; + } + + let Some(members) = resolve_group_members(&members_cs, hof_store_id, param_by_position) + else { + continue; + }; + + let target_pkg_id = candidate.call_pkg_id; + let new_item_id = assigners.with_package(store, target_pkg_id, |store, assigner| { + let mut assigner = assigner; + let result = specialize_many(store, target_pkg_id, &members, &mut assigner); + (assigner, result) + }); + + if let Some(id) = new_item_id { + dedup.insert(spec_key.clone(), id); + } + } + true +} + +/// Specializes every call site in a group independently under its +/// single-argument spec key (the per-row / branch-split candidate path). +/// +/// Dynamic callables and unresolved parameters record diagnostics or are +/// skipped; each resolved site is cloned via [`specialize_one`], preferring the +/// exact per-position parameter so a per-row spec removes its own slot rather +/// than the lowest-index slot, keeping specialize in agreement with the rewrite +/// side's slot removal at the same parameter position. +#[allow(clippy::too_many_arguments)] +fn specialize_per_row_group( + store: &mut PackageStore, + group: &[&CallSite], + param_lookup: &FxHashMap, + param_by_position: &FxHashMap<(StoreItemId, usize, Vec), &CallableParam>, + dedup: &mut FxHashMap, + errors: &mut Vec, + assigners: &mut PackageAssigners, +) { + for call_site in group { + let call_site: &CallSite = call_site; let spec_key = build_spec_key(call_site); // Already specialized — skip. @@ -126,40 +419,35 @@ pub(super) fn specialize( continue; } - // The HOF may live in a foreign package (e.g. a generic std lib - // callable monomorphized in place into its owning package). Confirm the - // HOF item actually exists in its declared package before proceeding. + // The HOF may live in a foreign package, for example a generic std + // lib callable monomorphized in place into its owning package. let hof_store_id = StoreItemId::from((call_site.hof_item_id.package, call_site.hof_item_id.item)); - if !store - .get(hof_store_id.package) - .items - .contains_key(hof_store_id.item) - { - continue; - } - - // Recursive specialization guard. - if recursion_guard.contains(&spec_key) { - let package = store.get(call_site.call_pkg_id); - let span = package.get_expr(call_site.call_expr_id).span; - errors.push(Error::RecursiveSpecialization(span)); - continue; - } - recursion_guard.insert(spec_key.clone()); - // Look up the callable parameter for this HOF. - let Some(param) = param_lookup.get(&hof_store_id).copied() else { - recursion_guard.remove(&spec_key); + // Look up the callable parameter for this HOF, preferring the exact + // per-position match so a per-row specialization removes its own slot + // rather than the lowest-index slot. This keeps the specialize side + // in agreement with the rewrite side's slot removal at the same + // parameter position. + let position_key = ( + hof_store_id, + call_site.top_level_param, + call_site.field_path.clone(), + ); + let Some(param) = param_by_position + .get(&position_key) + .or_else(|| param_lookup.get(&hof_store_id)) + .copied() + else { continue; }; // Clone the HOF and produce a specialized callable. The spec is - // allocated into the call site's owning package via that package's own - // assigner (mirroring monomorphize's specialize-in-place). When the - // HOF lives in a different package, the cloned body references that - // package's nodes directly; closures threaded as arguments are local - // to the call site's package and so remain locally resolvable. + // allocated into the call site's owning package via that package's + // own assigner, mirroring monomorphize's specialize-in-place. When + // the HOF lives in a different package, the cloned body references + // that package's nodes directly; closures threaded as arguments are + // local to the call site's package and so remain locally resolvable. let target_pkg_id = call_site.call_pkg_id; let new_item_id = assigners.with_package(store, target_pkg_id, |store, assigner| { let mut assigner = assigner; @@ -170,10 +458,20 @@ pub(super) fn specialize( if let Some(id) = new_item_id { dedup.insert(spec_key.clone(), id); } - - recursion_guard.remove(&spec_key); } +} +/// Emits a warning for each HOF whose specialization count exceeds +/// [`EXCESSIVE_SPECIALIZATION_THRESHOLD`]. +/// +/// Groups `dedup` entries by the HOF callable id embedded in each [`SpecKey`] +/// and pushes an [`Error::ExcessiveSpecializations`] for every HOF over the +/// threshold. +fn report_excessive_specializations( + store: &PackageStore, + dedup: &FxHashMap, + errors: &mut Vec, +) { // Count specializations per HOF and emit a warning when the threshold // is exceeded. Group dedup entries by the HOF callable_id embedded in // each SpecKey. @@ -193,8 +491,6 @@ pub(super) fn specialize( errors.push(Error::ExcessiveSpecializations(name, *count, span)); } } - - (dedup, errors) } /// Drives the post-transform retyping cascade across every spec impl of a @@ -270,6 +566,7 @@ fn refresh_stmt_types(package: &mut Package, stmt_id: qsc_fir::fir::StmtId) { /// Recomputes the type of an expression after rewriting, propagating the /// refreshed type through nested blocks, conditionals, calls, and tuple /// constructors. +#[allow(clippy::too_many_lines)] fn refresh_expr_types(package: &mut Package, expr_id: ExprId) -> Ty { let expr = package.get_expr(expr_id).clone(); let new_ty = match expr.kind { @@ -374,38 +671,54 @@ fn refresh_expr_types(package: &mut Package, expr_id: ExprId) -> Ty { new_ty } -/// Clones a HOF callable, transforms its body to replace the callable -/// parameter with the concrete callee, and inserts the specialized callable -/// into the target (`package_id`) package. The HOF body is read from -/// `call_site.hof_item_id.package`, which may differ from the target package. -/// Returns the `StoreItemId` of the new item. -#[allow(clippy::too_many_lines)] -fn specialize_one( +/// Specializes a higher-order function for a group of concrete callable +/// arguments that share one call expression — one row per arrow parameter. +/// +/// `group` must be ordered ascending by parameter position. The HOF is cloned +/// once; every parameter is rewritten against the shared clone so the cleanup +/// that clears a producer closure's body cannot leave a sibling parameter +/// referring to a removed body. Each closure argument's captures are threaded +/// onto the specialized input in ascending parameter order, and each closure's +/// capture-prepend post-pass is scoped to exactly the calls that parameter +/// retargeted so same-target producer closures receive their own captures. +/// +/// Single-row groups delegate to [`specialize_one`] so single-arrow-param +/// specializations stay byte-identical. +fn specialize_many( store: &mut PackageStore, package_id: PackageId, - call_site: &CallSite, - param: &CallableParam, + group: &[(&CallSite, &CallableParam)], assigner: &mut Assigner, ) -> Option { - // Extract needed data from the source package. - // The HOF may live in a different package (e.g. the standard library), - // so use hof_item_id.package rather than the target package_id. - let hof_pkg_id = call_site.hof_item_id.package; - let arg_label = resolve_callable_arg_label(store, &call_site.callable_arg); + if group.len() == 1 { + let (call_site, param) = group[0]; + return specialize_one(store, package_id, call_site, param, assigner); + } + + // Every row shares one call expression and therefore one HOF. + let hof_item_id = group[0].0.hof_item_id; + let hof_pkg_id = hof_item_id.package; + let (body_pkg, decl_snapshot) = { let package = store.get(hof_pkg_id); - let hof_item = package.get_item(call_site.hof_item_id.item); - + let hof_item = package.get_item(hof_item_id.item); let ItemKind::Callable(ref hof_decl) = hof_item.kind else { return None; }; - let body_pkg = extract_callable_body(package, hof_decl); let decl_snapshot = hof_decl.as_ref().clone(); (body_pkg, decl_snapshot) }; // immutable borrow released - // Clone body into target, transform, insert. + // Name the specialization after every argument label in parameter order: + // `HOF{labelA}{labelB}`. + let mut spec_name = decl_snapshot.name.name.to_string(); + for (call_site, _) in group { + let label = resolve_callable_arg_label(store, &call_site.callable_arg); + write!(spec_name, "{{{label}}}").expect("writing to a String is infallible"); + } + let hof_name: Rc = Rc::from(spec_name); + let target = store.get_mut(package_id); let new_item_id = assigner.next_item(); let owned_assigner = std::mem::take(assigner); @@ -417,26 +730,9 @@ fn specialize_one( let cloned_input = cloner.clone_pat(&body_pkg, decl_snapshot.input, target); let cloned_impl = cloner.clone_callable_impl(&body_pkg, &decl_snapshot.implementation, target); - let remapped_param_var = *cloner - .local_map() - .get(¶m.param_var) - .expect("param_var should be in local_map after cloning input first"); - - let remapped_param = CallableParam::new( - param.callable_id, - cloner - .pat_map() - .get(¶m.param_pat_id) - .copied() - .unwrap_or(param.param_pat_id), - param.top_level_param, - param.field_path.clone(), - remapped_param_var, - param.param_ty.clone(), - param.hof_input_is_tuple, - ); + // Remap each parameter through the cloner's maps. + let remapped_params = remap_group_params(&cloner, group); - let hof_name: Rc = Rc::from(format!("{}{{{arg_label}}}", decl_snapshot.name.name)); let mut new_decl = CallableDecl { span: decl_snapshot.span, kind: decl_snapshot.kind, @@ -453,57 +749,38 @@ fn specialize_one( attrs: decl_snapshot.attrs.clone(), }; - // Thread closure captures before recovering the assigner, since - // thread_closure_captures uses the cloner for pat/local allocation. - let closure_info = if let ConcreteCallable::Closure { - ref captures, - target: closure_target, - .. - } = call_site.callable_arg - { - let capture_bindings = thread_closure_captures( - &mut cloner, - target, - &mut new_decl, - &remapped_param, - captures, - ); - Some((closure_target, capture_bindings)) - } else { - None - }; + // Thread each closure argument's captures onto the specialized input in + // ascending parameter order, continuing one capture counter across + // parameters. Capture threading must happen before recovering the assigner + // because it allocates through the cloner. + let (closure_infos, total_captures) = + thread_group_closure_captures(&mut cloner, target, &mut new_decl, group, &remapped_params); - // Recover the assigner from the cloner so all subsequent allocations - // flow through the shared pipeline assigner. + // Recover the assigner from the cloner so all subsequent allocations flow + // through the shared pipeline assigner. *assigner = cloner.into_assigner(); - // Transform the body to replace callable param with the concrete callee. - let impl_clone = new_decl.implementation.clone(); - transform_callable_body( + let callable_array_position = find_callable_array_group_position(&remapped_params, group); + + transform_combined_callable_body( target, package_id, - &impl_clone, - &remapped_param, - &call_site.callable_arg, + group, + &remapped_params, + &closure_infos, + callable_array_position.as_ref(), + &new_decl.implementation, assigner, ); - if let Some((closure_target, capture_bindings)) = closure_info { - rewrite_closure_target_call_args( - target, - &new_decl.implementation, - package_id, - closure_target, - &capture_bindings, - assigner, - ); - } - - // Remove the callable parameter from the input pattern and update types. - remove_callable_param(target, &mut new_decl, &remapped_param); - refresh_rewritten_value_types(target, &new_decl.implementation); + remove_combined_callable_params( + target, + &mut new_decl, + &remapped_params, + callable_array_position.as_ref(), + total_captures, + ); - // Insert the new item. let new_item = Item { id: new_item_id, span: Span::default(), @@ -521,82 +798,196 @@ fn specialize_one( }) } -/// Transforms all specialization bodies in a callable implementation, -/// replacing uses of the callable parameter with direct calls to the concrete -/// callee. -fn transform_callable_body( - package: &mut Package, +/// Remaps each group member's callable parameter through the cloner's id maps +/// so it refers to the freshly cloned input pattern and locals. +fn remap_group_params( + cloner: &FirCloner, + group: &[(&CallSite, &CallableParam)], +) -> Vec { + group + .iter() + .map(|(_, param)| { + let remapped_param_var = *cloner + .local_map() + .get(¶m.param_var) + .expect("param_var should be in local_map after cloning input first"); + CallableParam::new( + param.callable_id, + cloner + .pat_map() + .get(¶m.param_pat_id) + .copied() + .unwrap_or(param.param_pat_id), + param.top_level_param, + param.field_path.clone(), + remapped_param_var, + param.param_ty.clone(), + param.hof_input_is_tuple, + ) + }) + .collect() +} + +/// Threads each closure argument's captures onto the combined spec input in +/// ascending parameter order, continuing one capture counter across parameters. +/// +/// Capture threading allocates through the cloner, so it must run before the +/// caller recovers the pipeline assigner. Returns the per-parameter +/// [`ClosureInfo`] (present only for closure arguments) and the total number of +/// capture slots threaded. +fn thread_group_closure_captures( + cloner: &mut FirCloner, + target: &mut Package, + new_decl: &mut CallableDecl, + group: &[(&CallSite, &CallableParam)], + remapped_params: &[CallableParam], +) -> (Vec, usize) { + let mut closure_infos: Vec = Vec::with_capacity(group.len()); + let mut capture_offset = 0usize; + for ((call_site, _), remapped_param) in group.iter().zip(remapped_params.iter()) { + if let ConcreteCallable::Closure { + ref captures, + target: closure_target, + .. + } = call_site.callable_arg + { + let callable_capture = captured_callable_specialization(target, captures); + let captures_to_thread: Vec = captures + .iter() + .enumerate() + .filter(|(idx, _)| { + callable_capture + .as_ref() + .is_none_or(|callable_capture| callable_capture.capture_idx != *idx) + }) + .map(|(_, capture)| capture.clone()) + .collect(); + let capture_bindings = thread_closure_captures( + cloner, + target, + new_decl, + remapped_param, + &captures_to_thread, + capture_offset, + ); + capture_offset += capture_bindings.len(); + closure_infos.push(Some(ClosureSpecializationInfo { + target: closure_target, + capture_bindings, + callable_capture, + })); + } else { + closure_infos.push(None); + } + } + (closure_infos, capture_offset) +} + +/// Transforms the combined spec body once per specialized parameter, replacing +/// each arrow parameter's calls with its concrete callable. +/// +/// A single dedup set is shared across parameters so a lifted lambda captured by +/// more than one parameter is specialized once. For a closure parameter the +/// capture-prepend is scoped to exactly the calls that parameter retargeted (the +/// set difference of calls to the closure target before and after the +/// transform), keeping same-target producer closures from double-prepending each +/// other's captures. Callable-array members skip the scoped prepend because +/// their captures are threaded through the array element rewrite instead. +#[allow(clippy::too_many_arguments)] +fn transform_combined_callable_body( + target: &mut Package, package_id: PackageId, + group: &[(&CallSite, &CallableParam)], + remapped_params: &[CallableParam], + closure_infos: &[ClosureInfo], + callable_array_position: Option<&(usize, Vec)>, callable_impl: &CallableImpl, - param: &CallableParam, - concrete: &ConcreteCallable, assigner: &mut Assigner, ) { - let mut alias_set = AliasSet::default(); - // A callable's functored specs share one lifted lambda item. When a closure - // captures the param being specialized, the shared lambda must be - // specialized only once, or sibling specs would corrupt it; each spec's - // closure still drops its own capture independently. Track which lambda - // targets are already specialized to enforce this. - let mut specialized_capture_targets: FxHashSet = FxHashSet::default(); - match callable_impl { - CallableImpl::Intrinsic => {} - CallableImpl::Spec(spec_impl) => { - transform_block( - package, + let impl_clone = callable_impl.clone(); + let mut specialized_capture_targets: FxHashSet = FxHashSet::default(); + let concrete_group = callable_array_position + .map(|position| { + group + .iter() + .zip(remapped_params.iter()) + .zip(closure_infos.iter()) + .filter(|&(((_, _), remapped_param), _)| { + ( + remapped_param.top_level_param, + remapped_param.field_path.clone(), + ) == *position + }) + .map(|(((call_site, _), _), closure_info)| { + if let Some(info) = closure_info { + concrete_with_threaded_captures( + &call_site.callable_arg, + &info.capture_bindings, + ) + } else { + call_site.callable_arg.clone() + } + }) + .collect::>() + }) + .unwrap_or_default(); + for (((call_site, _), remapped_param), closure_info) in group + .iter() + .zip(remapped_params.iter()) + .zip(closure_infos.iter()) + { + if let Some(info) = closure_info { + specialize_closure_target_callable_capture( + target, package_id, - spec_impl.body.block, - param, - concrete, - &mut alias_set, + info.target, + info.callable_capture.as_ref(), + assigner, + ); + let is_callable_array_member = callable_array_position.is_some_and(|position| { + ( + remapped_param.top_level_param, + remapped_param.field_path.clone(), + ) == *position + }); + let before = if is_callable_array_member { + FxHashSet::default() + } else { + collect_calls_to_closure_target(target, &impl_clone, package_id, info.target) + }; + let concrete = + concrete_with_threaded_captures(&call_site.callable_arg, &info.capture_bindings); + transform_callable_body( + target, + package_id, + &impl_clone, + remapped_param, + &concrete, + &concrete_group, &mut specialized_capture_targets, assigner, ); - if let Some(ref adj) = spec_impl.adj { - transform_block( - package, + if !is_callable_array_member { + let after = + collect_calls_to_closure_target(target, &impl_clone, package_id, info.target); + let retargeted: Vec = after.difference(&before).copied().collect(); + prepend_captures_to_calls( + target, + &retargeted, package_id, - adj.block, - param, - concrete, - &mut alias_set, - &mut specialized_capture_targets, + info.target, + &info.capture_bindings, assigner, ); } - if let Some(ref ctl) = spec_impl.ctl { - transform_block( - package, - package_id, - ctl.block, - param, - concrete, - &mut alias_set, - &mut specialized_capture_targets, - assigner, - ); - } - if let Some(ref ctl_adj) = spec_impl.ctl_adj { - transform_block( - package, - package_id, - ctl_adj.block, - param, - concrete, - &mut alias_set, - &mut specialized_capture_targets, - assigner, - ); - } - } - CallableImpl::SimulatableIntrinsic(spec_decl) => { - transform_block( - package, + } else { + transform_callable_body( + target, package_id, - spec_decl.block, - param, - concrete, - &mut alias_set, + &impl_clone, + remapped_param, + &call_site.callable_arg, + &concrete_group, &mut specialized_capture_targets, assigner, ); @@ -604,576 +995,2121 @@ fn transform_callable_body( } } -/// Recursively walks a block, transforming call expressions that invoke the -/// callable parameter. -#[allow(clippy::too_many_arguments)] -fn transform_block( - package: &mut Package, - package_id: PackageId, - block_id: qsc_fir::fir::BlockId, - param: &CallableParam, - concrete: &ConcreteCallable, - alias_set: &mut AliasSet, - specialized_capture_targets: &mut FxHashSet, - assigner: &mut Assigner, +/// Removes every specialized arrow parameter slot from the combined spec in a +/// single pass and refreshes value types. +/// +/// A tuple-valued parameter specialized through all of its arrow fields leaves +/// a dead `let (a, b, …) = ops` destructuring that still references the slot +/// about to be removed; those statements are dropped first. Callable-array +/// specializations remove their nested slots in descending position order so an +/// earlier removal does not shift a later slot. +fn remove_combined_callable_params( + target: &mut Package, + new_decl: &mut CallableDecl, + remapped_params: &[CallableParam], + callable_array_position: Option<&(usize, Vec)>, + total_captures: usize, ) { - let block = package - .blocks - .get(block_id) - .expect("block not found") - .clone(); - for &stmt_id in &block.stmts { - transform_stmt( - package, - package_id, - stmt_id, - param, - concrete, - alias_set, - specialized_capture_targets, - assigner, - ); + // Nested callable parameters usually leave a destructuring statement such + // as `let (first, second, target) = pair`. When the whole `pair` slot is + // removed, that destructuring is dead and must be dropped before the input + // pattern loses the slot it refers to. + let nested_param_vars: FxHashSet = remapped_params + .iter() + .filter(|p| !p.field_path.is_empty()) + .map(|p| p.param_var) + .collect(); + + // A repeated position can appear several times in the analysis group, for + // example one row per branch candidate or per callable-array element. The + // input pattern should be edited once per parameter position, not once per + // row, otherwise earlier removals would shift later indices. + let params_for_removal = unique_params_for_removal(remapped_params); + + // Whole-slot cleanup is only valid when every field of a tuple-valued + // parameter is consumed. If any non-callable field survives, keep the + // destructuring statement in place so `remove_nested_callable_param` can + // rewrite it to bind the surviving fields instead of deleting those locals. + let remove_partial_nested_params_individually = callable_array_position.is_none() + && should_remove_combined_nested_params_individually(target, new_decl, ¶ms_for_removal); + if !remove_partial_nested_params_individually && !nested_param_vars.is_empty() { + remove_param_destructuring_stmts(target, &new_decl.implementation, &nested_param_vars); } -} -/// Walks a pattern tree, returning the `LocalVarId` bound at the given -/// tuple-field path when every intermediate node is a tuple pattern and the -/// leaf is a `Bind`. -fn find_bind_local_at_field_path( - package: &Package, - pat_id: PatId, - field_path: &[usize], -) -> Option { - let pat = package.get_pat(pat_id); - match field_path.split_first() { - None => match &pat.kind { - PatKind::Bind(ident) => Some(ident.id), - PatKind::Tuple(_) | PatKind::Discard => None, - }, - Some((index, tail)) => match &pat.kind { - PatKind::Tuple(sub_pats) => sub_pats - .get(*index) - .and_then(|sub_pat_id| find_bind_local_at_field_path(package, *sub_pat_id, tail)), - PatKind::Bind(_) | PatKind::Discard => None, - }, + if callable_array_position.is_some() { + // Callable-array groups can contain many rows for one logical nested + // parameter. Removing nested positions from highest to lowest keeps + // earlier tuple fields from reindexing the positions still waiting to + // be removed. + if params_for_removal + .iter() + .all(|(param, _)| !param.field_path.is_empty()) + { + let mut removal_order = params_for_removal; + removal_order.sort_by(|(left, _), (right, _)| { + right + .top_level_param + .cmp(&left.top_level_param) + .then_with(|| right.field_path.cmp(&left.field_path)) + }); + for (param, had_closure_captures) in removal_order { + remove_nested_callable_param(target, new_decl, param, had_closure_captures); + } + } else { + // Top-level callable-array removal still uses the combined path: + // every removed position is a full input slot, so one pass over the + // outer pattern is the shape-preserving edit. + let param_refs: Vec<&CallableParam> = + params_for_removal.iter().map(|(param, _)| *param).collect(); + remove_callable_params(target, new_decl, ¶m_refs, total_captures); + } + } else if remove_partial_nested_params_individually { + // Mixed branch-split can combine only the callable fields of a tuple + // parameter. Use the nested-removal path so surviving fields keep their + // bindings and field projections are reindexed against the reduced + // tuple shape. + let mut removal_order = params_for_removal; + removal_order.sort_by(|(left, _), (right, _)| { + right + .top_level_param + .cmp(&left.top_level_param) + .then_with(|| right.field_path.cmp(&left.field_path)) + }); + for (param, had_closure_captures) in removal_order { + remove_nested_callable_param(target, new_decl, param, had_closure_captures); + } + } else { + // The ordinary combined case consumed whole input slots. Remove them + // in one pass so tuple-parameter indices are interpreted against the + // original input shape. + let param_refs: Vec<&CallableParam> = + params_for_removal.iter().map(|(param, _)| *param).collect(); + remove_callable_params(target, new_decl, ¶m_refs, total_captures); } + + // Parameter edits and capture threading can leave stale expression, block, + // and pattern types in the cloned body. Refresh after all structural edits + // so downstream invariants see one consistent final shape. + refresh_rewritten_value_types(target, &new_decl.implementation); } -/// Rewrites one statement in a specialized callable body and updates the alias -/// set used to recognize callable-parameter projections. +/// Returns `true` when a combined specialization must remove nested callable +/// fields one at a time instead of dropping their whole top-level parameter. /// -/// Before, destructuring locals in `stmt_id` may still hide the callable -/// parameter behind tuple-field aliases. After, any newly introduced aliases are -/// recorded in `alias_set` and all child expressions in the statement have been -/// visited for direct-call rewriting. -#[allow(clippy::too_many_arguments)] -fn transform_stmt( - package: &mut Package, - package_id: PackageId, - stmt_id: qsc_fir::fir::StmtId, - param: &CallableParam, - concrete: &ConcreteCallable, - alias_set: &mut AliasSet, - specialized_capture_targets: &mut FxHashSet, - assigner: &mut Assigner, -) { - let stmt = package.stmts.get(stmt_id).expect("stmt not found").clone(); - match &stmt.kind { - qsc_fir::fir::StmtKind::Expr(expr_id) | qsc_fir::fir::StmtKind::Semi(expr_id) => { - transform_expr( - package, - package_id, - *expr_id, - param, - concrete, - alias_set, - specialized_capture_targets, - assigner, - ); - } - qsc_fir::fir::StmtKind::Local(_, pat_id, expr_id) => { - // Record aliases introduced by destructuring the tuple-valued - // parameter down to the callable leaf. - if !param.field_path.is_empty() { - let init_expr = package.exprs.get(*expr_id).expect("expr not found"); - if let ExprKind::Var(Res::Local(var), _) = &init_expr.kind { - if *var == param.param_var { - if let Some(alias_var) = - find_bind_local_at_field_path(package, *pat_id, ¶m.field_path) - { - alias_set.insert(alias_var); - } - } else if alias_set.contains(var) { - let pat = package.pats.get(*pat_id).expect("pat not found"); - if let PatKind::Bind(ident) = &pat.kind { - alias_set.insert(ident.id); - } - } - } - } - transform_expr( - package, - package_id, - *expr_id, - param, - concrete, - alias_set, - specialized_capture_targets, - assigner, - ); - } - qsc_fir::fir::StmtKind::Item(_) => {} +/// The ordinary combined path removes a whole tuple-valued parameter after all +/// of that parameter's fields are specialized away. Mixed branch-split groups +/// can combine only some fields of a tuple-valued parameter, leaving non-arrow +/// siblings such as a target qubit still live in the specialized body. Those +/// partial-coverage groups need the nested removal path so destructuring +/// bindings and sibling field projections are rewritten to the surviving tuple +/// shape instead of being deleted as dead whole-parameter destructuring. +fn should_remove_combined_nested_params_individually( + package: &Package, + decl: &CallableDecl, + params_for_removal: &[(&CallableParam, bool)], +) -> bool { + if params_for_removal.is_empty() + || !params_for_removal + .iter() + .all(|(param, _)| !param.field_path.is_empty()) + { + return false; + } + + let mut removed_fields_by_slot: FxHashMap> = FxHashMap::default(); + for (param, _) in params_for_removal { + let [field] = param.field_path.as_slice() else { + return true; + }; + removed_fields_by_slot + .entry(param.top_level_param) + .or_default() + .insert(*field); } + + removed_fields_by_slot + .into_iter() + .any(|(top_level_param, removed_fields)| { + !nested_param_removal_covers_entire_slot( + package, + decl, + top_level_param, + removed_fields.len(), + ) + }) } -/// Rewrites an expression subtree in the cloned specialization so callable -/// parameter uses become concrete callees. +/// Checks whether the nested fields being removed account for every element in +/// the top-level tuple slot. /// -/// Before, calls may still target `param.param_var`, a tuple-field projection of -/// it, or an alias introduced by destructuring. After, every matching callee is -/// rewritten in place to invoke `concrete`, while nested blocks and control-flow -/// expressions are recursively normalized to the same post-specialization shape. -#[allow(clippy::too_many_lines)] -#[allow(clippy::too_many_arguments)] -fn transform_expr( - package: &mut Package, +/// Whole-slot coverage is the only case where `remove_callable_params` can +/// safely drop the top-level parameter and discard its destructuring. If any +/// field survives, callers must preserve the slot and let +/// `remove_nested_callable_param` rewrite the body against the reduced shape. +fn nested_param_removal_covers_entire_slot( + package: &Package, + decl: &CallableDecl, + top_level_param: usize, + removed_field_count: usize, +) -> bool { + let input_pat = package.get_pat(decl.input); + let slot_ty = match &input_pat.kind { + PatKind::Tuple(pats) => pats + .get(top_level_param) + .map(|pat_id| package.get_pat(*pat_id).ty.clone()), + PatKind::Bind(_) => Some(input_pat.ty.clone()), + PatKind::Discard => None, + }; + let Some(slot_ty) = slot_ty else { + return false; + }; + matches!(resolve_udt_ty(package, &slot_ty), Ty::Tuple(fields) if fields.len() == removed_field_count) +} + +/// Clones a HOF callable, transforms its body to replace the callable +/// parameter with the concrete callee, and inserts the specialized callable +/// into the target (`package_id`) package. The HOF body is read from +/// `call_site.hof_item_id.package`, which may differ from the target package. +/// Returns the `StoreItemId` of the new item. +fn specialize_one( + store: &mut PackageStore, package_id: PackageId, - expr_id: ExprId, + call_site: &CallSite, param: &CallableParam, - concrete: &ConcreteCallable, - alias_set: &mut AliasSet, - specialized_capture_targets: &mut FxHashSet, assigner: &mut Assigner, -) { - let expr = package.exprs.get(expr_id).expect("expr not found").clone(); - match &expr.kind { - ExprKind::Call(callee_id, args_id) => { - let callee_id = *callee_id; - let args_id = *args_id; - - // Check if the callee is our callable parameter (possibly wrapped - // in functor applications). - let (base_id, body_functor) = peel_body_functors(package, callee_id); - let base_kind = package.get_expr(base_id).kind.clone(); +) -> Option { + // Extract needed data from the source package. + // The HOF may live in a different package (e.g. the standard library), + // so use hof_item_id.package rather than the target package_id. + let hof_pkg_id = call_site.hof_item_id.package; + let arg_label = resolve_callable_arg_label(store, &call_site.callable_arg); + let (body_pkg, decl_snapshot) = { + let package = store.get(hof_pkg_id); + let hof_item = package.get_item(call_site.hof_item_id.item); - let replaced = if let ExprKind::Var(Res::Local(var), _) = &base_kind - && *var == param.param_var - && param.field_path.is_empty() - { - // Single-level param: direct use as callee. - replace_callee( - package, - package_id, - callee_id, - body_functor, - concrete, - assigner, - ); - true - } else if !param.field_path.is_empty() - && expr_matches_param_field_path( - package, - base_id, - param.param_var, - ¶m.field_path, - ) - { - replace_callee( - package, - package_id, - callee_id, - body_functor, - concrete, - assigner, - ); - true - } else { - false - }; + let ItemKind::Callable(ref hof_decl) = hof_item.kind else { + return None; + }; - // Also check alias set for nested params. - let replaced = if replaced { - true - } else if let ExprKind::Var(Res::Local(var), _) = &base_kind - && alias_set.contains(var) - { - replace_callee( - package, - package_id, - callee_id, - body_functor, - concrete, - assigner, - ); - true - } else { - false - }; + let body_pkg = extract_callable_body(package, hof_decl); + let decl_snapshot = hof_decl.as_ref().clone(); + (body_pkg, decl_snapshot) + }; // immutable borrow released - if !replaced { - transform_expr( - package, - package_id, - callee_id, - param, - concrete, - alias_set, - specialized_capture_targets, - assigner, - ); + // Clone body into target, transform, insert. + let target = store.get_mut(package_id); + let new_item_id = assigner.next_item(); + let owned_assigner = std::mem::take(assigner); + let mut cloner = FirCloner::from_assigner(owned_assigner); + cloner.reset_maps(); + + // Clone the input before the impl so `local_map` holds the input parameter + // mappings when the callable body is walked. + let cloned_input = cloner.clone_pat(&body_pkg, decl_snapshot.input, target); + let cloned_impl = cloner.clone_callable_impl(&body_pkg, &decl_snapshot.implementation, target); + + let (remapped_param, mut new_decl) = build_single_spec_decl( + &cloner, + param, + &decl_snapshot, + cloned_input, + cloned_impl, + &arg_label, + ); + + // Thread closure captures before recovering the assigner, since + // thread_closure_captures uses the cloner for pat/local allocation. + let closure_info = if let ConcreteCallable::Closure { + ref captures, + target: closure_target, + .. + } = call_site.callable_arg + { + let callable_capture = captured_callable_specialization(target, captures); + let captures_to_thread: Vec = captures + .iter() + .enumerate() + .filter(|(idx, _)| { + callable_capture + .as_ref() + .is_none_or(|callable_capture| callable_capture.capture_idx != *idx) + }) + .map(|(_, capture)| capture.clone()) + .collect(); + let capture_bindings = thread_closure_captures( + &mut cloner, + target, + &mut new_decl, + &remapped_param, + &captures_to_thread, + 0, + ); + Some(ClosureSpecializationInfo { + target: closure_target, + capture_bindings, + callable_capture, + }) + } else { + None + }; + + // Recover the assigner from the cloner so all subsequent allocations + // flow through the shared pipeline assigner. + *assigner = cloner.into_assigner(); + + apply_single_param_specialization( + target, + package_id, + &mut new_decl, + &remapped_param, + call_site, + closure_info, + assigner, + ); + + // Insert the new item. + let new_item = Item { + id: new_item_id, + span: Span::default(), + parent: None, + doc: Rc::from(""), + attrs: Vec::new(), + visibility: Visibility::Internal, + kind: ItemKind::Callable(Box::new(new_decl)), + }; + target.items.insert(new_item_id, new_item); + + Some(StoreItemId { + package: package_id, + item: new_item_id, + }) +} + +/// Builds the specialized callable declaration for a single-parameter spec, +/// remapping the callable parameter through the cloner's id maps. +/// +/// The spec is named `HOF{label}` after the concrete argument. Returns the +/// remapped [`CallableParam`] alongside the new [`CallableDecl`], which still +/// carries the original arrow parameter slot; the caller removes it after the +/// body transform. +fn build_single_spec_decl( + cloner: &FirCloner, + param: &CallableParam, + decl_snapshot: &CallableDecl, + cloned_input: PatId, + cloned_impl: CallableImpl, + arg_label: &str, +) -> (CallableParam, CallableDecl) { + let remapped_param_var = *cloner + .local_map() + .get(¶m.param_var) + .expect("param_var should be in local_map after cloning input first"); + + let remapped_param = CallableParam::new( + param.callable_id, + cloner + .pat_map() + .get(¶m.param_pat_id) + .copied() + .unwrap_or(param.param_pat_id), + param.top_level_param, + param.field_path.clone(), + remapped_param_var, + param.param_ty.clone(), + param.hof_input_is_tuple, + ); + + let hof_name: Rc = Rc::from(format!("{}{{{arg_label}}}", decl_snapshot.name.name)); + let new_decl = CallableDecl { + span: decl_snapshot.span, + kind: decl_snapshot.kind, + name: Ident { + id: LocalVarId::from(0_u32), + span: decl_snapshot.name.span, + name: hof_name, + }, + generics: decl_snapshot.generics.clone(), + input: cloned_input, + output: decl_snapshot.output.clone(), + functors: decl_snapshot.functors, + implementation: cloned_impl, + attrs: decl_snapshot.attrs.clone(), + }; + (remapped_param, new_decl) +} + +/// Transforms the single-parameter spec body to replace the callable parameter +/// with its concrete callee, then removes the now-dead parameter slot. +/// +/// A callable's functored specs share one lifted lambda item, so a fresh dedup +/// set guards against re-specializing it across the parameter's specs. When the +/// concrete argument is a closure with captures, those captures are threaded as +/// new input slots and each direct call to the closure target receives the +/// capture operands; a fully consumed tuple parameter is then dropped rather +/// than retyped to unit. +fn apply_single_param_specialization( + target: &mut Package, + package_id: PackageId, + new_decl: &mut CallableDecl, + remapped_param: &CallableParam, + call_site: &CallSite, + closure_info: ClosureInfo, + assigner: &mut Assigner, +) { + if let Some(info) = &closure_info { + specialize_closure_target_callable_capture( + target, + package_id, + info.target, + info.callable_capture.as_ref(), + assigner, + ); + } + + // A callable's functored specs share one lifted lambda item, so a fresh + // dedup set guards against re-specializing it across this param's specs. + let impl_clone = new_decl.implementation.clone(); + let mut specialized_capture_targets: FxHashSet = FxHashSet::default(); + let concrete = if let Some(info) = &closure_info { + concrete_with_threaded_captures(&call_site.callable_arg, &info.capture_bindings) + } else { + call_site.callable_arg.clone() + }; + transform_callable_body( + target, + package_id, + &impl_clone, + remapped_param, + &concrete, + &[], + &mut specialized_capture_targets, + assigner, + ); + + // Whether the removed callable threaded closure captures as new input + // slots. This governs how a fully consumed tuple parameter is handled below. + let had_closure_captures = closure_info + .as_ref() + .is_some_and(|info| !info.capture_bindings.is_empty()); + + if let Some(info) = closure_info { + rewrite_closure_target_call_args( + target, + &new_decl.implementation, + package_id, + info.target, + &info.capture_bindings, + assigner, + ); + } + + // Remove the callable parameter from the input pattern and update types. + // When the removed callable was a closure with captures, those captures were + // threaded as new input slots and the call site drops the consumed slot, so + // a fully consumed tuple parameter must be dropped rather than retyped to + // unit. + remove_callable_param(target, new_decl, remapped_param, had_closure_captures); + refresh_rewritten_value_types(target, &new_decl.implementation); +} + +/// Finds the single parameter position shared by a forwarded callable array, +/// if the call group qualifies for combined removal. +/// +/// Returns `None` when any call site in the group is conditional or dynamic, or +/// when the repeated parameter position is not unique or is not array-typed. +/// The returned `(top_level_param, field_path)` locates the array parameter that +/// combined removal collapses. +fn find_callable_array_group_position( + remapped_params: &[CallableParam], + group: &[(&CallSite, &CallableParam)], +) -> Option<(usize, Vec)> { + if group.iter().any(|(call_site, _)| { + !call_site.condition.is_empty() + || matches!(call_site.callable_arg, ConcreteCallable::Dynamic) + }) { + return None; + } + + let mut positions: FxHashMap<(usize, Vec), usize> = FxHashMap::default(); + for param in remapped_params { + *positions + .entry((param.top_level_param, param.field_path.clone())) + .or_default() += 1; + } + let repeated = positions + .into_iter() + .filter(|(_, count)| *count >= 2) + .map(|(position, _)| position) + .collect::>(); + let [position] = repeated.as_slice() else { + return None; + }; + remapped_params + .iter() + .find(|param| (param.top_level_param, param.field_path.clone()) == *position) + .and_then(|param| matches!(param.param_ty, Ty::Array(_)).then(|| position.clone())) +} + +/// Deduplicates callable parameters by their `(top_level_param, field_path)` +/// position, keeping the first occurrence of each. +/// +/// Each returned entry pairs the parameter with a flag for whether it is +/// array-typed, which downstream removal uses to tell a forwarded callable +/// array apart from a scalar callable parameter. +fn unique_params_for_removal(params: &[CallableParam]) -> Vec<(&CallableParam, bool)> { + let mut seen: FxHashSet<(usize, Vec)> = FxHashSet::default(); + params + .iter() + .filter_map(|param| { + let position = (param.top_level_param, param.field_path.clone()); + seen.insert(position) + .then_some((param, matches!(param.param_ty, Ty::Array(_)))) + }) + .collect() +} + +/// Rebuilds a concrete callable so a forwarded closure carries its captured +/// variables as explicit captures on the specialized callable. +/// +/// Global and dynamic callables have no captured environment and are returned +/// unchanged. +fn concrete_with_threaded_captures( + concrete: &ConcreteCallable, + capture_bindings: &[(LocalVarId, Ty)], +) -> ConcreteCallable { + match concrete { + ConcreteCallable::Closure { + target, functor, .. + } => { + // Thread captures through the specialized callable input so a + // forwarded closure keeps its runtime environment after the + // callable parameter that carried it is removed. + ConcreteCallable::Closure { + target: *target, + captures: capture_bindings + .iter() + .map(|(var, ty)| CapturedVar { + var: *var, + ty: ty.clone(), + expr: None, + caller_substitutions: Vec::new(), + }) + .collect(), + functor: *functor, } + } + ConcreteCallable::Global { .. } | ConcreteCallable::Dynamic => concrete.clone(), + } +} - // Recurse into the arguments. - transform_expr( - package, - package_id, - args_id, - param, - concrete, - alias_set, - specialized_capture_targets, - assigner, - ); +/// Identifies whether a closure's only captured value is itself a concrete +/// callable eligible to be baked into the closure target. +/// +/// Only a single-capture closure is considered. A qualifying capture yields a +/// [`CapturedCallableSpecialization`] recording its index, type, and resolved +/// concrete callable; any non-callable capture stays threaded as an ordinary +/// runtime operand. +fn captured_callable_specialization( + package: &Package, + captures: &[CapturedVar], +) -> Option { + if captures.len() != 1 { + return None; + } + let capture = captures.first()?; + let expr_id = capture.expr?; + concrete_callable_from_capture_expr(package, expr_id).map(|concrete| { + CapturedCallableSpecialization { + capture_idx: 0, + capture_ty: capture.ty.clone(), + concrete, } - ExprKind::Block(block_id) => { - transform_block( - package, - package_id, - *block_id, - param, - concrete, - alias_set, - specialized_capture_targets, - assigner, - ); + }) +} + +/// Resolves a capture initializer expression to a [`ConcreteCallable`] when it +/// is a statically-known callable value. +/// +/// Returns a `Global` for a non-generic item reference and a `Closure` for a +/// capture-free closure, in both cases only when the callable's own input does +/// not still contain an arrow (see [`callable_input_contains_arrow`]); any +/// other shape yields `None`. Functor wrappers on the capture are peeled and +/// folded into the returned callable's functor. +fn concrete_callable_from_capture_expr( + package: &Package, + expr_id: ExprId, +) -> Option { + let (base_id, functor) = peel_body_functors(package, expr_id); + match &package.get_expr(base_id).kind { + ExprKind::Var(Res::Item(item_id), generic_args) + if generic_args.is_empty() && !callable_input_contains_arrow(package, item_id.item) => + { + Some(ConcreteCallable::Global { + item_id: *item_id, + functor, + }) } - ExprKind::If(cond, body, els) => { - transform_expr( + ExprKind::Closure(captures, target) + if captures.is_empty() && !callable_input_contains_arrow(package, *target) => + { + Some(ConcreteCallable::Closure { + target: *target, + captures: Vec::new(), + functor, + }) + } + _ => None, + } +} + +/// Returns whether the callable's declared input type still contains an arrow +/// (a callable-typed parameter), resolving UDT wrappers first. +/// +/// Used as a conservative eligibility gate: a missing or non-callable item, or +/// an unresolvable UDT, reports `true` so an ambiguous capture is left on the +/// general dispatch path rather than baked in as a concrete callable. +fn callable_input_contains_arrow(package: &Package, callable: LocalItemId) -> bool { + let Some(Item { + kind: ItemKind::Callable(decl), + .. + }) = package.items.get(callable) + else { + return true; + }; + ty_contains_arrow_through_udts(package, &package.get_pat(decl.input).ty) +} + +/// Recursively tests whether a type contains an arrow, expanding UDT wrappers +/// via [`resolve_udt_ty`] on the way down. +/// +/// A residual `Ty::Udt` (one [`resolve_udt_ty`] could not expand, e.g. a +/// foreign or non-`Ty` item) conservatively counts as containing an arrow so an +/// unknown shape is never misclassified as arrow-free. +fn ty_contains_arrow_through_udts(package: &Package, ty: &Ty) -> bool { + match resolve_udt_ty(package, ty) { + Ty::Arrow(_) | Ty::Udt(_) => true, + Ty::Array(elem) => ty_contains_arrow_through_udts(package, &elem), + Ty::Tuple(elems) => elems + .iter() + .any(|elem| ty_contains_arrow_through_udts(package, elem)), + Ty::Infer(_) | Ty::Param(_) | Ty::Prim(_) | Ty::Err => false, + } +} + +/// Transforms all specialization bodies in a callable implementation, +/// replacing uses of the callable parameter with direct calls to the concrete +/// callee. +/// +/// `specialized_capture_targets` tracks each lifted lambda item and captured +/// callable parameter already specialized. It is supplied by the caller so a +/// multi-argument specialization can share one set across every parameter's +/// transform pass; single-argument callers pass a fresh set. +#[allow(clippy::too_many_arguments)] +fn transform_callable_body( + package: &mut Package, + package_id: PackageId, + callable_impl: &CallableImpl, + param: &CallableParam, + concrete: &ConcreteCallable, + concrete_group: &[ConcreteCallable], + specialized_capture_targets: &mut FxHashSet, + assigner: &mut Assigner, +) { + let mut alias_set = AliasSet::default(); + match callable_impl { + CallableImpl::Intrinsic => {} + CallableImpl::Spec(spec_impl) => { + transform_block( package, package_id, - *cond, + spec_impl.body.block, param, concrete, - alias_set, + concrete_group, + &mut alias_set, specialized_capture_targets, assigner, ); - transform_expr( - package, - package_id, - *body, - param, - concrete, - alias_set, - specialized_capture_targets, - assigner, - ); - if let Some(els_id) = els { - transform_expr( + if let Some(ref adj) = spec_impl.adj { + transform_block( package, package_id, - *els_id, + adj.block, param, concrete, - alias_set, + concrete_group, + &mut alias_set, specialized_capture_targets, assigner, ); } - } - ExprKind::While(cond, block_id) => { - transform_expr( - package, - package_id, - *cond, - param, - concrete, - alias_set, - specialized_capture_targets, - assigner, - ); - transform_block( - package, - package_id, - *block_id, - param, - concrete, - alias_set, - specialized_capture_targets, - assigner, - ); - } - ExprKind::Tuple(exprs) | ExprKind::Array(exprs) | ExprKind::ArrayLit(exprs) => { - for &e in exprs { - transform_expr( + if let Some(ref ctl) = spec_impl.ctl { + transform_block( package, package_id, - e, + ctl.block, param, concrete, - alias_set, + concrete_group, + &mut alias_set, + specialized_capture_targets, + assigner, + ); + } + if let Some(ref ctl_adj) = spec_impl.ctl_adj { + transform_block( + package, + package_id, + ctl_adj.block, + param, + concrete, + concrete_group, + &mut alias_set, specialized_capture_targets, assigner, ); } } - ExprKind::Assign(lhs, rhs) - | ExprKind::AssignOp(_, lhs, rhs) - | ExprKind::BinOp(_, lhs, rhs) - | ExprKind::ArrayRepeat(lhs, rhs) - | ExprKind::Index(lhs, rhs) => { - transform_expr( - package, - package_id, - *lhs, - param, - concrete, - alias_set, - specialized_capture_targets, - assigner, - ); - transform_expr( - package, - package_id, - *rhs, - param, - concrete, - alias_set, - specialized_capture_targets, - assigner, - ); - } - ExprKind::AssignField(a, _, b) | ExprKind::UpdateField(a, _, b) => { - transform_expr( - package, - package_id, - *a, - param, - concrete, - alias_set, - specialized_capture_targets, - assigner, - ); - transform_expr( - package, - package_id, - *b, - param, - concrete, - alias_set, - specialized_capture_targets, - assigner, - ); - } - ExprKind::AssignIndex(a, b, c) | ExprKind::UpdateIndex(a, b, c) => { - transform_expr( - package, - package_id, - *a, - param, - concrete, - alias_set, - specialized_capture_targets, - assigner, - ); - transform_expr( - package, - package_id, - *b, - param, - concrete, - alias_set, - specialized_capture_targets, - assigner, - ); - transform_expr( + CallableImpl::SimulatableIntrinsic(spec_decl) => { + transform_block( package, package_id, - *c, + spec_decl.block, param, concrete, - alias_set, + concrete_group, + &mut alias_set, specialized_capture_targets, assigner, ); } - ExprKind::UnOp(_, inner) | ExprKind::Return(inner) | ExprKind::Fail(inner) => { + } +} + +/// Recursively walks a block, transforming call expressions that invoke the +/// callable parameter. +#[allow(clippy::too_many_arguments)] +fn transform_block( + package: &mut Package, + package_id: PackageId, + block_id: qsc_fir::fir::BlockId, + param: &CallableParam, + concrete: &ConcreteCallable, + concrete_group: &[ConcreteCallable], + alias_set: &mut AliasSet, + specialized_capture_targets: &mut FxHashSet, + assigner: &mut Assigner, +) { + let block = package + .blocks + .get(block_id) + .expect("block not found") + .clone(); + for &stmt_id in &block.stmts { + transform_stmt( + package, + package_id, + stmt_id, + param, + concrete, + concrete_group, + alias_set, + specialized_capture_targets, + assigner, + ); + } +} + +/// Walks a pattern tree, returning the `LocalVarId` bound at the given +/// tuple-field path when every intermediate node is a tuple pattern and the +/// leaf is a `Bind`. +fn find_bind_local_at_field_path( + package: &Package, + pat_id: PatId, + field_path: &[usize], +) -> Option { + let pat = package.get_pat(pat_id); + match field_path.split_first() { + None => match &pat.kind { + PatKind::Bind(ident) => Some(ident.id), + PatKind::Tuple(_) | PatKind::Discard => None, + }, + Some((index, tail)) => match &pat.kind { + PatKind::Tuple(sub_pats) => sub_pats + .get(*index) + .and_then(|sub_pat_id| find_bind_local_at_field_path(package, *sub_pat_id, tail)), + PatKind::Bind(_) | PatKind::Discard => None, + }, + } +} + +/// Rewrites one statement in a specialized callable body and updates the alias +/// set used to recognize callable-parameter projections. +/// +/// Before, destructuring locals in `stmt_id` may still hide the callable +/// parameter behind tuple-field aliases. After, any newly introduced aliases are +/// recorded in `alias_set` and all child expressions in the statement have been +/// visited for direct-call rewriting. +#[allow(clippy::too_many_arguments)] +fn transform_stmt( + package: &mut Package, + package_id: PackageId, + stmt_id: qsc_fir::fir::StmtId, + param: &CallableParam, + concrete: &ConcreteCallable, + concrete_group: &[ConcreteCallable], + alias_set: &mut AliasSet, + specialized_capture_targets: &mut FxHashSet, + assigner: &mut Assigner, +) { + let stmt = package.stmts.get(stmt_id).expect("stmt not found").clone(); + match &stmt.kind { + qsc_fir::fir::StmtKind::Expr(expr_id) | qsc_fir::fir::StmtKind::Semi(expr_id) => { transform_expr( package, package_id, - *inner, + *expr_id, param, concrete, + concrete_group, alias_set, specialized_capture_targets, assigner, ); } - ExprKind::Field(inner_id, _) => { - // For nested callable params, check if this Field expression - // accesses the arrow element within the param variable. - if !param.field_path.is_empty() - && expr_matches_param_field_path( - package, - expr_id, - param.param_var, - ¶m.field_path, - ) - { - replace_callee( - package, - package_id, - expr_id, - FunctorApp::default(), - concrete, - assigner, - ); - return; + qsc_fir::fir::StmtKind::Local(_, pat_id, expr_id) => { + // Record aliases introduced by destructuring the tuple-valued + // parameter down to the callable leaf. + if !param.field_path.is_empty() { + let init_expr = package.exprs.get(*expr_id).expect("expr not found"); + if let ExprKind::Var(Res::Local(var), _) = &init_expr.kind { + if *var == param.param_var { + if let Some(alias_var) = + find_bind_local_at_field_path(package, *pat_id, ¶m.field_path) + { + alias_set.insert(alias_var); + } + } else if alias_set.contains(var) { + let pat = package.pats.get(*pat_id).expect("pat not found"); + if let PatKind::Bind(ident) = &pat.kind { + alias_set.insert(ident.id); + } + } + } } transform_expr( package, package_id, - *inner_id, + *expr_id, param, concrete, + concrete_group, alias_set, specialized_capture_targets, assigner, ); } - ExprKind::Range(a, b, c) => { - if let Some(a) = a { - transform_expr( - package, - package_id, - *a, - param, + qsc_fir::fir::StmtKind::Item(_) => {} + } +} + +/// Rewrites an expression subtree in the cloned specialization so callable +/// parameter uses become concrete callees. +/// +/// Before, calls may still target `param.param_var`, a tuple-field projection of +/// it, or an alias introduced by destructuring. After, every matching callee is +/// rewritten in place to invoke `concrete`, while nested blocks and control-flow +/// expressions are recursively normalized to the same post-specialization shape. +#[allow(clippy::too_many_lines)] +#[allow(clippy::too_many_arguments)] +fn transform_expr( + package: &mut Package, + package_id: PackageId, + expr_id: ExprId, + param: &CallableParam, + concrete: &ConcreteCallable, + concrete_group: &[ConcreteCallable], + alias_set: &mut AliasSet, + specialized_capture_targets: &mut FxHashSet, + assigner: &mut Assigner, +) { + let expr = package.exprs.get(expr_id).expect("expr not found").clone(); + match &expr.kind { + ExprKind::Call(callee_id, args_id) => { + let callee_id = *callee_id; + let args_id = *args_id; + + // Check if the callee is our callable parameter (possibly wrapped + // in functor applications). + let (base_id, body_functor) = peel_body_functors(package, callee_id); + let base_kind = package.get_expr(base_id).kind.clone(); + + let replaced = if let Some((array_id, index_id)) = indexed_callable_array_param_source( + package, + base_id, + param.param_var, + ¶m.field_path, + ) { + replace_indexed_callable_array_call( + package, + package_id, + expr_id, + callee_id, + args_id, + array_id, + index_id, + body_functor, + if concrete_group.is_empty() { + std::slice::from_ref(concrete) + } else { + concrete_group + }, + assigner, + ); + true + } else if let ExprKind::Var(Res::Local(var), _) = &base_kind + && *var == param.param_var + && param.field_path.is_empty() + { + // Single-level param: direct use as callee. + replace_callee( + package, + package_id, + callee_id, + body_functor, concrete, - alias_set, - specialized_capture_targets, assigner, ); - } - if let Some(b) = b { - transform_expr( + true + } else if expr_matches_param_field_path( + package, + base_id, + param.param_var, + ¶m.field_path, + ) { + // `expr_matches_param_field_path` already matches an empty + // field path for a single-field-UDT callee (e.g. + // `Field(Var(b), [])`), so no separate non-empty guard is + // needed here; that lets `replace_callee` fire for + // single-field-UDT callees as well as deeper paths. + replace_callee( package, package_id, - *b, - param, + callee_id, + body_functor, concrete, - alias_set, - specialized_capture_targets, assigner, ); - } - if let Some(c) = c { - transform_expr( + true + } else { + false + }; + + // Also check alias set for nested params. + let replaced = if replaced { + true + } else if let ExprKind::Var(Res::Local(var), _) = &base_kind + && alias_set.contains(var) + { + replace_callee( package, package_id, - *c, - param, + callee_id, + body_functor, concrete, - alias_set, - specialized_capture_targets, assigner, ); - } - } - ExprKind::String(components) => { - for comp in components { - if let qsc_fir::fir::StringComponent::Expr(e) = comp { - transform_expr( - package, - package_id, - *e, - param, - concrete, - alias_set, - specialized_capture_targets, - assigner, - ); - } - } - } - ExprKind::Struct(_, copy, fields) => { - if let Some(c) = copy { + true + } else { + false + }; + + if !replaced { transform_expr( package, package_id, - *c, + callee_id, param, concrete, + concrete_group, alias_set, specialized_capture_targets, assigner, ); + } else if matches!(concrete, ConcreteCallable::Closure { captures, .. } if captures.is_empty()) + { + let concrete = apply_body_functor_to_concrete(concrete, body_functor); + rewrite_indexed_closure_dispatch_args(package, args_id, &concrete, assigner); } - for f in fields { + + // Recurse into the arguments. + transform_expr( + package, + package_id, + args_id, + param, + concrete, + concrete_group, + alias_set, + specialized_capture_targets, + assigner, + ); + } + ExprKind::Block(block_id) => { + transform_block( + package, + package_id, + *block_id, + param, + concrete, + concrete_group, + alias_set, + specialized_capture_targets, + assigner, + ); + } + ExprKind::If(cond, body, els) => { + transform_expr( + package, + package_id, + *cond, + param, + concrete, + concrete_group, + alias_set, + specialized_capture_targets, + assigner, + ); + transform_expr( + package, + package_id, + *body, + param, + concrete, + concrete_group, + alias_set, + specialized_capture_targets, + assigner, + ); + if let Some(els_id) = els { transform_expr( package, package_id, - f.value, + *els_id, param, concrete, + concrete_group, alias_set, specialized_capture_targets, assigner, ); } } - // Substitute the callable parameter variable (or an alias from - // destructuring) at non-callee positions (e.g., when forwarded as an - // argument to an inner HOF). - ExprKind::Var(Res::Local(var), _) - if (*var == param.param_var && param.field_path.is_empty()) - || alias_set.contains(var) => - { - replace_callee( + ExprKind::While(cond, block_id) => { + transform_expr( package, package_id, - expr_id, - FunctorApp::default(), + *cond, + param, + concrete, + concrete_group, + alias_set, + specialized_capture_targets, + assigner, + ); + transform_block( + package, + package_id, + *block_id, + param, concrete, + concrete_group, + alias_set, + specialized_capture_targets, assigner, ); } - // When a closure captures the callable parameter being specialized, - // propagate the specialization into the closure's target callable and - // remove the capture. - ExprKind::Closure(captures, target) => { - if let Some(capture_idx) = captures - .iter() - .position(|&c| c == param.param_var || alias_set.contains(&c)) - { - let target = *target; - transform_closure_param_capture( + ExprKind::Tuple(exprs) | ExprKind::Array(exprs) | ExprKind::ArrayLit(exprs) => { + for &e in exprs { + transform_expr( package, package_id, - expr_id, - target, - capture_idx, + e, param, concrete, + concrete_group, + alias_set, specialized_capture_targets, assigner, ); } } - // Terminals with no sub-expressions. - ExprKind::Hole | ExprKind::Lit(_) | ExprKind::Var(_, _) => {} + ExprKind::Assign(lhs, rhs) + | ExprKind::AssignOp(_, lhs, rhs) + | ExprKind::BinOp(_, lhs, rhs) + | ExprKind::ArrayRepeat(lhs, rhs) + | ExprKind::Index(lhs, rhs) => { + transform_expr( + package, + package_id, + *lhs, + param, + concrete, + concrete_group, + alias_set, + specialized_capture_targets, + assigner, + ); + transform_expr( + package, + package_id, + *rhs, + param, + concrete, + concrete_group, + alias_set, + specialized_capture_targets, + assigner, + ); + } + ExprKind::AssignField(a, _, b) | ExprKind::UpdateField(a, _, b) => { + transform_expr( + package, + package_id, + *a, + param, + concrete, + concrete_group, + alias_set, + specialized_capture_targets, + assigner, + ); + transform_expr( + package, + package_id, + *b, + param, + concrete, + concrete_group, + alias_set, + specialized_capture_targets, + assigner, + ); + } + ExprKind::AssignIndex(a, b, c) | ExprKind::UpdateIndex(a, b, c) => { + transform_expr( + package, + package_id, + *a, + param, + concrete, + concrete_group, + alias_set, + specialized_capture_targets, + assigner, + ); + transform_expr( + package, + package_id, + *b, + param, + concrete, + concrete_group, + alias_set, + specialized_capture_targets, + assigner, + ); + transform_expr( + package, + package_id, + *c, + param, + concrete, + concrete_group, + alias_set, + specialized_capture_targets, + assigner, + ); + } + ExprKind::UnOp(_, inner) | ExprKind::Return(inner) | ExprKind::Fail(inner) => { + transform_expr( + package, + package_id, + *inner, + param, + concrete, + concrete_group, + alias_set, + specialized_capture_targets, + assigner, + ); + } + ExprKind::Field(inner_id, _) => { + // For nested callable params, check if this Field expression + // accesses the arrow element within the param variable. + if !param.field_path.is_empty() { + if expr_matches_param_field_path( + package, + expr_id, + param.param_var, + ¶m.field_path, + ) { + // The forwarded value can be a single callable read out of a + // struct/tuple param, or a whole callable array nested in + // that param and threaded to an inner HOF that indexes it. + // Rebuild the array literal so every candidate survives and + // any surviving index stays valid; substitute a single + // non-array value in place. + substitute_forwarded_callable( + package, + expr_id, + concrete, + concrete_group, + assigner, + ); + return; + } + } else if collect_field_path_from_param(package, expr_id, param.param_var).is_some() { + // Empty-path (single-field UDT) callable forwarded by value to + // an inner HOF: mirror the non-empty branch above and replace + // the field-access with the concrete value so the fixpoint + // re-analysis can resolve the inner call site (instead of + // leaving a forwarded field access that declines to + // `DynamicCallable`). + substitute_forwarded_callable(package, expr_id, concrete, concrete_group, assigner); + return; + } + transform_expr( + package, + package_id, + *inner_id, + param, + concrete, + concrete_group, + alias_set, + specialized_capture_targets, + assigner, + ); + } + ExprKind::Range(a, b, c) => { + if let Some(a) = a { + transform_expr( + package, + package_id, + *a, + param, + concrete, + concrete_group, + alias_set, + specialized_capture_targets, + assigner, + ); + } + if let Some(b) = b { + transform_expr( + package, + package_id, + *b, + param, + concrete, + concrete_group, + alias_set, + specialized_capture_targets, + assigner, + ); + } + if let Some(c) = c { + transform_expr( + package, + package_id, + *c, + param, + concrete, + concrete_group, + alias_set, + specialized_capture_targets, + assigner, + ); + } + } + ExprKind::String(components) => { + for comp in components { + if let qsc_fir::fir::StringComponent::Expr(e) = comp { + transform_expr( + package, + package_id, + *e, + param, + concrete, + concrete_group, + alias_set, + specialized_capture_targets, + assigner, + ); + } + } + } + ExprKind::Struct(_, copy, fields) => { + if let Some(c) = copy { + transform_expr( + package, + package_id, + *c, + param, + concrete, + concrete_group, + alias_set, + specialized_capture_targets, + assigner, + ); + } + for f in fields { + transform_expr( + package, + package_id, + f.value, + param, + concrete, + concrete_group, + alias_set, + specialized_capture_targets, + assigner, + ); + } + } + // Substitute the callable parameter variable (or an alias from + // destructuring) at non-callee positions (e.g., when forwarded as an + // argument to an inner HOF). + ExprKind::Var(Res::Local(var), _) + if (*var == param.param_var && param.field_path.is_empty()) + || alias_set.contains(var) => + { + // A callable parameter forwarded by value is either a single + // callable or a whole array threaded to an inner HOF that indexes + // it. For an array, rebuild the concrete array literal so every + // candidate (with its threaded capture) survives and any surviving + // index stays valid; a single non-array value is substituted in + // place. + substitute_forwarded_callable(package, expr_id, concrete, concrete_group, assigner); + } + // When a closure captures the callable parameter being specialized, + // propagate the specialization into the closure's target callable and + // remove the capture. + ExprKind::Closure(captures, target) => { + if let Some(capture_idx) = captures + .iter() + .position(|&c| c == param.param_var || alias_set.contains(&c)) + { + let target = *target; + transform_closure_param_capture( + package, + package_id, + expr_id, + target, + capture_idx, + param, + concrete, + specialized_capture_targets, + assigner, + ); + } + } + // Terminals with no sub-expressions. + ExprKind::Hole | ExprKind::Lit(_) | ExprKind::Var(_, _) => {} + } +} + +/// Builds the base expression kind, functor application, and inferred arrow +/// type for a concrete callable value. Shared by the in-place single-value +/// substitution ([`replace_callable_value`]) and the allocating per-element +/// reconstruction ([`alloc_callable_value_expr`]) so both emit identical +/// callable-value expressions. `hint_ty` is the arrow type of the slot the +/// value fills — the expression's own type for a single value, or the element +/// type when rebuilding an array — and lets a closure recover its concrete +/// callee type. Returns `None` for a dynamic callable, which has no concrete +/// value to emit. +fn build_callable_value_parts( + package: &Package, + concrete: &ConcreteCallable, + hint_ty: &Ty, +) -> Option<(ExprKind, FunctorApp, Option)> { + match concrete { + ConcreteCallable::Global { item_id, functor } => { + let ty = package + .items + .get(item_id.item) + .and_then(|item| match &item.kind { + ItemKind::Callable(decl) => Some(Ty::Arrow(Box::new(Arrow { + kind: decl.kind, + input: Box::new(package.get_pat(decl.input).ty.clone()), + output: Box::new(decl.output.clone()), + functors: qsc_fir::ty::FunctorSet::Value(decl.functors), + }))), + ItemKind::Ty(..) => None, + }); + Some((ExprKind::Var(Res::Item(*item_id), Vec::new()), *functor, ty)) + } + ConcreteCallable::Closure { + target, + captures, + functor, + } => { + let ty = build_direct_target_callee_ty(package, *target, hint_ty, 0).or_else(|| { + package + .items + .get(*target) + .and_then(|item| match &item.kind { + ItemKind::Callable(decl) => Some(Ty::Arrow(Box::new(Arrow { + kind: decl.kind, + input: Box::new(package.get_pat(decl.input).ty.clone()), + output: Box::new(decl.output.clone()), + functors: qsc_fir::ty::FunctorSet::Value(decl.functors), + }))), + ItemKind::Ty(..) => None, + }) + }); + Some(( + ExprKind::Closure( + captures.iter().map(|capture| capture.var).collect(), + *target, + ), + *functor, + ty, + )) + } + ConcreteCallable::Dynamic => None, + } +} + +/// Replaces a callable-valued expression while preserving closure captures. +/// Callee replacement can collapse a closure to its target item, but forwarded +/// callable values must remain closures so nested HOFs still receive captures. +fn replace_callable_value( + package: &mut Package, + expr_id: ExprId, + concrete: &ConcreteCallable, + assigner: &mut Assigner, +) { + let Some((base_kind, functor, base_ty)) = + build_callable_value_parts(package, concrete, &package.get_expr(expr_id).ty) + else { + return; + }; + + let expr = package.exprs.get(expr_id).expect("expr not found").clone(); + let new_ty = base_ty.unwrap_or_else(|| expr.ty.clone()); + if !functor.adjoint && functor.controlled == 0 { + let expr_mut = package.exprs.get_mut(expr_id).expect("expr not found"); + expr_mut.kind = base_kind; + expr_mut.ty = new_ty; + return; + } + + let outer_id = + alloc_functor_wrapped_expr(package, assigner, base_kind, functor, &new_ty, expr.span); + + let outermost_kind = package + .exprs + .get(outer_id) + .expect("expr not found") + .kind + .clone(); + let expr_mut = package.exprs.get_mut(expr_id).expect("expr not found"); + expr_mut.kind = outermost_kind; + expr_mut.ty = new_ty; +} + +/// Allocates a fresh callable-value expression (plus any functor-wrapper nodes) +/// for `concrete` and returns the id of the outermost node. This is the +/// allocating analogue of the in-place substitution [`replace_callable_value`] +/// performs; it is used to materialize each element when rebuilding a forwarded +/// callable array so a multi-candidate array is not collapsed to a single +/// callable. `hint_ty` is the arrow type of the slot the value fills (the array +/// element type) and `span` is applied to every allocated node. Returns `None` +/// for a dynamic callable, which has no concrete value to emit. +fn alloc_callable_value_expr( + package: &mut Package, + span: Span, + concrete: &ConcreteCallable, + hint_ty: &Ty, + assigner: &mut Assigner, +) -> Option { + let (base_kind, functor, base_ty) = build_callable_value_parts(package, concrete, hint_ty)?; + let new_ty = base_ty.unwrap_or_else(|| hint_ty.clone()); + + Some(alloc_functor_wrapped_expr( + package, assigner, base_kind, functor, &new_ty, span, + )) +} + +/// Rebuilds a forwarded callable array as a concrete array literal of its +/// candidates. An outer HOF that receives a callable array as a flat value can +/// forward it into an inner HOF that indexes it; substituting a single callable +/// there would collapse the whole array to one candidate and drop the other +/// closures' captures. Emitting an `ExprKind::Array` of the concrete callables +/// (each already carrying its own threaded capture) instead lets whole-program +/// re-analysis resolve the inner argument to the full candidate set and +/// specialize the inner indexed dispatch. `expr_id` must be array-typed; its +/// element order is preserved. +fn reconstruct_callable_array( + package: &mut Package, + expr_id: ExprId, + concrete_group: &[ConcreteCallable], + assigner: &mut Assigner, +) { + let expr = package.exprs.get(expr_id).expect("expr not found").clone(); + let Ty::Array(elem_ty) = &expr.ty else { + return; + }; + let elem_ty = elem_ty.as_ref().clone(); + + let mut elements = Vec::with_capacity(concrete_group.len()); + for concrete in concrete_group { + let Some(element_id) = + alloc_callable_value_expr(package, expr.span, concrete, &elem_ty, assigner) + else { + // A dynamic candidate cannot be materialized; leave the forwarded + // parameter in place so re-analysis treats the array as dynamic and + // falls back to the unspecialized path rather than miscompiling. + return; + }; + elements.push(element_id); + } + + let expr_mut = package.exprs.get_mut(expr_id).expect("expr not found"); + expr_mut.kind = ExprKind::Array(elements); + expr_mut.ty = expr.ty; +} + +/// Substitutes a forwarded callable-parameter use with its concrete value. +/// +/// A callable parameter forwarded by value can be a single callable or a whole +/// callable array threaded to an inner HOF. For an array-typed use the array +/// literal is rebuilt via [`reconstruct_callable_array`] so every candidate +/// survives and any surviving index expression (e.g. `arr[0]`) stays a valid +/// array index. A single-candidate specialization carries its value in +/// `concrete` with an empty `concrete_group`; it is still rebuilt as a +/// one-element array rather than collapsed to a scalar, which would leave an +/// index expression indexing a non-array value. A non-array use is replaced in +/// place with the single concrete value. +fn substitute_forwarded_callable( + package: &mut Package, + expr_id: ExprId, + concrete: &ConcreteCallable, + concrete_group: &[ConcreteCallable], + assigner: &mut Assigner, +) { + if matches!(package.get_expr(expr_id).ty, Ty::Array(_)) { + let group = if concrete_group.is_empty() { + std::slice::from_ref(concrete) + } else { + concrete_group + }; + reconstruct_callable_array(package, expr_id, group, assigner); + } else { + replace_callable_value(package, expr_id, concrete, assigner); + } +} + +/// Returns true when an expression is a field chain rooted at `param_var` +/// and its collected field path exactly matches `field_path`. +fn expr_matches_param_field_path( + package: &Package, + expr_id: ExprId, + param_var: LocalVarId, + field_path: &[usize], +) -> bool { + collect_field_path_from_param(package, expr_id, param_var) + .is_some_and(|path| path == field_path) +} + +/// Recognizes an `array[index]` expression whose array is the callable-array +/// parameter at `field_path`, returning the array and index sub-expressions. +/// +/// Returns `None` when the expression is not an index into that parameter. +fn indexed_callable_array_param_source( + package: &Package, + expr_id: ExprId, + param_var: LocalVarId, + field_path: &[usize], +) -> Option<(ExprId, ExprId)> { + let ExprKind::Index(array_id, index_id) = package.get_expr(expr_id).kind else { + return None; + }; + expr_matches_param_field_path(package, array_id, param_var, field_path) + .then_some((array_id, index_id)) +} + +#[allow(clippy::too_many_arguments)] +fn replace_indexed_callable_array_call( + package: &mut Package, + package_id: PackageId, + call_expr_id: ExprId, + callee_expr_id: ExprId, + args_id: ExprId, + array_id: ExprId, + index_id: ExprId, + body_functor: FunctorApp, + concrete_group: &[ConcreteCallable], + assigner: &mut Assigner, +) { + let Some(first) = concrete_group.first() else { + return; + }; + + let branch_callables: Vec = concrete_group + .iter() + .map(|concrete| apply_body_functor_to_concrete(concrete, body_functor)) + .collect(); + + if branch_callables.len() == 1 { + let branch_callable = branch_callables + .first() + .expect("branch callable should exist"); + replace_callee( + package, + package_id, + callee_expr_id, + body_functor, + first, + assigner, + ); + rewrite_indexed_closure_dispatch_args(package, args_id, branch_callable, assigner); + return; + } + + let Ty::Array(item_ty) = package.get_expr(array_id).ty.clone() else { + return; + }; + + let result_ty = package.get_expr(call_expr_id).ty.clone(); + let span = package.get_expr(call_expr_id).span; + let original_args = package.get_expr(args_id).clone(); + + let mut branch_calls = Vec::with_capacity(branch_callables.len()); + for (position, branch_callable) in branch_callables.iter().enumerate() { + let call_id = alloc_dispatch_branch_call( + package, + package_id, + span, + &result_ty, + item_ty.as_ref(), + &original_args, + branch_callable, + assigner, + ); + branch_calls.push((position, call_id)); + } + + let mut dispatch_id = branch_calls.last().expect("branch exists").1; + for (position, call_id) in branch_calls.into_iter().rev().skip(1) { + let condition_id = alloc_index_eq_expr(package, index_id, position, span, assigner); + dispatch_id = alloc_if_expr( + package, + span, + &result_ty, + condition_id, + call_id, + dispatch_id, + assigner, + ); + } + + let dispatch = package.get_expr(dispatch_id).clone(); + let call_expr = package + .exprs + .get_mut(call_expr_id) + .expect("call expr not found"); + call_expr.kind = dispatch.kind; + call_expr.ty = dispatch.ty; +} + +/// Composes a functor application drawn from a callable value's body onto a +/// concrete callable, folding it into the callable's accumulated functor. +fn apply_body_functor_to_concrete( + concrete: &ConcreteCallable, + body_functor: FunctorApp, +) -> ConcreteCallable { + match concrete { + ConcreteCallable::Global { item_id, functor } => ConcreteCallable::Global { + item_id: *item_id, + functor: compose_functors(functor, &body_functor), + }, + ConcreteCallable::Closure { + target, + captures, + functor, + } => ConcreteCallable::Closure { + target: *target, + captures: captures.clone(), + functor: compose_functors(functor, &body_functor), + }, + ConcreteCallable::Dynamic => ConcreteCallable::Dynamic, + } +} + +#[allow(clippy::too_many_arguments)] +fn alloc_dispatch_branch_call( + package: &mut Package, + package_id: PackageId, + span: Span, + result_ty: &Ty, + callee_ty: &Ty, + original_args: &Expr, + concrete: &ConcreteCallable, + assigner: &mut Assigner, +) -> ExprId { + let (item_id, functor, target) = match concrete { + ConcreteCallable::Closure { + target, functor, .. + } => ( + ItemId { + package: package_id, + item: *target, + }, + *functor, + Some(*target), + ), + ConcreteCallable::Global { item_id, functor } => (*item_id, *functor, None), + ConcreteCallable::Dynamic => return original_args.id, + }; + + let controlled_layers = usize::from(functor.controlled); + let direct_ty = match concrete { + ConcreteCallable::Closure { target, .. } => { + build_direct_target_callee_ty(package, *target, callee_ty, controlled_layers) + .unwrap_or_else(|| callee_ty.clone()) + } + _ => target + .and_then(|target| { + build_direct_target_callee_ty(package, target, callee_ty, controlled_layers) + }) + .unwrap_or_else(|| callee_ty.clone()), + }; + + let callee_id = alloc_item_var_expr(package, assigner, item_id, direct_ty, span); + + let args_id = alloc_expr( + package, + assigner, + original_args.ty.clone(), + original_args.kind.clone(), + original_args.span, + ); + + if let ConcreteCallable::Closure { + target, captures, .. + } = concrete + { + if let Some(target_input) = target_callable_input(package, *target) { + rewrite_closure_dispatch_branch_args( + package, + args_id, + captures, + &target_input, + controlled_layers, + assigner, + ); + } else { + let capture_bindings: Vec<(LocalVarId, Ty)> = captures + .iter() + .map(|capture| (capture.var, capture.ty.clone())) + .collect(); + prepend_capture_args_to_call( + package, + args_id, + &capture_bindings, + controlled_layers, + assigner, + ); + } + } + + alloc_call_expr( + package, + assigner, + callee_id, + args_id, + result_ty.clone(), + span, + ) +} + +/// Returns the input pattern type of the callable `target`, or `None` when the +/// item is not a callable. +fn target_callable_input(package: &Package, target: LocalItemId) -> Option { + let ItemKind::Callable(decl) = &package.get_item(target).kind else { + return None; + }; + Some(package.get_pat(decl.input).ty.clone()) +} + +/// Threads a closure's captured values into the argument tuple of a call to its +/// dispatch target. +/// +/// When a closure is specialized, its captured variables become ordinary +/// leading parameters of the target callable. This rewrites the call's argument +/// expression so the captures are passed first, followed by the original +/// arguments. A non-closure concrete is left unchanged. +fn rewrite_indexed_closure_dispatch_args( + package: &mut Package, + args_id: ExprId, + concrete: &ConcreteCallable, + assigner: &mut Assigner, +) { + let ConcreteCallable::Closure { + target, + captures, + functor, + } = concrete + else { + return; + }; + + let capture_bindings: Vec<(LocalVarId, Ty)> = captures + .iter() + .map(|capture| (capture.var, capture.ty.clone())) + .collect(); + rewrite_closure_target_args( + package, + args_id, + *target, + &capture_bindings, + usize::from(functor.controlled), + assigner, + ); +} + +/// Rewrites a call's argument expression so the closure target receives its +/// captured values alongside the original arguments. +/// +/// When the target's input type is known, the arguments are reshaped to match +/// it via [`rewrite_closure_dispatch_branch_args`]; otherwise the captures are +/// simply prepended to the existing argument tuple. +fn rewrite_closure_target_args( + package: &mut Package, + args_id: ExprId, + target: LocalItemId, + capture_bindings: &[(LocalVarId, Ty)], + controlled_layers: usize, + assigner: &mut Assigner, +) { + if let Some(target_input) = target_callable_input(package, target) { + let captures: Vec = capture_bindings + .iter() + .map(|(var, ty)| CapturedVar { + var: *var, + ty: ty.clone(), + expr: None, + caller_substitutions: Vec::new(), + }) + .collect(); + rewrite_closure_dispatch_branch_args( + package, + args_id, + &captures, + &target_input, + controlled_layers, + assigner, + ); + } else { + prepend_capture_args_to_call( + package, + args_id, + capture_bindings, + controlled_layers, + assigner, + ); + } +} + +/// Rewrites `args_id` in place so a specialized closure target receives its +/// captured values followed by the original call arguments, shaped to match the +/// target's input type. +/// +/// A `Controlled` functor layer wraps the whole base input as `(controls, +/// base)` without splitting the base tuple. Each layer is peeled while threading +/// the full `target_input` down the recursion, so the captures are spliced in +/// only at the innermost, uncontrolled layer. Descending into `target_input[1]` +/// instead would let the base arguments coincidentally match the target input +/// and drop the captures on the controlled path. +fn rewrite_closure_dispatch_branch_args( + package: &mut Package, + args_id: ExprId, + captures: &[CapturedVar], + target_input: &Ty, + controlled_layers: usize, + assigner: &mut Assigner, +) { + if controlled_layers > 0 { + let inner_id = match package.get_expr(args_id).kind { + ExprKind::Tuple(ref elements) if elements.len() > 1 => elements[1], + _ => return, + }; + // A control layer wraps the ENTIRE base target input as `(ctls, base_input)`; + // it does NOT split the base input tuple. Thread the full `target_input` + // through the recursion so the capture prepend at the deepest layer matches the + // target op's uncontrolled input `captures..., original_args`. Descending into + // `target_input[1]` here would let the base args coincidentally equal the target + // input and short-circuit the capture splice, dropping the capture on the + // controlled path. + rewrite_closure_dispatch_branch_args( + package, + inner_id, + captures, + target_input, + controlled_layers - 1, + assigner, + ); + let inner_ty = package.get_expr(inner_id).ty.clone(); + let args_expr = package.exprs.get_mut(args_id).expect("args expr not found"); + if let Ty::Tuple(ref mut tys) = args_expr.ty + && tys.len() > 1 + { + tys[1] = inner_ty; + } + return; + } + + let Some((kind, ty)) = + build_closure_dispatch_branch_args_data(package, args_id, captures, target_input, assigner) + else { + return; + }; + + let args_expr = package.exprs.get_mut(args_id).expect("args expr not found"); + args_expr.kind = kind; + args_expr.ty = ty; +} + +/// Builds the argument-tuple kind and type for a closure dispatch branch, +/// combining the closure's captured values with the original arguments. +/// +/// Tries two layouts and returns the first that matches the target's input +/// type: a flattened tuple where captures and the original tuple's fields sit +/// side by side ([`flattened_capture_arg_data`]), then a grouped tuple where the +/// original argument tuple is kept as a single trailing element +/// ([`grouped_capture_arg_data`]). Returns `None` when neither layout applies. +fn build_closure_dispatch_branch_args_data( + package: &mut Package, + args_id: ExprId, + captures: &[CapturedVar], + target_input: &Ty, + assigner: &mut Assigner, +) -> Option<(ExprKind, Ty)> { + let original_args = package.get_expr(args_id).clone(); + if original_args.ty == *target_input { + return Some((original_args.kind, original_args.ty)); + } + if captures.is_empty() + && let ExprKind::Tuple(elements) = &original_args.kind + && let [single] = elements.as_slice() + { + let single_expr = package.get_expr(*single); + if single_expr.ty == *target_input { + return Some((single_expr.kind.clone(), single_expr.ty.clone())); + } + } + + let capture_ids = allocate_capture_exprs(package, original_args.span, captures, assigner); + let capture_tys: Vec = captures.iter().map(|capture| capture.ty.clone()).collect(); + + let flattened = flattened_capture_arg_data( + package, + &original_args, + capture_ids.as_slice(), + &capture_tys, + target_input, + ); + if let Some(data) = flattened { + return Some(data); + } + + let grouped = grouped_capture_arg_data( + package, + &original_args, + capture_ids.as_slice(), + &capture_tys, + target_input, + assigner, + ); + grouped.or_else(|| { + captures + .is_empty() + .then_some((original_args.kind, original_args.ty)) + }) +} + +/// Builds a flattened capture-plus-argument tuple, where the captures and the +/// original tuple's fields become sibling elements. +/// +/// Returns `Some` only when `[capture_tys..., arg_tys...]` matches the target +/// input tuple exactly; otherwise `None`, so the caller can try another layout. +fn flattened_capture_arg_data( + package: &Package, + original_args: &Expr, + capture_ids: &[ExprId], + capture_tys: &[Ty], + target_input: &Ty, +) -> Option<(ExprKind, Ty)> { + let Ty::Tuple(target_items) = target_input else { + return None; + }; + let ExprKind::Tuple(arg_items) = &original_args.kind else { + return None; + }; + let Ty::Tuple(arg_tys) = &original_args.ty else { + return None; + }; + + let expected_tys: Vec = capture_tys + .iter() + .cloned() + .chain(arg_tys.iter().cloned()) + .collect(); + if expected_tys != *target_items { + return None; + } + + let elements: Vec = capture_ids + .iter() + .copied() + .chain(arg_items.iter().copied()) + .collect(); + Some(build_expr_data_from_elements(package, elements)) +} + +/// Builds a grouped capture-plus-argument tuple, where the captures are +/// followed by the original argument tuple as one trailing element. +/// +/// Returns `Some` only when `(capture_tys..., original_arg_ty)` matches the +/// target input; otherwise `None`. The original argument expression is copied +/// into a fresh node so it can be reused as the trailing element. +fn grouped_capture_arg_data( + package: &mut Package, + original_args: &Expr, + capture_ids: &[ExprId], + capture_tys: &[Ty], + target_input: &Ty, + assigner: &mut Assigner, +) -> Option<(ExprKind, Ty)> { + let expected_ty = if capture_tys.is_empty() { + original_args.ty.clone() + } else { + let mut tys = capture_tys.to_vec(); + tys.push(original_args.ty.clone()); + Ty::Tuple(tys) + }; + if &expected_ty != target_input { + return None; + } + + if capture_ids.is_empty() { + return Some((original_args.kind.clone(), original_args.ty.clone())); + } + + let preserved_args_id = alloc_expr( + package, + assigner, + original_args.ty.clone(), + original_args.kind.clone(), + original_args.span, + ); + + let mut elements = capture_ids.to_vec(); + elements.push(preserved_args_id); + Some(build_expr_data_from_elements(package, elements)) +} + +/// Allocates one expression per captured variable, to be passed as leading +/// arguments at a specialized call site. +/// +/// A capture with a recorded initializer expression reuses it; otherwise a +/// fresh `Var(Res::Local)` reference to the captured variable is synthesized. +fn allocate_capture_exprs( + package: &mut Package, + span: Span, + captures: &[CapturedVar], + assigner: &mut Assigner, +) -> Vec { + let mut ids = Vec::with_capacity(captures.len()); + + for capture in captures { + if let Some(expr_id) = capture.expr { + ids.push(expr_id); + continue; + } + + let expr_id = + alloc_local_var_expr(package, assigner, capture.var, capture.ty.clone(), span); + ids.push(expr_id); + } + + ids +} + +/// Builds the `ExprKind` and `Ty` for a tuple of the given elements, collapsing +/// the degenerate cases: an empty list becomes `Unit`, and a single element is +/// returned as-is rather than wrapped in a one-tuple. +fn build_expr_data_from_elements(package: &Package, elements: Vec) -> (ExprKind, Ty) { + match elements.as_slice() { + [] => (ExprKind::Tuple(Vec::new()), Ty::UNIT), + [single] => { + let expr = package.get_expr(*single); + (expr.kind.clone(), expr.ty.clone()) + } + _ => { + let tys = elements + .iter() + .map(|&expr_id| package.get_expr(expr_id).ty.clone()) + .collect(); + (ExprKind::Tuple(elements), Ty::Tuple(tys)) + } } } -/// Returns true when an expression is a field chain rooted at `param_var` -/// and its collected field path exactly matches `field_path`. -fn expr_matches_param_field_path( - package: &Package, - expr_id: ExprId, - param_var: LocalVarId, - field_path: &[usize], -) -> bool { - collect_field_path_from_param(package, expr_id, param_var) - .is_some_and(|path| path == field_path) +/// Synthesizes the boolean condition `index_expr == index_value`, used to +/// select one arm of an index-dispatch chain. +fn alloc_index_eq_expr( + package: &mut Package, + index_expr_id: ExprId, + index_value: usize, + span: Span, + assigner: &mut Assigner, +) -> ExprId { + let index_value = i64::try_from(index_value).expect("dispatch index should fit in i64"); + let lit_id = alloc_int_lit(package, assigner, index_value, span); + alloc_bin_op_expr( + package, + assigner, + BinOp::Eq, + index_expr_id, + lit_id, + Ty::Prim(Prim::Bool), + span, + ) +} + +/// Synthesizes an `if condition { true_id } else { false_id }` expression with +/// the given result type. +fn alloc_if_expr( + package: &mut Package, + span: Span, + result_ty: &Ty, + condition_id: ExprId, + true_id: ExprId, + false_id: ExprId, + assigner: &mut Assigner, +) -> ExprId { + crate::fir_builder::alloc_if_expr( + package, + assigner, + condition_id, + true_id, + Some(false_id), + result_ty.clone(), + span, + ) } /// Collects field indices from nested `Field(Path)` expressions rooted at `param_var`. @@ -1266,52 +3202,19 @@ fn replace_callee( expr.ty = callee_ty; } else { // Allocate fresh expressions for functor wrapper layers. - let mut current_id = assigner.next_expr(); - package.exprs.insert( - current_id, - Expr { - id: current_id, - span: callee_span, - ty: callee_ty.clone(), - kind: base_kind, - exec_graph_range: EMPTY_EXEC_RANGE, - }, + let outer_id = alloc_functor_wrapped_expr( + package, + assigner, + base_kind, + effective, + &callee_ty, + callee_span, ); - if effective.adjoint { - let adj_id = assigner.next_expr(); - package.exprs.insert( - adj_id, - Expr { - id: adj_id, - span: callee_span, - ty: callee_ty.clone(), - kind: ExprKind::UnOp(UnOp::Functor(Functor::Adj), current_id), - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); - current_id = adj_id; - } - - for _ in 0..effective.controlled { - let ctl_id = assigner.next_expr(); - package.exprs.insert( - ctl_id, - Expr { - id: ctl_id, - span: callee_span, - ty: callee_ty.clone(), - kind: ExprKind::UnOp(UnOp::Functor(Functor::Ctl), current_id), - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); - current_id = ctl_id; - } - // Copy the outermost node's kind into the original callee expr. let outermost_kind = package .exprs - .get(current_id) + .get(outer_id) .expect("expr not found") .kind .clone(); @@ -1413,20 +3316,21 @@ fn transform_closure_param_capture( capture_idx: usize, param: &CallableParam, concrete: &ConcreteCallable, - specialized_capture_targets: &mut FxHashSet, + specialized_capture_targets: &mut FxHashSet, assigner: &mut Assigner, ) { // The lambda item is shared across the enclosing callable's functored specs. // Only the first referring closure specializes it; sibling closures must not // re-run that mutation against the already-rewritten lambda. Each closure // still drops the capture from its own capture list independently. - if specialized_capture_targets.insert(closure_target) { + let capture_key = (closure_target, param.param_var); + if specialized_capture_targets.insert(capture_key) { specialize_closure_target_for_captured_param( package, package_id, closure_target, capture_idx, - param, + ¶m.param_ty, concrete, assigner, ); @@ -1452,7 +3356,7 @@ fn specialize_closure_target_for_captured_param( package_id: PackageId, closure_target: LocalItemId, capture_idx: usize, - param: &CallableParam, + capture_ty: &Ty, concrete: &ConcreteCallable, assigner: &mut Assigner, ) { @@ -1496,23 +3400,69 @@ fn specialize_closure_target_for_captured_param( capture_idx, Vec::new(), capture_param_var, - param.param_ty.clone(), + capture_ty.clone(), matches!(package.get_pat(target_decl.input).kind, PatKind::Tuple(_)), ); // Step 3: Transform the target callable's body to replace uses of the - // captured param with the concrete callable. + // captured param with the concrete callable. This rewrites a distinct + // callable, the closure target, so it uses its own fresh dedup set. + let mut specialized_capture_targets: FxHashSet = FxHashSet::default(); transform_callable_body( package, package_id, &target_decl.implementation, &closure_param, concrete, + &[], + &mut specialized_capture_targets, assigner, ); // Step 4: Remove the capture binding from the target callable's input. remove_capture_from_closure_target(package, closure_target, capture_idx); + refresh_callable_types(package, closure_target); +} + +/// Re-runs the post-transform type-refresh cascade over a callable item's +/// implementation, re-establishing `PostDefunc` type consistency after its body +/// or signature was rewritten in place (e.g. a captured callable was baked in). +fn refresh_callable_types(package: &mut Package, item_id: LocalItemId) { + let Some(Item { + kind: ItemKind::Callable(decl), + .. + }) = package.items.get(item_id) + else { + return; + }; + let implementation = decl.implementation.clone(); + refresh_rewritten_value_types(package, &implementation); +} + +/// Specializes a closure target for its captured concrete callable. +/// +/// Inlines the recorded [`CapturedCallableSpecialization`] into the target body +/// and drops its capture parameter via +/// [`specialize_closure_target_for_captured_param`]. +fn specialize_closure_target_callable_capture( + package: &mut Package, + package_id: PackageId, + closure_target: LocalItemId, + callable_capture: Option<&CapturedCallableSpecialization>, + assigner: &mut Assigner, +) { + let Some(capture) = callable_capture else { + return; + }; + specialize_closure_target_for_captured_param( + package, + package_id, + closure_target, + capture.capture_idx, + &capture.capture_ty, + &capture.concrete, + assigner, + ); } /// Removes the capture at `capture_idx` from the closure target callable's @@ -1621,6 +3571,7 @@ fn thread_closure_captures( decl: &mut CallableDecl, _param: &CallableParam, captures: &[CapturedVar], + name_offset: usize, ) -> Vec<(LocalVarId, Ty)> { if captures.is_empty() { return Vec::new(); @@ -1636,7 +3587,11 @@ fn thread_closure_captures( let new_local_var = cloner.alloc_local(capture.var); capture_bindings.push((new_local_var, capture.ty.clone())); - let name: Rc = Rc::from(format!("{CAPTURE_NAME_PREFIX}_{i}")); + // `name_offset` continues the capture counter across parameters so a + // multi-argument specialization gets `_.capture_0`, `_.capture_1`, … + // without collisions; single-argument callers pass `0`, preserving the + // original spelling. + let name: Rc = Rc::from(format!("{CAPTURE_NAME_PREFIX}_{}", name_offset + i)); let new_pat = Pat { id: new_pat_id, span: Span::default(), @@ -1695,6 +3650,168 @@ fn thread_closure_captures( capture_bindings } +/// Returns whether an expression resolves to a direct callee of the given +/// closure target item. +/// +/// Matches both a direct `Var(Res::Item)` reference to the target and a +/// capture-free `Closure` over it; any other expression (or a closure still +/// carrying captures) is not treated as a direct call to the target. +fn expr_is_closure_target_callee( + package: &Package, + expr_id: ExprId, + _package_id: PackageId, + closure_target: LocalItemId, +) -> bool { + match &package.get_expr(expr_id).kind { + ExprKind::Var( + Res::Item(ItemId { + item: callee_item, .. + }), + _, + ) => *callee_item == closure_target, + ExprKind::Closure(captures, target) => captures.is_empty() && *target == closure_target, + _ => false, + } +} + +/// Updates a callee expression's arrow type to the closure target's direct-call +/// signature after retargeting, peeling `controlled_layers` control tuples to +/// reach the right input slot. No-op when the callee type is not an arrow. +fn refresh_closure_target_callee_expr_ty( + package: &mut Package, + callee_id: ExprId, + closure_target: LocalItemId, + controlled_layers: usize, +) { + let original_ty = package.get_expr(callee_id).ty.clone(); + if let Some(new_ty) = + build_direct_target_callee_ty(package, closure_target, &original_ty, controlled_layers) + { + package + .exprs + .get_mut(callee_id) + .expect("callee expr not found") + .ty = new_ty; + } +} + +/// Read-only collector that records every `Call` expression whose callee +/// resolves to a specific closure-target item in `package_id` after peeling +/// functor wrappers. Mirrors the matcher in +/// [`rewrite_closure_target_call_args_in_expr`] so the call sets agree. +struct ClosureTargetCallCollector<'a> { + package: &'a Package, + package_id: PackageId, + closure_target: LocalItemId, + calls: FxHashSet, +} + +impl<'a> Visitor<'a> for ClosureTargetCallCollector<'a> { + fn get_block(&self, id: BlockId) -> &'a Block { + self.package.get_block(id) + } + + fn get_expr(&self, id: ExprId) -> &'a Expr { + self.package.get_expr(id) + } + + fn get_pat(&self, id: PatId) -> &'a Pat { + self.package.get_pat(id) + } + + fn get_stmt(&self, id: StmtId) -> &'a Stmt { + self.package.get_stmt(id) + } + + fn visit_expr(&mut self, id: ExprId) { + if let ExprKind::Call(callee_id, _) = self.package.get_expr(id).kind { + let (base_id, _) = peel_body_functors(self.package, callee_id); + if expr_is_closure_target_callee( + self.package, + base_id, + self.package_id, + self.closure_target, + ) { + self.calls.insert(id); + } + } + visit::walk_expr(self, id); + } +} + +/// Collects the set of `Call` expressions in a callable implementation whose +/// callee resolves to `closure_target`. Used by [`specialize_many`] to scope a +/// closure's capture-prepend to exactly the calls a parameter retargeted. +fn collect_calls_to_closure_target( + package: &Package, + callable_impl: &CallableImpl, + package_id: PackageId, + closure_target: LocalItemId, +) -> FxHashSet { + let mut collector = ClosureTargetCallCollector { + package, + package_id, + closure_target, + calls: FxHashSet::default(), + }; + match callable_impl { + CallableImpl::Intrinsic => {} + CallableImpl::Spec(spec_impl) => { + collector.visit_block(spec_impl.body.block); + if let Some(adj) = &spec_impl.adj { + collector.visit_block(adj.block); + } + if let Some(ctl) = &spec_impl.ctl { + collector.visit_block(ctl.block); + } + if let Some(ctl_adj) = &spec_impl.ctl_adj { + collector.visit_block(ctl_adj.block); + } + } + CallableImpl::SimulatableIntrinsic(spec_decl) => { + collector.visit_block(spec_decl.block); + } + } + collector.calls +} + +/// Prepends a closure's captured operands to a specific set of already-located +/// `Call` expressions, the calls a single parameter retargeted, re-peeling each +/// call's functor wrappers to recover its controlled-layer count. This is the +/// scoped counterpart to [`rewrite_closure_target_call_args`], which walks a +/// whole body for one closure target. +fn prepend_captures_to_calls( + package: &mut Package, + call_ids: &[ExprId], + package_id: PackageId, + closure_target: LocalItemId, + capture_bindings: &[(LocalVarId, Ty)], + assigner: &mut Assigner, +) { + for &call_id in call_ids { + let ExprKind::Call(callee_id, args_id) = package.get_expr(call_id).kind else { + continue; + }; + let (base_id, outer_functor) = peel_body_functors(package, callee_id); + if expr_is_closure_target_callee(package, base_id, package_id, closure_target) { + refresh_closure_target_callee_expr_ty( + package, + callee_id, + closure_target, + usize::from(outer_functor.controlled), + ); + rewrite_closure_target_args( + package, + args_id, + closure_target, + capture_bindings, + usize::from(outer_functor.controlled), + assigner, + ); + } + } +} + /// Rewrites the call-argument expression for a closure target by splicing /// the captured bindings into the appropriate slot of the call's argument /// tuple. @@ -1873,20 +3990,17 @@ fn rewrite_closure_target_call_args_in_expr( ); let (base_id, outer_functor) = peel_body_functors(package, callee_id); - let base_expr = package.get_expr(base_id); - if matches!( - base_expr.kind, - ExprKind::Var( - Res::Item(ItemId { - package: callee_package, - item: callee_item, - }), - _ - ) if callee_package == package_id && callee_item == closure_target - ) { - prepend_capture_args_to_call( + if expr_is_closure_target_callee(package, base_id, package_id, closure_target) { + refresh_closure_target_callee_expr_ty( + package, + callee_id, + closure_target, + usize::from(outer_functor.controlled), + ); + rewrite_closure_target_args( package, args_id, + closure_target, capture_bindings, usize::from(outer_functor.controlled), assigner, @@ -2115,6 +4229,10 @@ fn prepend_capture_args_to_call( controlled_layers: usize, assigner: &mut Assigner, ) { + if capture_bindings.is_empty() { + return; + } + if controlled_layers > 0 { let inner_id = match package.get_expr(args_id).kind { ExprKind::Tuple(ref elements) if elements.len() > 1 => elements[1], @@ -2138,31 +4256,23 @@ fn prepend_capture_args_to_call( } let original_args = package.get_expr(args_id).clone(); - let preserved_args_id = assigner.next_expr(); - package.exprs.insert( - preserved_args_id, - Expr { - id: preserved_args_id, - span: original_args.span, - ty: original_args.ty.clone(), - kind: original_args.kind, - exec_graph_range: EMPTY_EXEC_RANGE, - }, + let preserved_args_id = alloc_expr( + package, + assigner, + original_args.ty.clone(), + original_args.kind, + original_args.span, ); let mut tuple_items = Vec::with_capacity(capture_bindings.len() + 1); let mut tuple_tys = Vec::with_capacity(capture_bindings.len() + 1); for (capture_var, capture_ty) in capture_bindings { - let capture_expr_id = assigner.next_expr(); - package.exprs.insert( - capture_expr_id, - Expr { - id: capture_expr_id, - span: original_args.span, - ty: capture_ty.clone(), - kind: ExprKind::Var(Res::Local(*capture_var), Vec::new()), - exec_graph_range: EMPTY_EXEC_RANGE, - }, + let capture_expr_id = alloc_local_var_expr( + package, + assigner, + *capture_var, + capture_ty.clone(), + original_args.span, ); tuple_items.push(capture_expr_id); tuple_tys.push(capture_ty.clone()); @@ -2175,25 +4285,194 @@ fn prepend_capture_args_to_call( args_expr.ty = Ty::Tuple(tuple_tys); } -/// Removes the callable parameter from the specialized callable's input -/// pattern and updates the corresponding types. +/// Collects the block ids of every specialization of a callable implementation: +/// `body`, `adj`, `ctl`, and `ctl_adj`. +fn spec_block_ids(callable_impl: &CallableImpl) -> Vec { + let mut ids = Vec::new(); + match callable_impl { + CallableImpl::Intrinsic => {} + CallableImpl::Spec(spec_impl) => { + ids.push(spec_impl.body.block); + if let Some(ref adj) = spec_impl.adj { + ids.push(adj.block); + } + if let Some(ref ctl) = spec_impl.ctl { + ids.push(ctl.block); + } + if let Some(ref ctl_adj) = spec_impl.ctl_adj { + ids.push(ctl_adj.block); + } + } + CallableImpl::SimulatableIntrinsic(spec_decl) => ids.push(spec_decl.block), + } + ids +} + +/// Drops `let = ` destructuring statements from every +/// specialization block. +/// +/// The combined nested path in [`specialize_many`] removes a tuple-valued +/// parameter slot once all of its arrow fields have been specialized away and +/// their projected calls retargeted. The destructuring that bound those fields +/// is then dead and still references the slot about to be removed, so it must +/// be dropped to avoid a dangling parameter reference. Orphaned statement nodes +/// are reclaimed by the later unreachable-node collection. +/// +/// Only statements whose initializer is a direct `Var(Local(param_var))` are +/// removed; tuple fields reached through intermediate alias bindings are not +/// covered, which keeps the combined path scoped to direct destructuring. +fn remove_param_destructuring_stmts( + package: &mut Package, + callable_impl: &CallableImpl, + param_vars: &FxHashSet, +) { + for block_id in spec_block_ids(callable_impl) { + let stmt_ids = package + .blocks + .get(block_id) + .expect("block not found") + .stmts + .clone(); + let mut retained = Vec::with_capacity(stmt_ids.len()); + for stmt_id in stmt_ids { + let stmt = package.stmts.get(stmt_id).expect("stmt not found"); + let drop_stmt = if let qsc_fir::fir::StmtKind::Local(_, _, expr_id) = &stmt.kind { + let init = package.exprs.get(*expr_id).expect("expr not found"); + matches!(&init.kind, ExprKind::Var(Res::Local(var), _) if param_vars.contains(var)) + } else { + false + }; + if !drop_stmt { + retained.push(stmt_id); + } + } + package + .blocks + .get_mut(block_id) + .expect("block not found") + .stmts = retained; + } +} + +/// Removes several callable parameter slots from a specialized callable's input +/// pattern in a single pass, updating the corresponding types. /// /// # Before /// ```text -/// input = (param_0, callable_param, param_2) // top_level_param = 1 +/// input = (param_0, callable_param, param_2) // a removed slot at index 1 /// ``` /// # After /// ```text -/// input = (param_0, param_2) // callable_param removed +/// input = (param_0, param_2) // removed slots dropped /// ``` /// /// # Mutations /// - Rewrites the input `Pat` node's `kind` and `ty` in place. /// - Flattens single-element tuples. /// - For nested params, delegates to [`remove_nested_callable_param`]. -fn remove_callable_param(package: &mut Package, decl: &mut CallableDecl, param: &CallableParam) { +/// +/// Used by [`specialize_many`] when more than one arrow parameter is removed at +/// once: filtering the slots one at a time would shift the surviving indices +/// between removals. Each member is removed by its top-level slot. A nested +/// member, one selecting an arrow field of a tuple-valued parameter, only +/// reaches here when its group covers every field of that tuple, which +/// [`super::is_combined_eligible`] checks, so its whole top-level slot is +/// dropped and the dead destructuring of that slot is removed beforehand by +/// [`remove_param_destructuring_stmts`]. +/// +/// `num_appended_captures` is the count of capture patterns already appended to +/// the input by [`thread_closure_captures`]. The surviving input is flattened to +/// a scalar only when exactly one element remains and no captures were appended, +/// so a lone surviving capture keeps its tuple shape and stays aligned with the +/// call-site rewrite that supplies the capture operand. +fn remove_callable_params( + package: &mut Package, + decl: &mut CallableDecl, + params: &[&CallableParam], + num_appended_captures: usize, +) { + // Every member is removed by its top-level slot. A nested member, one that + // selects an arrow field of a tuple-valued parameter, reaches here only when + // its group covers the whole tuple, which `is_combined_eligible` checks, so + // the entire slot is dropped; the now-dead destructuring of that slot is + // removed separately by `remove_param_destructuring_stmts`. + let remove: FxHashSet = params.iter().map(|p| p.top_level_param).collect(); + + let input_pat = package + .pats + .get(decl.input) + .expect("input pat not found") + .clone(); + + match &input_pat.kind { + PatKind::Tuple(pats) => { + let tys = match &input_pat.ty { + Ty::Tuple(tys) => tys.clone(), + _ => vec![input_pat.ty.clone(); pats.len()], + }; + + let mut new_pats: Vec = Vec::new(); + let mut new_tys: Vec = Vec::new(); + for (i, (&pat_id, ty)) in pats.iter().zip(tys.iter()).enumerate() { + if !remove.contains(&i) { + new_pats.push(pat_id); + new_tys.push(ty.clone()); + } + } + + if new_pats.len() == 1 && num_appended_captures == 0 { + // Flatten single-element tuple to the single pattern. + decl.input = new_pats[0]; + } else { + let input_pat_mut = package.pats.get_mut(decl.input).expect("pat not found"); + input_pat_mut.kind = PatKind::Tuple(new_pats); + input_pat_mut.ty = Ty::Tuple(new_tys); + } + } + PatKind::Bind(_) => { + // A single tuple-valued parameter whose every arrow field is + // specialized away leaves no surviving input. Captures, when + // present, are threaded by wrapping the bind in a tuple before this + // runs, so reaching the bind arm means no captures were appended and + // the input collapses to unit. + debug_assert!( + num_appended_captures == 0, + "captures wrap the input in a tuple before removal" + ); + let input_pat_mut = package.pats.get_mut(decl.input).expect("pat not found"); + input_pat_mut.kind = PatKind::Tuple(Vec::new()); + input_pat_mut.ty = Ty::UNIT; + } + PatKind::Discard => { + // A discard input binds nothing to remove. + } + } +} + +/// Removes a top-level callable parameter from the specialized callable's +/// input pattern, collapsing a singleton tuple to a bare pattern and +/// replacing a bare-bind pattern with `Unit` when the removed parameter was +/// the only one. +/// +/// Delegates to [`remove_nested_callable_param`] when `param.field_path` is +/// non-empty (the callable argument is nested inside a tuple parameter). +/// +/// # Before +/// ```text +/// input = (a, callable, b) // top_level_param = 1 +/// ``` +/// # After +/// ```text +/// input = (a, b) // callable removed, tuple shrunk +/// ``` +fn remove_callable_param( + package: &mut Package, + decl: &mut CallableDecl, + param: &CallableParam, + had_closure_captures: bool, +) { if !param.field_path.is_empty() { - remove_nested_callable_param(package, decl, param); + remove_nested_callable_param(package, decl, param, had_closure_captures); return; } @@ -2261,6 +4540,7 @@ fn remove_nested_callable_param( package: &mut Package, decl: &mut CallableDecl, param: &CallableParam, + had_closure_captures: bool, ) { let input_pat = package .pats @@ -2271,6 +4551,15 @@ fn remove_nested_callable_param( let outer_idx = param.top_level_param; let inner_path = param.field_path.as_slice(); + // Set when the nested removal consumes the parameter's entire tuple, a + // single-field tuple whose only element was an inlined arrow, and closure + // captures were threaded as new slots. In that case the parameter's slot and + // its destructuring are removed rather than retyped to unit, matching the + // call site that drops the consumed slot and supplies the captures. Without + // threaded captures the call site keeps the slot as `()`, so the parameter + // is retyped to unit; the captureless path is unchanged. + let mut fully_consumed = false; + // Structural type of the parameter before its callable field is removed. // Captured so sibling field accesses in the body can be reindexed against // the parameter's pre-removal shape. @@ -2281,13 +4570,44 @@ fn remove_nested_callable_param( let sub_pat = package.pats.get(sub_pat_id).expect("pat not found").clone(); let orig_ty = sub_pat.ty.clone(); let new_ty = remove_ty_at_nested_path(package, &sub_pat.ty, inner_path); - let sub_pat_mut = package.pats.get_mut(sub_pat_id).expect("pat not found"); - sub_pat_mut.ty = new_ty.clone(); + fully_consumed = + had_closure_captures && matches!(new_ty, Ty::Tuple(ref t) if t.is_empty()); + + if fully_consumed { + // The removal consumed the parameter's entire tuple, a + // single-field tuple whose only element was an inlined arrow. + // The sub-slot carries no surviving input, so drop it from the + // outer tuple, mirroring the non-nested removal, instead of + // leaving a phantom unit parameter that the call site does not + // supply. The dead destructuring of this slot is removed below. + let tys = match &input_pat.ty { + Ty::Tuple(tys) => tys.clone(), + _ => vec![input_pat.ty.clone(); pats.len()], + }; + let mut new_pats: Vec = Vec::new(); + let mut new_tys: Vec = Vec::new(); + for (i, (&pat_id, ty)) in pats.iter().zip(tys.iter()).enumerate() { + if i != outer_idx { + new_pats.push(pat_id); + new_tys.push(ty.clone()); + } + } + // Keep the surviving slots as a tuple rather than flattening a + // lone survivor. The call-site rewrite preserves the outer + // tuple shape, swapping the consumed callable for its appended + // capture, so the specialized input must stay a tuple to match. + let input_pat_mut = package.pats.get_mut(decl.input).expect("pat not found"); + input_pat_mut.kind = PatKind::Tuple(new_pats); + input_pat_mut.ty = Ty::Tuple(new_tys); + } else { + let sub_pat_mut = package.pats.get_mut(sub_pat_id).expect("pat not found"); + sub_pat_mut.ty = new_ty.clone(); - // Update the outer tuple's type to reflect the changed sub-parameter. - let input_pat_mut = package.pats.get_mut(decl.input).expect("pat not found"); - if let Ty::Tuple(ref mut tys) = input_pat_mut.ty { - tys[outer_idx] = new_ty; + // Update the outer tuple's type to reflect the changed sub-parameter. + let input_pat_mut = package.pats.get_mut(decl.input).expect("pat not found"); + if let Ty::Tuple(ref mut tys) = input_pat_mut.ty { + tys[outer_idx] = new_ty; + } } Some(orig_ty) } @@ -2321,7 +4641,15 @@ fn remove_nested_callable_param( // Rewrite destructuring patterns in the body that bind param_var's tuple. if !inner_path.is_empty() { - if let CallableImpl::Spec(spec_impl) = &decl.implementation { + if fully_consumed { + // The parameter's entire tuple was consumed, so its destructuring + // binding `let (a,) = ops` is dead; the body's calls were already + // rewritten to the inlined callable. Remove the binding rather than + // rewriting it to a dangling `let () = ops`. + let mut param_vars = FxHashSet::default(); + param_vars.insert(param.param_var); + remove_param_destructuring_stmts(package, &decl.implementation, ¶m_vars); + } else if let CallableImpl::Spec(spec_impl) = &decl.implementation { rewrite_destructuring_pat_in_block( package, spec_impl.body.block, @@ -2739,7 +5067,6 @@ fn extract_stmt(source: &Package, stmt_id: qsc_fir::fir::StmtId, target: &mut Pa } } -#[allow(clippy::too_many_lines)] /// Recursively copies an expression and its transitive references into the /// extraction target. /// @@ -2749,6 +5076,7 @@ fn extract_stmt(source: &Package, stmt_id: qsc_fir::fir::StmtId, target: &mut Pa /// carries a bare `LocalItemId` with no package qualifier, so the lambda it /// references must live in the same package as the closure expression. Named /// nested functions in `StmtKind::Item` are followed for the same reason. +#[allow(clippy::too_many_lines)] fn extract_expr(source: &Package, expr_id: ExprId, target: &mut Package) { if target.exprs.contains_key(expr_id) { return; diff --git a/source/compiler/qsc_fir_transforms/src/defunctionalize/tests.rs b/source/compiler/qsc_fir_transforms/src/defunctionalize/tests.rs index 0668199398e..38783bc7f25 100644 --- a/source/compiler/qsc_fir_transforms/src/defunctionalize/tests.rs +++ b/source/compiler/qsc_fir_transforms/src/defunctionalize/tests.rs @@ -516,22 +516,6 @@ fn error_diagnostic_has_code() { assert_eq!(code.to_string(), "Qsc.Defunctionalize.DynamicCallable"); } -#[test] -fn error_recursive_specialization() { - use miette::Diagnostic; - use qsc_data_structures::span::Span; - - let error = super::Error::RecursiveSpecialization(Span { lo: 42, hi: 50 }); - expect!["specialization leads to infinite recursion"].assert_eq(&error.to_string()); - let code = error - .code() - .expect("RecursiveSpecialization should have a diagnostic code"); - assert_eq!( - code.to_string(), - "Qsc.Defunctionalize.RecursiveSpecialization" - ); -} - #[test] fn empty_entrypoint_remains_unchanged() { let source = "operation Main() : Unit { }"; diff --git a/source/compiler/qsc_fir_transforms/src/defunctionalize/tests/analysis.rs b/source/compiler/qsc_fir_transforms/src/defunctionalize/tests/analysis.rs index 129e1ef3ec4..9cf899f3214 100644 --- a/source/compiler/qsc_fir_transforms/src/defunctionalize/tests/analysis.rs +++ b/source/compiler/qsc_fir_transforms/src/defunctionalize/tests/analysis.rs @@ -155,14 +155,6 @@ fn analysis_multiple_callable_params() { f(q); g(q); } - operation ApplyTwo_AdjCtl__AdjCtl__H_(g : (Qubit => Unit is Adj + Ctl), q : Qubit) : Unit { - H(q); - g(q); - } - operation ApplyTwo_AdjCtl__AdjCtl__X_(g : (Qubit => Unit is Adj + Ctl), q : Qubit) : Unit { - X(q); - g(q); - } operation ApplyTwo_AdjCtl__AdjCtl__H__X_(q : Qubit) : Unit { H(q); X(q); @@ -2028,6 +2020,35 @@ fn reaching_def_mutable_in_loop_dynamic() { ); } +/// A conditional callable whose selecting guard reads a local that is +/// reassigned after the callable is bound must not be lowered to a guarded +/// dispatch: rewrite re-evaluates the guard at the *apply* site, but the guard +/// variable's value there differs from its value at the *binding* site, so the +/// dispatch would silently select the wrong callable. The analysis degrades +/// such a guard to `Dynamic`, surfacing a clear diagnostic instead of emitting +/// incorrect dispatch. +#[test] +fn reaching_def_conditional_callable_reassigned_guard_dynamic() { + check_errors( + r#" + operation ApplyOp(op : Qubit => Unit is Adj, q : Qubit) : Unit is Adj { + op(q); + } + operation Main() : Unit { + use q = Qubit(); + use a = Qubit(); + X(a); + let ra = MResetZ(a); + mutable flag = ra == One; + let op = if flag { X } else { Z }; + set flag = false; + ApplyOp(op, q); + } + "#, + &expect!["callable argument could not be resolved statically"], + ); +} + #[test] fn analysis_closure_through_multiple_levels() { let source = r#" @@ -2820,6 +2841,919 @@ fn analysis_callable_from_constant_callable_array_loop() { ); } +#[test] +fn indexed_closure_callable_array_loop_dispatches_closures() { + let source = r#" + struct Config { + Ops : ((Qubit, Qubit[]) => Unit)[], + Count : Int + } + + operation ApplyParityOperation(value : Int, control : Qubit, register : Qubit[]) : Unit { + if value == 1 { + Controlled X([control], register[0]); + } + } + + operation Run(config : Config) : Unit { + use qs = Qubit[config.Count + 1]; + let controls = qs[0..config.Count - 1]; + let targets = qs[config.Count...]; + for idx in 0..config.Count - 1 { + config.Ops[idx](controls[idx], targets); + } + ResetAll(qs); + } + + operation Main() : Unit { + let ops = [ApplyParityOperation(1, _, _), ApplyParityOperation(2, _, _)]; + Run(new Config { Ops = ops, Count = 2 }); + } + "#; + check_rewrite( + source, + &expect![[r#" + BEFORE: + newtype Config = (((Qubit, Qubit[]) => Unit)[], Int); + operation ApplyParityOperation(value : Int, control : Qubit, register : Qubit[]) : Unit { + if value == 1 { + Controlled X([control], register[0]); + } + + } + operation Run(config : __UDT_Item_1__Package_2_) : Unit { + let qs : Qubit[] = AllocateQubitArray(config::Count + 1); + let controls : Qubit[] = qs[0..config::Count - 1]; + let targets : Qubit[] = qs[config::Count...]; + { + let _range_id_165 : Range = 0..config::Count - 1; + mutable _index_id_168 : Int = _range_id_165::Start; + let _step_id_173 : Int = _range_id_165::Step; + let _end_id_178 : Int = _range_id_165::End; + while _step_id_173 > 0 and _index_id_168 <= _end_id_178 or _step_id_173 < 0 and _index_id_168 >= _end_id_178 { + let idx : Int = _index_id_168; + config::Ops[idx](controls[idx], targets); + _index_id_168 += _step_id_173; + } + + } + + ResetAll(qs); + ReleaseQubitArray(qs); + } + operation Main() : Unit { + let ops : ((Qubit, Qubit[]) => Unit)[] = [{ + let arg : Int = 1; + / * closure item = 5 captures = [arg] * / _lambda_5 + }, { + let arg : Int = 2; + / * closure item = 6 captures = [arg] * / _lambda_6 + }]; + Run(new Config { + Ops = ops, + Count = 2 + }); + } + operation _lambda_5(arg : Int, (hole : Qubit, hole : Qubit[])) : Unit { + ApplyParityOperation(arg, hole, hole) + } + operation _lambda_6(arg : Int, (hole : Qubit, hole : Qubit[])) : Unit { + ApplyParityOperation(arg, hole, hole) + } + // entry + Main() + + AFTER: + newtype Config = (((Qubit, Qubit[]) => Unit)[], Int); + operation ApplyParityOperation(value : Int, control : Qubit, register : Qubit[]) : Unit { + if value == 1 { + Controlled X([control], register[0]); + } + + } + operation Run(config : __UDT_Item_1__Package_2_) : Unit { + let qs : Qubit[] = AllocateQubitArray(config::Count + 1); + let controls : Qubit[] = qs[0..config::Count - 1]; + let targets : Qubit[] = qs[config::Count...]; + { + let _range_id_165 : Range = 0..config::Count - 1; + mutable _index_id_168 : Int = _range_id_165::Start; + let _step_id_173 : Int = _range_id_165::Step; + let _end_id_178 : Int = _range_id_165::End; + while _step_id_173 > 0 and _index_id_168 <= _end_id_178 or _step_id_173 < 0 and _index_id_168 >= _end_id_178 { + let idx : Int = _index_id_168; + config::Ops[idx](controls[idx], targets); + _index_id_168 += _step_id_173; + } + + } + + ResetAll(qs); + ReleaseQubitArray(qs); + } + operation Main() : Unit { + Run_closure__closure_(2, 1, 2); + } + operation _lambda_5(arg : Int, (hole : Qubit, hole : Qubit[])) : Unit { + ApplyParityOperation(arg, hole, hole) + } + operation _lambda_6(arg : Int, (hole : Qubit, hole : Qubit[])) : Unit { + ApplyParityOperation(arg, hole, hole) + } + operation Run_closure__closure_(config : Int, __capture_0 : Int, __capture_1 : Int) : Unit { + let qs : Qubit[] = AllocateQubitArray(config + 1); + let controls : Qubit[] = qs[0..config - 1]; + let targets : Qubit[] = qs[config...]; + { + let _range_id_165 : Range = 0..config - 1; + mutable _index_id_168 : Int = _range_id_165::Start; + let _step_id_173 : Int = _range_id_165::Step; + let _end_id_178 : Int = _range_id_165::End; + while _step_id_173 > 0 and _index_id_168 <= _end_id_178 or _step_id_173 < 0 and _index_id_168 >= _end_id_178 { + let idx : Int = _index_id_168; + if idx == 0 { + _lambda_5(__capture_0, (controls[idx], targets)) + } else { + _lambda_6(__capture_1, (controls[idx], targets)) + }; + _index_id_168 += _step_id_173; + } + + } + + ResetAll(qs); + ReleaseQubitArray(qs); + } + // entry + Main() + "#]], + ); +} + +/// A closure callable-array forwarded through a struct-literal field and fully +/// consumed by an indexed dispatch inside the callee leaves the source-array +/// local dead in the reachable caller. Closure cleanup blanks each element to +/// unit, so the surviving array binding would be an arrow-typed block with a +/// unit tail. The dead binding must be removed before the `PostDefunc` +/// invariant walk observes it; this exercises that walk over the same shape as +/// `indexed_closure_callable_array_loop_dispatches_closures`. +#[test] +fn indexed_closure_callable_array_loop_passes_invariants() { + let source = r#" + struct Config { + Ops : ((Qubit, Qubit[]) => Unit)[], + Count : Int + } + + operation ApplyParityOperation(value : Int, control : Qubit, register : Qubit[]) : Unit { + if value == 1 { + Controlled X([control], register[0]); + } + } + + operation Run(config : Config) : Unit { + use qs = Qubit[config.Count + 1]; + let controls = qs[0..config.Count - 1]; + let targets = qs[config.Count...]; + for idx in 0..config.Count - 1 { + config.Ops[idx](controls[idx], targets); + } + ResetAll(qs); + } + + operation Main() : Unit { + let ops = [ApplyParityOperation(1, _, _), ApplyParityOperation(2, _, _)]; + Run(new Config { Ops = ops, Count = 2 }); + } + "#; + check_invariants(source); + check_pipeline(source); +} + +#[test] +fn indexed_closure_callable_array_tuple_arg_loop_dispatches_closures() { + let source = r#" + operation ApplyParityOperation(value : Int, control : Qubit, register : Qubit[]) : Unit { + if value == 1 { + Controlled X([control], register[0]); + } + } + + operation Run( + statePrep : Qubit[] => Unit, + controlledUnitary : ((Qubit, Qubit[]) => Unit)[], + numBits : Int, + systems : Int[], + phaseQubitPrep : Qubit[] => Unit, + numAncillaQubits : Int + ) : Unit { + use qs = Qubit[numBits + Length(systems) + numAncillaQubits]; + let ancillas = qs[0..numBits - 1]; + let allTargets = qs[numBits...]; + + statePrep(allTargets); + phaseQubitPrep(ancillas); + + for ancillaIdx in 0..numBits - 1 { + controlledUnitary[ancillaIdx](ancillas[ancillaIdx], allTargets); + } + + ResetAll(qs); + } + + operation PrepareSystems(systems : Qubit[]) : Unit { + X(systems[0]); + } + + operation PreparePhase(ancillas : Qubit[]) : Unit { + for q in ancillas { + H(q); + } + } + + operation Main() : Unit { + let controlledUnitary = [ + ApplyParityOperation(1, _, _), + ApplyParityOperation(2, _, _) + ]; + Run(PrepareSystems, controlledUnitary, 2, [0, 1], PreparePhase, 0); + } + "#; + check_rewrite( + source, + &expect![[r#" + BEFORE: + operation ApplyParityOperation(value : Int, control : Qubit, register : Qubit[]) : Unit { + if value == 1 { + Controlled X([control], register[0]); + } + + } + operation Run(statePrep : (Qubit[] => Unit), controlledUnitary : ((Qubit, Qubit[]) => Unit)[], numBits : Int, systems : Int[], phaseQubitPrep : (Qubit[] => Unit), numAncillaQubits : Int) : Unit { + let qs : Qubit[] = AllocateQubitArray(numBits + Length(systems) + numAncillaQubits); + let ancillas : Qubit[] = qs[0..numBits - 1]; + let allTargets : Qubit[] = qs[numBits...]; + statePrep(allTargets); + phaseQubitPrep(ancillas); + { + let _range_id_214 : Range = 0..numBits - 1; + mutable _index_id_217 : Int = _range_id_214::Start; + let _step_id_222 : Int = _range_id_214::Step; + let _end_id_227 : Int = _range_id_214::End; + while _step_id_222 > 0 and _index_id_217 <= _end_id_227 or _step_id_222 < 0 and _index_id_217 >= _end_id_227 { + let ancillaIdx : Int = _index_id_217; + controlledUnitary[ancillaIdx](ancillas[ancillaIdx], allTargets); + _index_id_217 += _step_id_222; + } + + } + + ResetAll(qs); + ReleaseQubitArray(qs); + } + operation PrepareSystems(systems : Qubit[]) : Unit { + X(systems[0]); + } + operation PreparePhase(ancillas : Qubit[]) : Unit { + { + let _array_id_257 : Qubit[] = ancillas; + let _len_id_261 : Int = Length(_array_id_257); + mutable _index_id_266 : Int = 0; + while _index_id_266 < _len_id_261 { + let q : Qubit = _array_id_257[_index_id_266]; + H(q); + _index_id_266 += 1; + } + + } + + } + operation Main() : Unit { + let controlledUnitary : ((Qubit, Qubit[]) => Unit)[] = [{ + let arg : Int = 1; + / * closure item = 6 captures = [arg] * / _lambda_6 + }, { + let arg : Int = 2; + / * closure item = 7 captures = [arg] * / _lambda_7 + }]; + Run_Empty__Empty__Empty_(PrepareSystems, controlledUnitary, 2, [0, 1], PreparePhase, 0); + } + operation _lambda_6(arg : Int, (hole : Qubit, hole : Qubit[])) : Unit { + ApplyParityOperation(arg, hole, hole) + } + operation _lambda_7(arg : Int, (hole : Qubit, hole : Qubit[])) : Unit { + ApplyParityOperation(arg, hole, hole) + } + operation Run_Empty__Empty__Empty_(statePrep : (Qubit[] => Unit), controlledUnitary : ((Qubit, Qubit[]) => Unit)[], numBits : Int, systems : Int[], phaseQubitPrep : (Qubit[] => Unit), numAncillaQubits : Int) : Unit { + let qs : Qubit[] = AllocateQubitArray(numBits + Length(systems) + numAncillaQubits); + let ancillas : Qubit[] = qs[0..numBits - 1]; + let allTargets : Qubit[] = qs[numBits...]; + statePrep(allTargets); + phaseQubitPrep(ancillas); + { + let _range_id_214 : Range = 0..numBits - 1; + mutable _index_id_217 : Int = _range_id_214::Start; + let _step_id_222 : Int = _range_id_214::Step; + let _end_id_227 : Int = _range_id_214::End; + while _step_id_222 > 0 and _index_id_217 <= _end_id_227 or _step_id_222 < 0 and _index_id_217 >= _end_id_227 { + let ancillaIdx : Int = _index_id_217; + controlledUnitary[ancillaIdx](ancillas[ancillaIdx], allTargets); + _index_id_217 += _step_id_222; + } + + } + + ResetAll(qs); + ReleaseQubitArray(qs); + } + // entry + Main() + + AFTER: + operation ApplyParityOperation(value : Int, control : Qubit, register : Qubit[]) : Unit { + if value == 1 { + Controlled X([control], register[0]); + } + + } + operation Run(statePrep : (Qubit[] => Unit), controlledUnitary : ((Qubit, Qubit[]) => Unit)[], numBits : Int, systems : Int[], phaseQubitPrep : (Qubit[] => Unit), numAncillaQubits : Int) : Unit { + let qs : Qubit[] = AllocateQubitArray(numBits + Length(systems) + numAncillaQubits); + let ancillas : Qubit[] = qs[0..numBits - 1]; + let allTargets : Qubit[] = qs[numBits...]; + statePrep(allTargets); + phaseQubitPrep(ancillas); + { + let _range_id_214 : Range = 0..numBits - 1; + mutable _index_id_217 : Int = _range_id_214::Start; + let _step_id_222 : Int = _range_id_214::Step; + let _end_id_227 : Int = _range_id_214::End; + while _step_id_222 > 0 and _index_id_217 <= _end_id_227 or _step_id_222 < 0 and _index_id_217 >= _end_id_227 { + let ancillaIdx : Int = _index_id_217; + controlledUnitary[ancillaIdx](ancillas[ancillaIdx], allTargets); + _index_id_217 += _step_id_222; + } + + } + + ResetAll(qs); + ReleaseQubitArray(qs); + } + operation PrepareSystems(systems : Qubit[]) : Unit { + X(systems[0]); + } + operation PreparePhase(ancillas : Qubit[]) : Unit { + { + let _array_id_257 : Qubit[] = ancillas; + let _len_id_261 : Int = Length(_array_id_257); + mutable _index_id_266 : Int = 0; + while _index_id_266 < _len_id_261 { + let q : Qubit = _array_id_257[_index_id_266]; + H(q); + _index_id_266 += 1; + } + + } + + } + operation Main() : Unit { + Run_Empty__Empty__Empty__PrepareSystems__closure__closure__PreparePhase_(2, [0, 1], 0, 1, 2); + } + operation _lambda_6(arg : Int, (hole : Qubit, hole : Qubit[])) : Unit { + ApplyParityOperation(arg, hole, hole) + } + operation _lambda_7(arg : Int, (hole : Qubit, hole : Qubit[])) : Unit { + ApplyParityOperation(arg, hole, hole) + } + operation Run_Empty__Empty__Empty_(statePrep : (Qubit[] => Unit), controlledUnitary : ((Qubit, Qubit[]) => Unit)[], numBits : Int, systems : Int[], phaseQubitPrep : (Qubit[] => Unit), numAncillaQubits : Int) : Unit { + let qs : Qubit[] = AllocateQubitArray(numBits + Length(systems) + numAncillaQubits); + let ancillas : Qubit[] = qs[0..numBits - 1]; + let allTargets : Qubit[] = qs[numBits...]; + statePrep(allTargets); + phaseQubitPrep(ancillas); + { + let _range_id_214 : Range = 0..numBits - 1; + mutable _index_id_217 : Int = _range_id_214::Start; + let _step_id_222 : Int = _range_id_214::Step; + let _end_id_227 : Int = _range_id_214::End; + while _step_id_222 > 0 and _index_id_217 <= _end_id_227 or _step_id_222 < 0 and _index_id_217 >= _end_id_227 { + let ancillaIdx : Int = _index_id_217; + controlledUnitary[ancillaIdx](ancillas[ancillaIdx], allTargets); + _index_id_217 += _step_id_222; + } + + } + + ResetAll(qs); + ReleaseQubitArray(qs); + } + operation Run_Empty__Empty__Empty__PrepareSystems__closure__closure__PreparePhase_(numBits : Int, systems : Int[], numAncillaQubits : Int, __capture_0 : Int, __capture_1 : Int) : Unit { + let qs : Qubit[] = AllocateQubitArray(numBits + Length(systems) + numAncillaQubits); + let ancillas : Qubit[] = qs[0..numBits - 1]; + let allTargets : Qubit[] = qs[numBits...]; + PrepareSystems(allTargets); + PreparePhase(ancillas); + { + let _range_id_214 : Range = 0..numBits - 1; + mutable _index_id_217 : Int = _range_id_214::Start; + let _step_id_222 : Int = _range_id_214::Step; + let _end_id_227 : Int = _range_id_214::End; + while _step_id_222 > 0 and _index_id_217 <= _end_id_227 or _step_id_222 < 0 and _index_id_217 >= _end_id_227 { + let ancillaIdx : Int = _index_id_217; + if ancillaIdx == 0 { + _lambda_6(__capture_0, (ancillas[ancillaIdx], allTargets)) + } else { + _lambda_7(__capture_1, (ancillas[ancillaIdx], allTargets)) + }; + _index_id_217 += _step_id_222; + } + + } + + ResetAll(qs); + ReleaseQubitArray(qs); + } + // entry + Main() + "#]], + ); +} + +#[test] +fn indexed_same_target_closure_callable_array_tuple_arg_dispatches_closures() { + let source = r#" + operation ApplyParityOperation(value : Int, control : Qubit, register : Qubit[]) : Unit { + if value == 1 { + Controlled X([control], register[0]); + } + } + + operation Run( + statePrep : Qubit[] => Unit, + controlledUnitary : ((Qubit, Qubit[]) => Unit)[], + numBits : Int, + systems : Int[], + phaseQubitPrep : Qubit[] => Unit, + numAncillaQubits : Int + ) : Unit { + use qs = Qubit[numBits + Length(systems) + numAncillaQubits]; + let ancillas = qs[0..numBits - 1]; + let allTargets = qs[numBits...]; + + statePrep(allTargets); + phaseQubitPrep(ancillas); + + for ancillaIdx in 0..numBits - 1 { + controlledUnitary[ancillaIdx](ancillas[ancillaIdx], allTargets); + } + + ResetAll(qs); + } + + operation PrepareSystems(systems : Qubit[]) : Unit { + X(systems[0]); + } + + operation PreparePhase(ancillas : Qubit[]) : Unit { + for q in ancillas { + H(q); + } + } + + operation Main() : Unit { + let first = 1; + let second = 2; + let controlledUnitary = [ + ApplyParityOperation(first, _, _), + ApplyParityOperation(second, _, _) + ]; + Run(PrepareSystems, controlledUnitary, 2, [0, 1], PreparePhase, 0); + } + "#; + check_rewrite( + source, + &expect![[r#" + BEFORE: + operation ApplyParityOperation(value : Int, control : Qubit, register : Qubit[]) : Unit { + if value == 1 { + Controlled X([control], register[0]); + } + + } + operation Run(statePrep : (Qubit[] => Unit), controlledUnitary : ((Qubit, Qubit[]) => Unit)[], numBits : Int, systems : Int[], phaseQubitPrep : (Qubit[] => Unit), numAncillaQubits : Int) : Unit { + let qs : Qubit[] = AllocateQubitArray(numBits + Length(systems) + numAncillaQubits); + let ancillas : Qubit[] = qs[0..numBits - 1]; + let allTargets : Qubit[] = qs[numBits...]; + statePrep(allTargets); + phaseQubitPrep(ancillas); + { + let _range_id_222 : Range = 0..numBits - 1; + mutable _index_id_225 : Int = _range_id_222::Start; + let _step_id_230 : Int = _range_id_222::Step; + let _end_id_235 : Int = _range_id_222::End; + while _step_id_230 > 0 and _index_id_225 <= _end_id_235 or _step_id_230 < 0 and _index_id_225 >= _end_id_235 { + let ancillaIdx : Int = _index_id_225; + controlledUnitary[ancillaIdx](ancillas[ancillaIdx], allTargets); + _index_id_225 += _step_id_230; + } + + } + + ResetAll(qs); + ReleaseQubitArray(qs); + } + operation PrepareSystems(systems : Qubit[]) : Unit { + X(systems[0]); + } + operation PreparePhase(ancillas : Qubit[]) : Unit { + { + let _array_id_265 : Qubit[] = ancillas; + let _len_id_269 : Int = Length(_array_id_265); + mutable _index_id_274 : Int = 0; + while _index_id_274 < _len_id_269 { + let q : Qubit = _array_id_265[_index_id_274]; + H(q); + _index_id_274 += 1; + } + + } + + } + operation Main() : Unit { + let first : Int = 1; + let second : Int = 2; + let controlledUnitary : ((Qubit, Qubit[]) => Unit)[] = [{ + let arg : Int = first; + / * closure item = 6 captures = [arg] * / _lambda_6 + }, { + let arg : Int = second; + / * closure item = 7 captures = [arg] * / _lambda_7 + }]; + Run_Empty__Empty__Empty_(PrepareSystems, controlledUnitary, 2, [0, 1], PreparePhase, 0); + } + operation _lambda_6(arg : Int, (hole : Qubit, hole : Qubit[])) : Unit { + ApplyParityOperation(arg, hole, hole) + } + operation _lambda_7(arg : Int, (hole : Qubit, hole : Qubit[])) : Unit { + ApplyParityOperation(arg, hole, hole) + } + operation Run_Empty__Empty__Empty_(statePrep : (Qubit[] => Unit), controlledUnitary : ((Qubit, Qubit[]) => Unit)[], numBits : Int, systems : Int[], phaseQubitPrep : (Qubit[] => Unit), numAncillaQubits : Int) : Unit { + let qs : Qubit[] = AllocateQubitArray(numBits + Length(systems) + numAncillaQubits); + let ancillas : Qubit[] = qs[0..numBits - 1]; + let allTargets : Qubit[] = qs[numBits...]; + statePrep(allTargets); + phaseQubitPrep(ancillas); + { + let _range_id_222 : Range = 0..numBits - 1; + mutable _index_id_225 : Int = _range_id_222::Start; + let _step_id_230 : Int = _range_id_222::Step; + let _end_id_235 : Int = _range_id_222::End; + while _step_id_230 > 0 and _index_id_225 <= _end_id_235 or _step_id_230 < 0 and _index_id_225 >= _end_id_235 { + let ancillaIdx : Int = _index_id_225; + controlledUnitary[ancillaIdx](ancillas[ancillaIdx], allTargets); + _index_id_225 += _step_id_230; + } + + } + + ResetAll(qs); + ReleaseQubitArray(qs); + } + // entry + Main() + + AFTER: + operation ApplyParityOperation(value : Int, control : Qubit, register : Qubit[]) : Unit { + if value == 1 { + Controlled X([control], register[0]); + } + + } + operation Run(statePrep : (Qubit[] => Unit), controlledUnitary : ((Qubit, Qubit[]) => Unit)[], numBits : Int, systems : Int[], phaseQubitPrep : (Qubit[] => Unit), numAncillaQubits : Int) : Unit { + let qs : Qubit[] = AllocateQubitArray(numBits + Length(systems) + numAncillaQubits); + let ancillas : Qubit[] = qs[0..numBits - 1]; + let allTargets : Qubit[] = qs[numBits...]; + statePrep(allTargets); + phaseQubitPrep(ancillas); + { + let _range_id_222 : Range = 0..numBits - 1; + mutable _index_id_225 : Int = _range_id_222::Start; + let _step_id_230 : Int = _range_id_222::Step; + let _end_id_235 : Int = _range_id_222::End; + while _step_id_230 > 0 and _index_id_225 <= _end_id_235 or _step_id_230 < 0 and _index_id_225 >= _end_id_235 { + let ancillaIdx : Int = _index_id_225; + controlledUnitary[ancillaIdx](ancillas[ancillaIdx], allTargets); + _index_id_225 += _step_id_230; + } + + } + + ResetAll(qs); + ReleaseQubitArray(qs); + } + operation PrepareSystems(systems : Qubit[]) : Unit { + X(systems[0]); + } + operation PreparePhase(ancillas : Qubit[]) : Unit { + { + let _array_id_265 : Qubit[] = ancillas; + let _len_id_269 : Int = Length(_array_id_265); + mutable _index_id_274 : Int = 0; + while _index_id_274 < _len_id_269 { + let q : Qubit = _array_id_265[_index_id_274]; + H(q); + _index_id_274 += 1; + } + + } + + } + operation Main() : Unit { + let first : Int = 1; + let second : Int = 2; + Run_Empty__Empty__Empty__PrepareSystems__closure__closure__PreparePhase_(2, [0, 1], 0, first, second); + } + operation _lambda_6(arg : Int, (hole : Qubit, hole : Qubit[])) : Unit { + ApplyParityOperation(arg, hole, hole) + } + operation _lambda_7(arg : Int, (hole : Qubit, hole : Qubit[])) : Unit { + ApplyParityOperation(arg, hole, hole) + } + operation Run_Empty__Empty__Empty_(statePrep : (Qubit[] => Unit), controlledUnitary : ((Qubit, Qubit[]) => Unit)[], numBits : Int, systems : Int[], phaseQubitPrep : (Qubit[] => Unit), numAncillaQubits : Int) : Unit { + let qs : Qubit[] = AllocateQubitArray(numBits + Length(systems) + numAncillaQubits); + let ancillas : Qubit[] = qs[0..numBits - 1]; + let allTargets : Qubit[] = qs[numBits...]; + statePrep(allTargets); + phaseQubitPrep(ancillas); + { + let _range_id_222 : Range = 0..numBits - 1; + mutable _index_id_225 : Int = _range_id_222::Start; + let _step_id_230 : Int = _range_id_222::Step; + let _end_id_235 : Int = _range_id_222::End; + while _step_id_230 > 0 and _index_id_225 <= _end_id_235 or _step_id_230 < 0 and _index_id_225 >= _end_id_235 { + let ancillaIdx : Int = _index_id_225; + controlledUnitary[ancillaIdx](ancillas[ancillaIdx], allTargets); + _index_id_225 += _step_id_230; + } + + } + + ResetAll(qs); + ReleaseQubitArray(qs); + } + operation Run_Empty__Empty__Empty__PrepareSystems__closure__closure__PreparePhase_(numBits : Int, systems : Int[], numAncillaQubits : Int, __capture_0 : Int, __capture_1 : Int) : Unit { + let qs : Qubit[] = AllocateQubitArray(numBits + Length(systems) + numAncillaQubits); + let ancillas : Qubit[] = qs[0..numBits - 1]; + let allTargets : Qubit[] = qs[numBits...]; + PrepareSystems(allTargets); + PreparePhase(ancillas); + { + let _range_id_222 : Range = 0..numBits - 1; + mutable _index_id_225 : Int = _range_id_222::Start; + let _step_id_230 : Int = _range_id_222::Step; + let _end_id_235 : Int = _range_id_222::End; + while _step_id_230 > 0 and _index_id_225 <= _end_id_235 or _step_id_230 < 0 and _index_id_225 >= _end_id_235 { + let ancillaIdx : Int = _index_id_225; + if ancillaIdx == 0 { + _lambda_6(__capture_0, (ancillas[ancillaIdx], allTargets)) + } else { + _lambda_7(__capture_1, (ancillas[ancillaIdx], allTargets)) + }; + _index_id_225 += _step_id_230; + } + + } + + ResetAll(qs); + ReleaseQubitArray(qs); + } + // entry + Main() + "#]], + ); +} + +#[test] +fn indexed_closure_callable_array_udt_with_callable_siblings_dispatches_closures() { + let source = r#" + struct Config { + StatePrep : Qubit[] => Unit, + ControlledUnitary : ((Qubit, Qubit[]) => Unit)[], + PhaseQubitPrep : Qubit[] => Unit, + NumBits : Int, + Systems : Int[], + NumAncillaQubits : Int + } + + operation ApplyParityOperation(value : Int, control : Qubit, register : Qubit[]) : Unit { + if value == 1 { + Controlled X([control], register[0]); + } + } + + operation Run(config : Config) : Unit { + use qs = Qubit[config.NumBits + Length(config.Systems) + config.NumAncillaQubits]; + let ancillas = qs[0..config.NumBits - 1]; + let allTargets = qs[config.NumBits...]; + + config.StatePrep(allTargets); + config.PhaseQubitPrep(ancillas); + + for ancillaIdx in 0..config.NumBits - 1 { + config.ControlledUnitary[ancillaIdx](ancillas[ancillaIdx], allTargets); + } + + ResetAll(qs); + } + + operation PrepareSystems(systems : Qubit[]) : Unit { + X(systems[0]); + } + + operation PreparePhase(ancillas : Qubit[]) : Unit { + for q in ancillas { + H(q); + } + } + + operation Main() : Unit { + let controlledUnitary = [ + ApplyParityOperation(1, _, _), + ApplyParityOperation(2, _, _) + ]; + Run(new Config { + StatePrep = PrepareSystems, + ControlledUnitary = controlledUnitary, + PhaseQubitPrep = PreparePhase, + NumBits = 2, + Systems = [0, 1], + NumAncillaQubits = 0 + }); + } + "#; + check_rewrite( + source, + &expect![[r#" + BEFORE: + newtype Config = ((Qubit[] => Unit), ((Qubit, Qubit[]) => Unit)[], (Qubit[] => Unit), Int, Int[], Int); + operation ApplyParityOperation(value : Int, control : Qubit, register : Qubit[]) : Unit { + if value == 1 { + Controlled X([control], register[0]); + } + + } + operation Run(config : __UDT_Item_1__Package_2_) : Unit { + let qs : Qubit[] = AllocateQubitArray(config::NumBits + Length(config::Systems) + config::NumAncillaQubits); + let ancillas : Qubit[] = qs[0..config::NumBits - 1]; + let allTargets : Qubit[] = qs[config::NumBits...]; + config::StatePrep(allTargets); + config::PhaseQubitPrep(ancillas); + { + let _range_id_219 : Range = 0..config::NumBits - 1; + mutable _index_id_222 : Int = _range_id_219::Start; + let _step_id_227 : Int = _range_id_219::Step; + let _end_id_232 : Int = _range_id_219::End; + while _step_id_227 > 0 and _index_id_222 <= _end_id_232 or _step_id_227 < 0 and _index_id_222 >= _end_id_232 { + let ancillaIdx : Int = _index_id_222; + config::ControlledUnitary[ancillaIdx](ancillas[ancillaIdx], allTargets); + _index_id_222 += _step_id_227; + } + + } + + ResetAll(qs); + ReleaseQubitArray(qs); + } + operation PrepareSystems(systems : Qubit[]) : Unit { + X(systems[0]); + } + operation PreparePhase(ancillas : Qubit[]) : Unit { + { + let _array_id_262 : Qubit[] = ancillas; + let _len_id_266 : Int = Length(_array_id_262); + mutable _index_id_271 : Int = 0; + while _index_id_271 < _len_id_266 { + let q : Qubit = _array_id_262[_index_id_271]; + H(q); + _index_id_271 += 1; + } + + } + + } + operation Main() : Unit { + let controlledUnitary : ((Qubit, Qubit[]) => Unit)[] = [{ + let arg : Int = 1; + / * closure item = 7 captures = [arg] * / _lambda_7 + }, { + let arg : Int = 2; + / * closure item = 8 captures = [arg] * / _lambda_8 + }]; + Run(new Config { + StatePrep = PrepareSystems, + ControlledUnitary = controlledUnitary, + PhaseQubitPrep = PreparePhase, + NumBits = 2, + Systems = [0, 1], + NumAncillaQubits = 0 + }); + } + operation _lambda_7(arg : Int, (hole : Qubit, hole : Qubit[])) : Unit { + ApplyParityOperation(arg, hole, hole) + } + operation _lambda_8(arg : Int, (hole : Qubit, hole : Qubit[])) : Unit { + ApplyParityOperation(arg, hole, hole) + } + // entry + Main() + + AFTER: + newtype Config = ((Qubit[] => Unit), ((Qubit, Qubit[]) => Unit)[], (Qubit[] => Unit), Int, Int[], Int); + operation ApplyParityOperation(value : Int, control : Qubit, register : Qubit[]) : Unit { + if value == 1 { + Controlled X([control], register[0]); + } + + } + operation Run(config : __UDT_Item_1__Package_2_) : Unit { + let qs : Qubit[] = AllocateQubitArray(config::NumBits + Length(config::Systems) + config::NumAncillaQubits); + let ancillas : Qubit[] = qs[0..config::NumBits - 1]; + let allTargets : Qubit[] = qs[config::NumBits...]; + config::StatePrep(allTargets); + config::PhaseQubitPrep(ancillas); + { + let _range_id_219 : Range = 0..config::NumBits - 1; + mutable _index_id_222 : Int = _range_id_219::Start; + let _step_id_227 : Int = _range_id_219::Step; + let _end_id_232 : Int = _range_id_219::End; + while _step_id_227 > 0 and _index_id_222 <= _end_id_232 or _step_id_227 < 0 and _index_id_222 >= _end_id_232 { + let ancillaIdx : Int = _index_id_222; + config::ControlledUnitary[ancillaIdx](ancillas[ancillaIdx], allTargets); + _index_id_222 += _step_id_227; + } + + } + + ResetAll(qs); + ReleaseQubitArray(qs); + } + operation PrepareSystems(systems : Qubit[]) : Unit { + X(systems[0]); + } + operation PreparePhase(ancillas : Qubit[]) : Unit { + { + let _array_id_262 : Qubit[] = ancillas; + let _len_id_266 : Int = Length(_array_id_262); + mutable _index_id_271 : Int = 0; + while _index_id_271 < _len_id_266 { + let q : Qubit = _array_id_262[_index_id_271]; + H(q); + _index_id_271 += 1; + } + + } + + } + operation Main() : Unit { + Run_PrepareSystems__closure__closure__PreparePhase_(2, [0, 1], 0, 1, 2); + } + operation _lambda_7(arg : Int, (hole : Qubit, hole : Qubit[])) : Unit { + ApplyParityOperation(arg, hole, hole) + } + operation _lambda_8(arg : Int, (hole : Qubit, hole : Qubit[])) : Unit { + ApplyParityOperation(arg, hole, hole) + } + operation Run_PrepareSystems__closure__closure__PreparePhase_(config : (Int, Int[], Int), __capture_0 : Int, __capture_1 : Int) : Unit { + let qs : Qubit[] = AllocateQubitArray(config::StatePrep + Length(config::ControlledUnitary) + config::PhaseQubitPrep); + let ancillas : Qubit[] = qs[0..config::StatePrep - 1]; + let allTargets : Qubit[] = qs[config::StatePrep...]; + PrepareSystems(allTargets); + PreparePhase(ancillas); + { + let _range_id_219 : Range = 0..config::StatePrep - 1; + mutable _index_id_222 : Int = _range_id_219::Start; + let _step_id_227 : Int = _range_id_219::Step; + let _end_id_232 : Int = _range_id_219::End; + while _step_id_227 > 0 and _index_id_222 <= _end_id_232 or _step_id_227 < 0 and _index_id_222 >= _end_id_232 { + let ancillaIdx : Int = _index_id_222; + if ancillaIdx == 0 { + _lambda_7(__capture_0, (ancillas[ancillaIdx], allTargets)) + } else { + _lambda_8(__capture_1, (ancillas[ancillaIdx], allTargets)) + }; + _index_id_222 += _step_id_227; + } + + } + + ResetAll(qs); + ReleaseQubitArray(qs); + } + // entry + Main() + "#]], + ); +} + #[test] fn analysis_callable_returning_partial_application_from_function_in_loop() { let source = r#" diff --git a/source/compiler/qsc_fir_transforms/src/defunctionalize/tests/cross_package.rs b/source/compiler/qsc_fir_transforms/src/defunctionalize/tests/cross_package.rs index 6c49b4c40be..dbe12c04975 100644 --- a/source/compiler/qsc_fir_transforms/src/defunctionalize/tests/cross_package.rs +++ b/source/compiler/qsc_fir_transforms/src/defunctionalize/tests/cross_package.rs @@ -35,6 +35,9 @@ fn closure_spec_key_distinguishes_packages() { package: PackageId::from(0usize), item: LocalItemId::from(7usize), }, + top_level_param: 0, + field_path: vec![], + hof_input_is_tuple: false, callable_arg: ConcreteCallable::Closure { target: LocalItemId::from(0usize), captures: vec![], @@ -595,33 +598,10 @@ fn analysis_apply_operation_power_ca_consumer() { } operation Main() : Unit { let qs : Qubit[] = AllocateQubitArray(1); - Consume_AdjCtl__closure__U_(qs); + Consume_AdjCtl__closure_(qs); ReleaseQubitArray(qs); } - operation _lambda_4(arg : (Qubit[] => Unit is Adj + Ctl), (hole : Int, hole : Qubit[])) : Unit is Adj + Ctl { - body ... { - ApplyOperationPowerCA__Qubit_____AdjCtl_(hole, arg, hole) - } - adjoint ... { - Adjoint ApplyOperationPowerCA__Qubit_____AdjCtl_(hole, arg, hole) - } - controlled (ctls, ...) { - Controlled ApplyOperationPowerCA__Qubit_____AdjCtl_(ctls, (hole, arg, hole)) - } - controlled adjoint (ctls, ...) { - Controlled Adjoint ApplyOperationPowerCA__Qubit_____AdjCtl_(ctls, (hole, arg, hole)) - } - } - operation Consume_AdjCtl_(apply_power_of_u : ((Int, Qubit[]) => Unit is Adj + Ctl), target : Qubit[]) : Unit { - apply_power_of_u(1, target); - } - operation Consume_AdjCtl__closure_(target : Qubit[], __capture_0 : (Qubit[] => Unit is Adj + Ctl)) : Unit { - _lambda_4(__capture_0, (1, target)); - } - operation Consume_AdjCtl__closure__U_(target : Qubit[]) : Unit { - _lambda_4_U_(1, target); - } - operation _lambda_4_U_(hole : Int, hole : Qubit[]) : Unit is Adj + Ctl { + operation _lambda_4(hole : Int, hole : Qubit[]) : Unit is Adj + Ctl { body ... { ApplyOperationPowerCA__Qubit_____AdjCtl__U_(hole, hole) } @@ -635,21 +615,27 @@ fn analysis_apply_operation_power_ca_consumer() { Controlled Adjoint ApplyOperationPowerCA__Qubit_____AdjCtl__U_(ctls, (hole, hole)) } } + operation Consume_AdjCtl_(apply_power_of_u : ((Int, Qubit[]) => Unit is Adj + Ctl), target : Qubit[]) : Unit { + apply_power_of_u(1, target); + } + operation Consume_AdjCtl__closure_(target : Qubit[]) : Unit { + _lambda_4(1, target); + } operation ApplyOperationPowerCA__Qubit_____AdjCtl__U_(power : Int, target : Qubit[]) : Unit is Adj + Ctl { body ... { { - let _range_id_48030 : Range = 1..AbsI(power); - mutable _index_id_48033 : Int = _range_id_48030::Start; - let _step_id_48038 : Int = _range_id_48030::Step; - let _end_id_48043 : Int = _range_id_48030::End; - while _step_id_48038 > 0 and _index_id_48033 <= _end_id_48043 or _step_id_48038 < 0 and _index_id_48033 >= _end_id_48043 { - let _ : Int = _index_id_48033; + let _range_id_48101 : Range = 1..AbsI(power); + mutable _index_id_48104 : Int = _range_id_48101::Start; + let _step_id_48109 : Int = _range_id_48101::Step; + let _end_id_48114 : Int = _range_id_48101::End; + while _step_id_48109 > 0 and _index_id_48104 <= _end_id_48114 or _step_id_48109 < 0 and _index_id_48104 >= _end_id_48114 { + let _ : Int = _index_id_48104; if power >= 0 { U(target) } else { Adjoint U(target) }; - _index_id_48033 += _step_id_48038; + _index_id_48104 += _step_id_48109; } } @@ -659,18 +645,18 @@ fn analysis_apply_operation_power_ca_consumer() { { let _range : Range = 1..AbsI(power); { - let _range_id_48073 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; - mutable _index_id_48076 : Int = _range_id_48073::Start; - let _step_id_48081 : Int = _range_id_48073::Step; - let _end_id_48086 : Int = _range_id_48073::End; - while _step_id_48081 > 0 and _index_id_48076 <= _end_id_48086 or _step_id_48081 < 0 and _index_id_48076 >= _end_id_48086 { - let _ : Int = _index_id_48076; + let _range_id_48144 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; + mutable _index_id_48147 : Int = _range_id_48144::Start; + let _step_id_48152 : Int = _range_id_48144::Step; + let _end_id_48157 : Int = _range_id_48144::End; + while _step_id_48152 > 0 and _index_id_48147 <= _end_id_48157 or _step_id_48152 < 0 and _index_id_48147 >= _end_id_48157 { + let _ : Int = _index_id_48147; if power >= 0 { Adjoint U(target) } else { U(target) }; - _index_id_48076 += _step_id_48081; + _index_id_48147 += _step_id_48152; } } @@ -680,18 +666,18 @@ fn analysis_apply_operation_power_ca_consumer() { } controlled (ctls, ...) { { - let _range_id_48116 : Range = 1..AbsI(power); - mutable _index_id_48119 : Int = _range_id_48116::Start; - let _step_id_48124 : Int = _range_id_48116::Step; - let _end_id_48129 : Int = _range_id_48116::End; - while _step_id_48124 > 0 and _index_id_48119 <= _end_id_48129 or _step_id_48124 < 0 and _index_id_48119 >= _end_id_48129 { - let _ : Int = _index_id_48119; + let _range_id_48187 : Range = 1..AbsI(power); + mutable _index_id_48190 : Int = _range_id_48187::Start; + let _step_id_48195 : Int = _range_id_48187::Step; + let _end_id_48200 : Int = _range_id_48187::End; + while _step_id_48195 > 0 and _index_id_48190 <= _end_id_48200 or _step_id_48195 < 0 and _index_id_48190 >= _end_id_48200 { + let _ : Int = _index_id_48190; if power >= 0 { Controlled U(ctls, target) } else { Controlled Adjoint U(ctls, target) }; - _index_id_48119 += _step_id_48124; + _index_id_48190 += _step_id_48195; } } @@ -701,18 +687,18 @@ fn analysis_apply_operation_power_ca_consumer() { { let _range : Range = 1..AbsI(power); { - let _range_id_48159 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; - mutable _index_id_48162 : Int = _range_id_48159::Start; - let _step_id_48167 : Int = _range_id_48159::Step; - let _end_id_48172 : Int = _range_id_48159::End; - while _step_id_48167 > 0 and _index_id_48162 <= _end_id_48172 or _step_id_48167 < 0 and _index_id_48162 >= _end_id_48172 { - let _ : Int = _index_id_48162; + let _range_id_48230 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; + mutable _index_id_48233 : Int = _range_id_48230::Start; + let _step_id_48238 : Int = _range_id_48230::Step; + let _end_id_48243 : Int = _range_id_48230::End; + while _step_id_48238 > 0 and _index_id_48233 <= _end_id_48243 or _step_id_48238 < 0 and _index_id_48233 >= _end_id_48243 { + let _ : Int = _index_id_48233; if power >= 0 { Controlled Adjoint U(ctls, target) } else { Controlled U(ctls, target) }; - _index_id_48162 += _step_id_48167; + _index_id_48233 += _step_id_48238; } } @@ -1030,13 +1016,13 @@ fn analysis_bernstein_vazirani_sample_shape() { operation ApplyToEachA_Qubit__AdjCtl__H_(register : Qubit[]) : Unit is Adj { body ... { { - let _array_id_46247 : Qubit[] = register; - let _len_id_46251 : Int = Length(_array_id_46247); - mutable _index_id_46256 : Int = 0; - while _index_id_46256 < _len_id_46251 { - let item : Qubit = _array_id_46247[_index_id_46256]; + let _array_id_46318 : Qubit[] = register; + let _len_id_46322 : Int = Length(_array_id_46318); + mutable _index_id_46327 : Int = 0; + while _index_id_46327 < _len_id_46322 { + let item : Qubit = _array_id_46318[_index_id_46327]; H(item); - _index_id_46256 += 1; + _index_id_46327 += 1; } } @@ -1046,15 +1032,15 @@ fn analysis_bernstein_vazirani_sample_shape() { { let _array : Qubit[] = register; { - let _range_id_46275 : Range = Length(_array) - 1..-1..0; - mutable _index_id_46278 : Int = _range_id_46275::Start; - let _step_id_46283 : Int = _range_id_46275::Step; - let _end_id_46288 : Int = _range_id_46275::End; - while _step_id_46283 > 0 and _index_id_46278 <= _end_id_46288 or _step_id_46283 < 0 and _index_id_46278 >= _end_id_46288 { - let _index : Int = _index_id_46278; + let _range_id_46346 : Range = Length(_array) - 1..-1..0; + mutable _index_id_46349 : Int = _range_id_46346::Start; + let _step_id_46354 : Int = _range_id_46346::Step; + let _end_id_46359 : Int = _range_id_46346::End; + while _step_id_46354 > 0 and _index_id_46349 <= _end_id_46359 or _step_id_46354 < 0 and _index_id_46349 >= _end_id_46359 { + let _index : Int = _index_id_46349; let item : Qubit = _array[_index]; Adjoint H(item); - _index_id_46278 += _step_id_46283; + _index_id_46349 += _step_id_46354; } } @@ -1066,13 +1052,13 @@ fn analysis_bernstein_vazirani_sample_shape() { operation ApplyToEachA_Qubit__AdjCtl__H_(register : Qubit[]) : Unit is Adj { body ... { { - let _array_id_46247 : Qubit[] = register; - let _len_id_46251 : Int = Length(_array_id_46247); - mutable _index_id_46256 : Int = 0; - while _index_id_46256 < _len_id_46251 { - let item : Qubit = _array_id_46247[_index_id_46256]; + let _array_id_46318 : Qubit[] = register; + let _len_id_46322 : Int = Length(_array_id_46318); + mutable _index_id_46327 : Int = 0; + while _index_id_46327 < _len_id_46322 { + let item : Qubit = _array_id_46318[_index_id_46327]; H(item); - _index_id_46256 += 1; + _index_id_46327 += 1; } } @@ -1082,15 +1068,15 @@ fn analysis_bernstein_vazirani_sample_shape() { { let _array : Qubit[] = register; { - let _range_id_46275 : Range = Length(_array) - 1..-1..0; - mutable _index_id_46278 : Int = _range_id_46275::Start; - let _step_id_46283 : Int = _range_id_46275::Step; - let _end_id_46288 : Int = _range_id_46275::End; - while _step_id_46283 > 0 and _index_id_46278 <= _end_id_46288 or _step_id_46283 < 0 and _index_id_46278 >= _end_id_46288 { - let _index : Int = _index_id_46278; + let _range_id_46346 : Range = Length(_array) - 1..-1..0; + mutable _index_id_46349 : Int = _range_id_46346::Start; + let _step_id_46354 : Int = _range_id_46346::Step; + let _end_id_46359 : Int = _range_id_46346::End; + while _step_id_46354 > 0 and _index_id_46349 <= _end_id_46359 or _step_id_46354 < 0 and _index_id_46349 >= _end_id_46359 { + let _index : Int = _index_id_46349; let item : Qubit = _array[_index]; Adjoint H(item); - _index_id_46278 += _step_id_46283; + _index_id_46349 += _step_id_46354; } } @@ -1965,13 +1951,13 @@ fn full_pipeline_handles_stdlib_apply_to_each() { } operation ApplyToEach_Qubit__AdjCtl__H_(register : Qubit[]) : Unit { { - let _array_id_46209 : Qubit[] = register; - let _len_id_46213 : Int = Length(_array_id_46209); - mutable _index_id_46218 : Int = 0; - while _index_id_46218 < _len_id_46213 { - let item : Qubit = _array_id_46209[_index_id_46218]; + let _array_id_46280 : Qubit[] = register; + let _len_id_46284 : Int = Length(_array_id_46280); + mutable _index_id_46289 : Int = 0; + while _index_id_46289 < _len_id_46284 { + let item : Qubit = _array_id_46280[_index_id_46289]; H(item); - _index_id_46218 += 1; + _index_id_46289 += 1; } } @@ -2013,13 +1999,13 @@ fn full_pipeline_handles_stdlib_apply_to_each_with_custom_intrinsic() { } operation ApplyToEach_Qubit__AdjCtl__SX_(register : Qubit[]) : Unit { { - let _array_id_46209 : Qubit[] = register; - let _len_id_46213 : Int = Length(_array_id_46209); - mutable _index_id_46218 : Int = 0; - while _index_id_46218 < _len_id_46213 { - let item : Qubit = _array_id_46209[_index_id_46218]; + let _array_id_46280 : Qubit[] = register; + let _len_id_46284 : Int = Length(_array_id_46280); + mutable _index_id_46289 : Int = 0; + while _index_id_46289 < _len_id_46284 { + let item : Qubit = _array_id_46280[_index_id_46289]; SX(item); - _index_id_46218 += 1; + _index_id_46289 += 1; } } @@ -2061,13 +2047,13 @@ fn apply_to_each_body_callable_defunctionalizes() { } operation ApplyToEach_Qubit__AdjCtl__H_(register : Qubit[]) : Unit { { - let _array_id_46209 : Qubit[] = register; - let _len_id_46213 : Int = Length(_array_id_46209); - mutable _index_id_46218 : Int = 0; - while _index_id_46218 < _len_id_46213 { - let item : Qubit = _array_id_46209[_index_id_46218]; + let _array_id_46280 : Qubit[] = register; + let _len_id_46284 : Int = Length(_array_id_46280); + mutable _index_id_46289 : Int = 0; + while _index_id_46289 < _len_id_46284 { + let item : Qubit = _array_id_46280[_index_id_46289]; H(item); - _index_id_46218 += 1; + _index_id_46289 += 1; } } @@ -2113,13 +2099,13 @@ fn apply_to_each_a_adjoint_callable_defunctionalizes() { operation ApplyToEachA_Qubit__AdjCtl__S_(register : Qubit[]) : Unit is Adj { body ... { { - let _array_id_46237 : Qubit[] = register; - let _len_id_46241 : Int = Length(_array_id_46237); - mutable _index_id_46246 : Int = 0; - while _index_id_46246 < _len_id_46241 { - let item : Qubit = _array_id_46237[_index_id_46246]; + let _array_id_46308 : Qubit[] = register; + let _len_id_46312 : Int = Length(_array_id_46308); + mutable _index_id_46317 : Int = 0; + while _index_id_46317 < _len_id_46312 { + let item : Qubit = _array_id_46308[_index_id_46317]; S(item); - _index_id_46246 += 1; + _index_id_46317 += 1; } } @@ -2129,15 +2115,15 @@ fn apply_to_each_a_adjoint_callable_defunctionalizes() { { let _array : Qubit[] = register; { - let _range_id_46265 : Range = Length(_array) - 1..-1..0; - mutable _index_id_46268 : Int = _range_id_46265::Start; - let _step_id_46273 : Int = _range_id_46265::Step; - let _end_id_46278 : Int = _range_id_46265::End; - while _step_id_46273 > 0 and _index_id_46268 <= _end_id_46278 or _step_id_46273 < 0 and _index_id_46268 >= _end_id_46278 { - let _index : Int = _index_id_46268; + let _range_id_46336 : Range = Length(_array) - 1..-1..0; + mutable _index_id_46339 : Int = _range_id_46336::Start; + let _step_id_46344 : Int = _range_id_46336::Step; + let _end_id_46349 : Int = _range_id_46336::End; + while _step_id_46344 > 0 and _index_id_46339 <= _end_id_46349 or _step_id_46344 < 0 and _index_id_46339 >= _end_id_46349 { + let _index : Int = _index_id_46339; let item : Qubit = _array[_index]; Adjoint S(item); - _index_id_46268 += _step_id_46273; + _index_id_46339 += _step_id_46344; } } @@ -2189,13 +2175,13 @@ fn apply_to_each_c_controlled_callable_defunctionalizes() { operation ApplyToEachC_Qubit__AdjCtl__X_(register : Qubit[]) : Unit is Ctl { body ... { { - let _array_id_46308 : Qubit[] = register; - let _len_id_46312 : Int = Length(_array_id_46308); - mutable _index_id_46317 : Int = 0; - while _index_id_46317 < _len_id_46312 { - let item : Qubit = _array_id_46308[_index_id_46317]; + let _array_id_46379 : Qubit[] = register; + let _len_id_46383 : Int = Length(_array_id_46379); + mutable _index_id_46388 : Int = 0; + while _index_id_46388 < _len_id_46383 { + let item : Qubit = _array_id_46379[_index_id_46388]; X(item); - _index_id_46317 += 1; + _index_id_46388 += 1; } } @@ -2203,13 +2189,13 @@ fn apply_to_each_c_controlled_callable_defunctionalizes() { } controlled (ctls, ...) { { - let _array_id_46336 : Qubit[] = register; - let _len_id_46340 : Int = Length(_array_id_46336); - mutable _index_id_46345 : Int = 0; - while _index_id_46345 < _len_id_46340 { - let item : Qubit = _array_id_46336[_index_id_46345]; + let _array_id_46407 : Qubit[] = register; + let _len_id_46411 : Int = Length(_array_id_46407); + mutable _index_id_46416 : Int = 0; + while _index_id_46416 < _len_id_46411 { + let item : Qubit = _array_id_46407[_index_id_46416]; Controlled X(ctls, item); - _index_id_46345 += 1; + _index_id_46416 += 1; } } @@ -2253,13 +2239,13 @@ fn apply_to_each_ca_callable_defunctionalizes() { operation ApplyToEachCA_Qubit__AdjCtl__S_(register : Qubit[]) : Unit is Adj + Ctl { body ... { { - let _array_id_46364 : Qubit[] = register; - let _len_id_46368 : Int = Length(_array_id_46364); - mutable _index_id_46373 : Int = 0; - while _index_id_46373 < _len_id_46368 { - let item : Qubit = _array_id_46364[_index_id_46373]; + let _array_id_46435 : Qubit[] = register; + let _len_id_46439 : Int = Length(_array_id_46435); + mutable _index_id_46444 : Int = 0; + while _index_id_46444 < _len_id_46439 { + let item : Qubit = _array_id_46435[_index_id_46444]; S(item); - _index_id_46373 += 1; + _index_id_46444 += 1; } } @@ -2269,15 +2255,15 @@ fn apply_to_each_ca_callable_defunctionalizes() { { let _array : Qubit[] = register; { - let _range_id_46392 : Range = Length(_array) - 1..-1..0; - mutable _index_id_46395 : Int = _range_id_46392::Start; - let _step_id_46400 : Int = _range_id_46392::Step; - let _end_id_46405 : Int = _range_id_46392::End; - while _step_id_46400 > 0 and _index_id_46395 <= _end_id_46405 or _step_id_46400 < 0 and _index_id_46395 >= _end_id_46405 { - let _index : Int = _index_id_46395; + let _range_id_46463 : Range = Length(_array) - 1..-1..0; + mutable _index_id_46466 : Int = _range_id_46463::Start; + let _step_id_46471 : Int = _range_id_46463::Step; + let _end_id_46476 : Int = _range_id_46463::End; + while _step_id_46471 > 0 and _index_id_46466 <= _end_id_46476 or _step_id_46471 < 0 and _index_id_46466 >= _end_id_46476 { + let _index : Int = _index_id_46466; let item : Qubit = _array[_index]; Adjoint S(item); - _index_id_46395 += _step_id_46400; + _index_id_46466 += _step_id_46471; } } @@ -2287,13 +2273,13 @@ fn apply_to_each_ca_callable_defunctionalizes() { } controlled (ctls, ...) { { - let _array_id_46435 : Qubit[] = register; - let _len_id_46439 : Int = Length(_array_id_46435); - mutable _index_id_46444 : Int = 0; - while _index_id_46444 < _len_id_46439 { - let item : Qubit = _array_id_46435[_index_id_46444]; + let _array_id_46506 : Qubit[] = register; + let _len_id_46510 : Int = Length(_array_id_46506); + mutable _index_id_46515 : Int = 0; + while _index_id_46515 < _len_id_46510 { + let item : Qubit = _array_id_46506[_index_id_46515]; Controlled S(ctls, item); - _index_id_46444 += 1; + _index_id_46515 += 1; } } @@ -2303,15 +2289,15 @@ fn apply_to_each_ca_callable_defunctionalizes() { { let _array : Qubit[] = register; { - let _range_id_46463 : Range = Length(_array) - 1..-1..0; - mutable _index_id_46466 : Int = _range_id_46463::Start; - let _step_id_46471 : Int = _range_id_46463::Step; - let _end_id_46476 : Int = _range_id_46463::End; - while _step_id_46471 > 0 and _index_id_46466 <= _end_id_46476 or _step_id_46471 < 0 and _index_id_46466 >= _end_id_46476 { - let _index : Int = _index_id_46466; + let _range_id_46534 : Range = Length(_array) - 1..-1..0; + mutable _index_id_46537 : Int = _range_id_46534::Start; + let _step_id_46542 : Int = _range_id_46534::Step; + let _end_id_46547 : Int = _range_id_46534::End; + while _step_id_46542 > 0 and _index_id_46537 <= _end_id_46547 or _step_id_46542 < 0 and _index_id_46537 >= _end_id_46547 { + let _index : Int = _index_id_46537; let item : Qubit = _array[_index]; Controlled Adjoint S(ctls, item); - _index_id_46466 += _step_id_46471; + _index_id_46537 += _step_id_46542; } } @@ -2365,13 +2351,13 @@ fn cross_package_apply_to_each_closure_arg_defunctionalizes() { } operation ApplyToEach_Qubit__Empty__closure_(register : Qubit[], __capture_0 : Double) : Unit { { - let _array_id_46209 : Qubit[] = register; - let _len_id_46213 : Int = Length(_array_id_46209); - mutable _index_id_46218 : Int = 0; - while _index_id_46218 < _len_id_46213 { - let item : Qubit = _array_id_46209[_index_id_46218]; + let _array_id_46280 : Qubit[] = register; + let _len_id_46284 : Int = Length(_array_id_46280); + mutable _index_id_46289 : Int = 0; + while _index_id_46289 < _len_id_46284 { + let item : Qubit = _array_id_46280[_index_id_46289]; _lambda_2(__capture_0, item); - _index_id_46218 += 1; + _index_id_46289 += 1; } } @@ -2413,13 +2399,13 @@ fn cross_package_apply_to_each_adjoint_arg_defunctionalizes() { } operation ApplyToEach_Qubit__AdjCtl__Adj_S_(register : Qubit[]) : Unit { { - let _array_id_46209 : Qubit[] = register; - let _len_id_46213 : Int = Length(_array_id_46209); - mutable _index_id_46218 : Int = 0; - while _index_id_46218 < _len_id_46213 { - let item : Qubit = _array_id_46209[_index_id_46218]; + let _array_id_46280 : Qubit[] = register; + let _len_id_46284 : Int = Length(_array_id_46280); + mutable _index_id_46289 : Int = 0; + while _index_id_46289 < _len_id_46284 { + let item : Qubit = _array_id_46280[_index_id_46289]; Adjoint S(item); - _index_id_46218 += 1; + _index_id_46289 += 1; } } @@ -2462,13 +2448,13 @@ fn adjoint_cross_package_apply_to_each_ca_defunctionalizes() { operation ApplyToEachCA_Qubit__AdjCtl__S_(register : Qubit[]) : Unit is Adj + Ctl { body ... { { - let _array_id_46364 : Qubit[] = register; - let _len_id_46368 : Int = Length(_array_id_46364); - mutable _index_id_46373 : Int = 0; - while _index_id_46373 < _len_id_46368 { - let item : Qubit = _array_id_46364[_index_id_46373]; + let _array_id_46435 : Qubit[] = register; + let _len_id_46439 : Int = Length(_array_id_46435); + mutable _index_id_46444 : Int = 0; + while _index_id_46444 < _len_id_46439 { + let item : Qubit = _array_id_46435[_index_id_46444]; S(item); - _index_id_46373 += 1; + _index_id_46444 += 1; } } @@ -2478,15 +2464,15 @@ fn adjoint_cross_package_apply_to_each_ca_defunctionalizes() { { let _array : Qubit[] = register; { - let _range_id_46392 : Range = Length(_array) - 1..-1..0; - mutable _index_id_46395 : Int = _range_id_46392::Start; - let _step_id_46400 : Int = _range_id_46392::Step; - let _end_id_46405 : Int = _range_id_46392::End; - while _step_id_46400 > 0 and _index_id_46395 <= _end_id_46405 or _step_id_46400 < 0 and _index_id_46395 >= _end_id_46405 { - let _index : Int = _index_id_46395; + let _range_id_46463 : Range = Length(_array) - 1..-1..0; + mutable _index_id_46466 : Int = _range_id_46463::Start; + let _step_id_46471 : Int = _range_id_46463::Step; + let _end_id_46476 : Int = _range_id_46463::End; + while _step_id_46471 > 0 and _index_id_46466 <= _end_id_46476 or _step_id_46471 < 0 and _index_id_46466 >= _end_id_46476 { + let _index : Int = _index_id_46466; let item : Qubit = _array[_index]; Adjoint S(item); - _index_id_46395 += _step_id_46400; + _index_id_46466 += _step_id_46471; } } @@ -2496,13 +2482,13 @@ fn adjoint_cross_package_apply_to_each_ca_defunctionalizes() { } controlled (ctls, ...) { { - let _array_id_46435 : Qubit[] = register; - let _len_id_46439 : Int = Length(_array_id_46435); - mutable _index_id_46444 : Int = 0; - while _index_id_46444 < _len_id_46439 { - let item : Qubit = _array_id_46435[_index_id_46444]; + let _array_id_46506 : Qubit[] = register; + let _len_id_46510 : Int = Length(_array_id_46506); + mutable _index_id_46515 : Int = 0; + while _index_id_46515 < _len_id_46510 { + let item : Qubit = _array_id_46506[_index_id_46515]; Controlled S(ctls, item); - _index_id_46444 += 1; + _index_id_46515 += 1; } } @@ -2512,15 +2498,15 @@ fn adjoint_cross_package_apply_to_each_ca_defunctionalizes() { { let _array : Qubit[] = register; { - let _range_id_46463 : Range = Length(_array) - 1..-1..0; - mutable _index_id_46466 : Int = _range_id_46463::Start; - let _step_id_46471 : Int = _range_id_46463::Step; - let _end_id_46476 : Int = _range_id_46463::End; - while _step_id_46471 > 0 and _index_id_46466 <= _end_id_46476 or _step_id_46471 < 0 and _index_id_46466 >= _end_id_46476 { - let _index : Int = _index_id_46466; + let _range_id_46534 : Range = Length(_array) - 1..-1..0; + mutable _index_id_46537 : Int = _range_id_46534::Start; + let _step_id_46542 : Int = _range_id_46534::Step; + let _end_id_46547 : Int = _range_id_46534::End; + while _step_id_46542 > 0 and _index_id_46537 <= _end_id_46547 or _step_id_46542 < 0 and _index_id_46537 >= _end_id_46547 { + let _index : Int = _index_id_46537; let item : Qubit = _array[_index]; Controlled Adjoint S(ctls, item); - _index_id_46466 += _step_id_46471; + _index_id_46537 += _step_id_46542; } } @@ -2638,13 +2624,13 @@ fn controlled_apply_to_each_ca_keeps_body_callable_static() { operation ApplyToEachCA_Qubit__AdjCtl__X_(register : Qubit[]) : Unit is Adj + Ctl { body ... { { - let _array_id_46364 : Qubit[] = register; - let _len_id_46368 : Int = Length(_array_id_46364); - mutable _index_id_46373 : Int = 0; - while _index_id_46373 < _len_id_46368 { - let item : Qubit = _array_id_46364[_index_id_46373]; + let _array_id_46435 : Qubit[] = register; + let _len_id_46439 : Int = Length(_array_id_46435); + mutable _index_id_46444 : Int = 0; + while _index_id_46444 < _len_id_46439 { + let item : Qubit = _array_id_46435[_index_id_46444]; X(item); - _index_id_46373 += 1; + _index_id_46444 += 1; } } @@ -2654,15 +2640,15 @@ fn controlled_apply_to_each_ca_keeps_body_callable_static() { { let _array : Qubit[] = register; { - let _range_id_46392 : Range = Length(_array) - 1..-1..0; - mutable _index_id_46395 : Int = _range_id_46392::Start; - let _step_id_46400 : Int = _range_id_46392::Step; - let _end_id_46405 : Int = _range_id_46392::End; - while _step_id_46400 > 0 and _index_id_46395 <= _end_id_46405 or _step_id_46400 < 0 and _index_id_46395 >= _end_id_46405 { - let _index : Int = _index_id_46395; + let _range_id_46463 : Range = Length(_array) - 1..-1..0; + mutable _index_id_46466 : Int = _range_id_46463::Start; + let _step_id_46471 : Int = _range_id_46463::Step; + let _end_id_46476 : Int = _range_id_46463::End; + while _step_id_46471 > 0 and _index_id_46466 <= _end_id_46476 or _step_id_46471 < 0 and _index_id_46466 >= _end_id_46476 { + let _index : Int = _index_id_46466; let item : Qubit = _array[_index]; Adjoint X(item); - _index_id_46395 += _step_id_46400; + _index_id_46466 += _step_id_46471; } } @@ -2672,13 +2658,13 @@ fn controlled_apply_to_each_ca_keeps_body_callable_static() { } controlled (ctls, ...) { { - let _array_id_46435 : Qubit[] = register; - let _len_id_46439 : Int = Length(_array_id_46435); - mutable _index_id_46444 : Int = 0; - while _index_id_46444 < _len_id_46439 { - let item : Qubit = _array_id_46435[_index_id_46444]; + let _array_id_46506 : Qubit[] = register; + let _len_id_46510 : Int = Length(_array_id_46506); + mutable _index_id_46515 : Int = 0; + while _index_id_46515 < _len_id_46510 { + let item : Qubit = _array_id_46506[_index_id_46515]; Controlled X(ctls, item); - _index_id_46444 += 1; + _index_id_46515 += 1; } } @@ -2688,15 +2674,15 @@ fn controlled_apply_to_each_ca_keeps_body_callable_static() { { let _array : Qubit[] = register; { - let _range_id_46463 : Range = Length(_array) - 1..-1..0; - mutable _index_id_46466 : Int = _range_id_46463::Start; - let _step_id_46471 : Int = _range_id_46463::Step; - let _end_id_46476 : Int = _range_id_46463::End; - while _step_id_46471 > 0 and _index_id_46466 <= _end_id_46476 or _step_id_46471 < 0 and _index_id_46466 >= _end_id_46476 { - let _index : Int = _index_id_46466; + let _range_id_46534 : Range = Length(_array) - 1..-1..0; + mutable _index_id_46537 : Int = _range_id_46534::Start; + let _step_id_46542 : Int = _range_id_46534::Step; + let _end_id_46547 : Int = _range_id_46534::End; + while _step_id_46542 > 0 and _index_id_46537 <= _end_id_46547 or _step_id_46542 < 0 and _index_id_46537 >= _end_id_46547 { + let _index : Int = _index_id_46537; let item : Qubit = _array[_index]; Controlled Adjoint X(ctls, item); - _index_id_46466 += _step_id_46471; + _index_id_46537 += _step_id_46542; } } @@ -2708,13 +2694,13 @@ fn controlled_apply_to_each_ca_keeps_body_callable_static() { operation ApplyToEachCA_Qubit__AdjCtl__H_(register : Qubit[]) : Unit is Adj + Ctl { body ... { { - let _array_id_46364 : Qubit[] = register; - let _len_id_46368 : Int = Length(_array_id_46364); - mutable _index_id_46373 : Int = 0; - while _index_id_46373 < _len_id_46368 { - let item : Qubit = _array_id_46364[_index_id_46373]; + let _array_id_46435 : Qubit[] = register; + let _len_id_46439 : Int = Length(_array_id_46435); + mutable _index_id_46444 : Int = 0; + while _index_id_46444 < _len_id_46439 { + let item : Qubit = _array_id_46435[_index_id_46444]; H(item); - _index_id_46373 += 1; + _index_id_46444 += 1; } } @@ -2724,15 +2710,15 @@ fn controlled_apply_to_each_ca_keeps_body_callable_static() { { let _array : Qubit[] = register; { - let _range_id_46392 : Range = Length(_array) - 1..-1..0; - mutable _index_id_46395 : Int = _range_id_46392::Start; - let _step_id_46400 : Int = _range_id_46392::Step; - let _end_id_46405 : Int = _range_id_46392::End; - while _step_id_46400 > 0 and _index_id_46395 <= _end_id_46405 or _step_id_46400 < 0 and _index_id_46395 >= _end_id_46405 { - let _index : Int = _index_id_46395; + let _range_id_46463 : Range = Length(_array) - 1..-1..0; + mutable _index_id_46466 : Int = _range_id_46463::Start; + let _step_id_46471 : Int = _range_id_46463::Step; + let _end_id_46476 : Int = _range_id_46463::End; + while _step_id_46471 > 0 and _index_id_46466 <= _end_id_46476 or _step_id_46471 < 0 and _index_id_46466 >= _end_id_46476 { + let _index : Int = _index_id_46466; let item : Qubit = _array[_index]; Adjoint H(item); - _index_id_46395 += _step_id_46400; + _index_id_46466 += _step_id_46471; } } @@ -2742,13 +2728,13 @@ fn controlled_apply_to_each_ca_keeps_body_callable_static() { } controlled (ctls, ...) { { - let _array_id_46435 : Qubit[] = register; - let _len_id_46439 : Int = Length(_array_id_46435); - mutable _index_id_46444 : Int = 0; - while _index_id_46444 < _len_id_46439 { - let item : Qubit = _array_id_46435[_index_id_46444]; + let _array_id_46506 : Qubit[] = register; + let _len_id_46510 : Int = Length(_array_id_46506); + mutable _index_id_46515 : Int = 0; + while _index_id_46515 < _len_id_46510 { + let item : Qubit = _array_id_46506[_index_id_46515]; Controlled H(ctls, item); - _index_id_46444 += 1; + _index_id_46515 += 1; } } @@ -2758,15 +2744,15 @@ fn controlled_apply_to_each_ca_keeps_body_callable_static() { { let _array : Qubit[] = register; { - let _range_id_46463 : Range = Length(_array) - 1..-1..0; - mutable _index_id_46466 : Int = _range_id_46463::Start; - let _step_id_46471 : Int = _range_id_46463::Step; - let _end_id_46476 : Int = _range_id_46463::End; - while _step_id_46471 > 0 and _index_id_46466 <= _end_id_46476 or _step_id_46471 < 0 and _index_id_46466 >= _end_id_46476 { - let _index : Int = _index_id_46466; + let _range_id_46534 : Range = Length(_array) - 1..-1..0; + mutable _index_id_46537 : Int = _range_id_46534::Start; + let _step_id_46542 : Int = _range_id_46534::Step; + let _end_id_46547 : Int = _range_id_46534::End; + while _step_id_46542 > 0 and _index_id_46537 <= _end_id_46547 or _step_id_46542 < 0 and _index_id_46537 >= _end_id_46547 { + let _index : Int = _index_id_46537; let item : Qubit = _array[_index]; Controlled Adjoint H(ctls, item); - _index_id_46466 += _step_id_46471; + _index_id_46537 += _step_id_46542; } } @@ -2818,13 +2804,13 @@ fn cross_package_mapped_defunctionalizes() { function Mapped_Int__Int__Double_(array : Int[]) : Int[] { mutable mapped : Int[] = []; { - let _array_id_45723 : Int[] = array; - let _len_id_45727 : Int = Length(_array_id_45723); - mutable _index_id_45732 : Int = 0; - while _index_id_45732 < _len_id_45727 { - let element : Int = _array_id_45723[_index_id_45732]; + let _array_id_45794 : Int[] = array; + let _len_id_45798 : Int = Length(_array_id_45794); + mutable _index_id_45803 : Int = 0; + while _index_id_45803 < _len_id_45798 { + let element : Int = _array_id_45794[_index_id_45803]; mapped += [Double(element)]; - _index_id_45732 += 1; + _index_id_45803 += 1; } } @@ -2868,13 +2854,13 @@ fn cross_package_for_each_defunctionalizes() { operation ForEach_Qubit__Unit__AdjCtl__H_(array : Qubit[]) : Unit[] { mutable output : Unit[] = []; { - let _array_id_45495 : Qubit[] = array; - let _len_id_45499 : Int = Length(_array_id_45495); - mutable _index_id_45504 : Int = 0; - while _index_id_45504 < _len_id_45499 { - let element : Qubit = _array_id_45495[_index_id_45504]; + let _array_id_45566 : Qubit[] = array; + let _len_id_45570 : Int = Length(_array_id_45566); + mutable _index_id_45575 : Int = 0; + while _index_id_45575 < _len_id_45570 { + let element : Qubit = _array_id_45566[_index_id_45575]; output += [H(element)]; - _index_id_45504 += 1; + _index_id_45575 += 1; } } @@ -2929,13 +2915,13 @@ fn stdlib_hof_specialized_with_concrete_callable() { function Mapped_Int__Int__closure_(array : Int[]) : Int[] { mutable mapped : Int[] = []; { - let _array_id_45723 : Int[] = array; - let _len_id_45727 : Int = Length(_array_id_45723); - mutable _index_id_45732 : Int = 0; - while _index_id_45732 < _len_id_45727 { - let element : Int = _array_id_45723[_index_id_45732]; + let _array_id_45794 : Int[] = array; + let _len_id_45798 : Int = Length(_array_id_45794); + mutable _index_id_45803 : Int = 0; + while _index_id_45803 < _len_id_45798 { + let element : Int = _array_id_45794[_index_id_45803]; mapped += [_lambda_2(element, )]; - _index_id_45732 += 1; + _index_id_45803 += 1; } } @@ -3013,13 +2999,13 @@ fn lambda_expression_sample_shape_has_no_defunctionalization_errors() { function Fold_Int__Int__closure_(state : Int, array : Int[]) : Int { mutable current : Int = state; { - let _array_id_45467 : Int[] = array; - let _len_id_45471 : Int = Length(_array_id_45467); - mutable _index_id_45476 : Int = 0; - while _index_id_45476 < _len_id_45471 { - let element : Int = _array_id_45467[_index_id_45476]; + let _array_id_45538 : Int[] = array; + let _len_id_45542 : Int = Length(_array_id_45538); + mutable _index_id_45547 : Int = 0; + while _index_id_45547 < _len_id_45542 { + let element : Int = _array_id_45538[_index_id_45547]; current = _lambda_2((current, element), ); - _index_id_45476 += 1; + _index_id_45547 += 1; } } @@ -3029,13 +3015,13 @@ fn lambda_expression_sample_shape_has_no_defunctionalization_errors() { function Mapped_Int__Int__closure_(array : Int[]) : Int[] { mutable mapped : Int[] = []; { - let _array_id_45723 : Int[] = array; - let _len_id_45727 : Int = Length(_array_id_45723); - mutable _index_id_45732 : Int = 0; - while _index_id_45732 < _len_id_45727 { - let element : Int = _array_id_45723[_index_id_45732]; + let _array_id_45794 : Int[] = array; + let _len_id_45798 : Int = Length(_array_id_45794); + mutable _index_id_45803 : Int = 0; + while _index_id_45803 < _len_id_45798 { + let element : Int = _array_id_45794[_index_id_45803]; mapped += [_lambda_4(element, )]; - _index_id_45732 += 1; + _index_id_45803 += 1; } } @@ -3151,13 +3137,13 @@ fn partial_application_sample_shape_has_no_defunctionalization_errors() { function Mapped_Int__Int__closure_(array : Int[], __capture_0 : Int) : Int[] { mutable mapped : Int[] = []; { - let _array_id_45723 : Int[] = array; - let _len_id_45727 : Int = Length(_array_id_45723); - mutable _index_id_45732 : Int = 0; - while _index_id_45732 < _len_id_45727 { - let element : Int = _array_id_45723[_index_id_45732]; + let _array_id_45794 : Int[] = array; + let _len_id_45798 : Int = Length(_array_id_45794); + mutable _index_id_45803 : Int = 0; + while _index_id_45803 < _len_id_45798 { + let element : Int = _array_id_45794[_index_id_45803]; mapped += [_lambda_8(__capture_0, element)]; - _index_id_45732 += 1; + _index_id_45803 += 1; } } diff --git a/source/compiler/qsc_fir_transforms/src/defunctionalize/tests/fixpoint.rs b/source/compiler/qsc_fir_transforms/src/defunctionalize/tests/fixpoint.rs index 8934d57def5..4c6175922d1 100644 --- a/source/compiler/qsc_fir_transforms/src/defunctionalize/tests/fixpoint.rs +++ b/source/compiler/qsc_fir_transforms/src/defunctionalize/tests/fixpoint.rs @@ -811,14 +811,6 @@ fn nested_hof_requires_multi_iteration_convergence() { op(q); op(q); } - operation ApplyAndMeasure_Empty__AdjCtl__ApplyTwice_Empty__(op : (Qubit => Unit is Adj + Ctl), q : Qubit) : Result { - ApplyTwice_Empty_(op, q); - M(q) - } - operation ApplyAndMeasure_Empty__AdjCtl__H_(op : (Qubit => Unit is Adj + Ctl), q : Qubit) : Result { - H(op, q); - M(q) - } operation ApplyAndMeasure_Empty__AdjCtl__ApplyTwice_Empty___H_(q : Qubit) : Result { ApplyTwice_Empty__H_(q); M(q) @@ -1039,12 +1031,6 @@ fn transient_dynamic_resolves_after_outer_hof_specialization() { operation ApplyMiddle_Empty_(op : (Qubit => Unit), q : Qubit) : Unit { ApplyInner_Empty_(op, q); } - operation ApplyOuter_Empty__AdjCtl__ApplyMiddle_Empty__(op : (Qubit => Unit is Adj + Ctl), q : Qubit) : Unit { - ApplyMiddle_Empty_(op, q); - } - operation ApplyOuter_Empty__AdjCtl__H_(op : (Qubit => Unit is Adj + Ctl), q : Qubit) : Unit { - H(op, q); - } operation ApplyOuter_Empty__AdjCtl__ApplyMiddle_Empty___H_(q : Qubit) : Unit { ApplyMiddle_Empty__H_(q); } @@ -1060,6 +1046,273 @@ fn transient_dynamic_resolves_after_outer_hof_specialization() { ); } +/// Two-level cross-HOF regression for callable-array forwarding. An outer HOF +/// receives a closure array as a flat parameter and forwards it to an inner HOF +/// that indexes the array under a loop. The closures capture DISTINCT integer +/// values, so a collapse to a single element would be observable. +/// +/// The correct post-fix behavior threads ALL array elements across both HOF +/// levels: the inner HOF specializes into an `if idx == p` dispatch chain with a +/// DISTINCT `__capture_i` per branch, and the outer forwards every capture (not +/// just `__capture_0`). A pre-fix cross-HOF array collapse would have produced a +/// single `__capture_0` and no dispatch chain. +#[test] +fn two_level_cross_hof_closure_array_forwarding_threads_all_captures() { + let source = r#" + operation ApplyParityOperation(value : Int, control : Qubit, register : Qubit[]) : Unit { + if value == 1 { + Controlled X([control], register[0]); + } + } + + operation ApplyInner( + ops : ((Qubit, Qubit[]) => Unit)[], + count : Int, + controls : Qubit[], + targets : Qubit[] + ) : Unit { + for idx in 0..count - 1 { + ops[idx](controls[idx], targets); + } + } + + operation ApplyOuter( + ops : ((Qubit, Qubit[]) => Unit)[], + count : Int, + controls : Qubit[], + targets : Qubit[] + ) : Unit { + ApplyInner(ops, count, controls, targets); + } + + operation Main() : Unit { + use qs = Qubit[3]; + let controls = qs[0..1]; + let targets = qs[2...]; + let ops = [ApplyParityOperation(1, _, _), ApplyParityOperation(2, _, _)]; + ApplyOuter(ops, 2, controls, targets); + ResetAll(qs); + } + "#; + check_errors(source, &expect!["(no error)"]); + check_analysis( + source, + &expect![[r#" + callable_params: 2 + param: callable_id=, path=[0], ty=(((Qubit, (Qubit)[]) => Unit))[] + param: callable_id=, path=[0], ty=(((Qubit, (Qubit)[]) => Unit))[] + call_sites: 3 + site: hof=ApplyInner, arg=Dynamic + site: hof=ApplyOuter, arg=Closure(target=5, Body) + site: hof=ApplyOuter, arg=Closure(target=6, Body) + direct_call_sites: 1 + site: callee=X:Ctl, default"#]], + ); + check_rewrite( + source, + &expect![[r#" + BEFORE: + operation ApplyParityOperation(value : Int, control : Qubit, register : Qubit[]) : Unit { + if value == 1 { + Controlled X([control], register[0]); + } + + } + operation ApplyInner(ops : ((Qubit, Qubit[]) => Unit)[], count : Int, controls : Qubit[], targets : Qubit[]) : Unit { + { + let _range_id_183 : Range = 0..count - 1; + mutable _index_id_186 : Int = _range_id_183::Start; + let _step_id_191 : Int = _range_id_183::Step; + let _end_id_196 : Int = _range_id_183::End; + while _step_id_191 > 0 and _index_id_186 <= _end_id_196 or _step_id_191 < 0 and _index_id_186 >= _end_id_196 { + let idx : Int = _index_id_186; + ops[idx](controls[idx], targets); + _index_id_186 += _step_id_191; + } + + } + + } + operation ApplyOuter(ops : ((Qubit, Qubit[]) => Unit)[], count : Int, controls : Qubit[], targets : Qubit[]) : Unit { + ApplyInner_Empty_(ops, count, controls, targets); + } + operation Main() : Unit { + let qs : Qubit[] = AllocateQubitArray(3); + let controls : Qubit[] = qs[0..1]; + let targets : Qubit[] = qs[2...]; + let ops : ((Qubit, Qubit[]) => Unit)[] = [{ + let arg : Int = 1; + / * closure item = 5 captures = [arg] * / _lambda_5 + }, { + let arg : Int = 2; + / * closure item = 6 captures = [arg] * / _lambda_6 + }]; + ApplyOuter_Empty_(ops, 2, controls, targets); + ResetAll(qs); + ReleaseQubitArray(qs); + } + operation _lambda_5(arg : Int, (hole : Qubit, hole : Qubit[])) : Unit { + ApplyParityOperation(arg, hole, hole) + } + operation _lambda_6(arg : Int, (hole : Qubit, hole : Qubit[])) : Unit { + ApplyParityOperation(arg, hole, hole) + } + operation ApplyInner_Empty_(ops : ((Qubit, Qubit[]) => Unit)[], count : Int, controls : Qubit[], targets : Qubit[]) : Unit { + { + let _range_id_183 : Range = 0..count - 1; + mutable _index_id_186 : Int = _range_id_183::Start; + let _step_id_191 : Int = _range_id_183::Step; + let _end_id_196 : Int = _range_id_183::End; + while _step_id_191 > 0 and _index_id_186 <= _end_id_196 or _step_id_191 < 0 and _index_id_186 >= _end_id_196 { + let idx : Int = _index_id_186; + ops[idx](controls[idx], targets); + _index_id_186 += _step_id_191; + } + + } + + } + operation ApplyOuter_Empty_(ops : ((Qubit, Qubit[]) => Unit)[], count : Int, controls : Qubit[], targets : Qubit[]) : Unit { + ApplyInner_Empty_(ops, count, controls, targets); + } + // entry + Main() + + AFTER: + operation ApplyParityOperation(value : Int, control : Qubit, register : Qubit[]) : Unit { + if value == 1 { + Controlled X([control], register[0]); + } + + } + operation ApplyInner(ops : ((Qubit, Qubit[]) => Unit)[], count : Int, controls : Qubit[], targets : Qubit[]) : Unit { + { + let _range_id_183 : Range = 0..count - 1; + mutable _index_id_186 : Int = _range_id_183::Start; + let _step_id_191 : Int = _range_id_183::Step; + let _end_id_196 : Int = _range_id_183::End; + while _step_id_191 > 0 and _index_id_186 <= _end_id_196 or _step_id_191 < 0 and _index_id_186 >= _end_id_196 { + let idx : Int = _index_id_186; + ops[idx](controls[idx], targets); + _index_id_186 += _step_id_191; + } + + } + + } + operation ApplyOuter(ops : ((Qubit, Qubit[]) => Unit)[], count : Int, controls : Qubit[], targets : Qubit[]) : Unit { + ApplyInner_Empty_(ops, count, controls, targets); + } + operation Main() : Unit { + let qs : Qubit[] = AllocateQubitArray(3); + let controls : Qubit[] = qs[0..1]; + let targets : Qubit[] = qs[2...]; + ApplyOuter_Empty__closure__closure_(2, controls, targets, 1, 2); + ResetAll(qs); + ReleaseQubitArray(qs); + } + operation _lambda_5(arg : Int, (hole : Qubit, hole : Qubit[])) : Unit { + ApplyParityOperation(arg, hole, hole) + } + operation _lambda_6(arg : Int, (hole : Qubit, hole : Qubit[])) : Unit { + ApplyParityOperation(arg, hole, hole) + } + operation ApplyInner_Empty_(ops : ((Qubit, Qubit[]) => Unit)[], count : Int, controls : Qubit[], targets : Qubit[]) : Unit { + { + let _range_id_183 : Range = 0..count - 1; + mutable _index_id_186 : Int = _range_id_183::Start; + let _step_id_191 : Int = _range_id_183::Step; + let _end_id_196 : Int = _range_id_183::End; + while _step_id_191 > 0 and _index_id_186 <= _end_id_196 or _step_id_191 < 0 and _index_id_186 >= _end_id_196 { + let idx : Int = _index_id_186; + ops[idx](controls[idx], targets); + _index_id_186 += _step_id_191; + } + + } + + } + operation ApplyOuter_Empty_(ops : ((Qubit, Qubit[]) => Unit)[], count : Int, controls : Qubit[], targets : Qubit[]) : Unit { + ApplyInner_Empty_(ops, count, controls, targets); + } + operation ApplyOuter_Empty__closure__closure_(count : Int, controls : Qubit[], targets : Qubit[], __capture_0 : Int, __capture_1 : Int) : Unit { + ApplyInner_Empty__closure__closure_(count, controls, targets, __capture_0, __capture_1); + } + operation ApplyInner_Empty__closure__closure_(count : Int, controls : Qubit[], targets : Qubit[], __capture_0 : Int, __capture_1 : Int) : Unit { + { + let _range_id_183 : Range = 0..count - 1; + mutable _index_id_186 : Int = _range_id_183::Start; + let _step_id_191 : Int = _range_id_183::Step; + let _end_id_196 : Int = _range_id_183::End; + while _step_id_191 > 0 and _index_id_186 <= _end_id_196 or _step_id_191 < 0 and _index_id_186 >= _end_id_196 { + let idx : Int = _index_id_186; + if idx == 0 { + _lambda_5(__capture_0, (controls[idx], targets)) + } else { + _lambda_6(__capture_1, (controls[idx], targets)) + }; + _index_id_186 += _step_id_191; + } + + } + + } + // entry + Main() + "#]], + ); +} + +/// A closure callable-array forwarded across two higher-order levels and fully +/// consumed by the innermost indexed dispatch leaves the source-array local +/// dead in the reachable caller. Because closure cleanup blanks each element to +/// unit, the surviving array binding would be an arrow-typed block with a unit +/// tail that trips the `PostDefunc` non-unit block-tail invariant. The dead +/// binding must be removed; this runs the invariant walk and full pipeline over +/// the same shape as +/// `two_level_cross_hof_closure_array_forwarding_threads_all_captures`. +#[test] +fn two_level_cross_hof_closure_array_forwarding_passes_invariants() { + let source = r#" + operation ApplyParityOperation(value : Int, control : Qubit, register : Qubit[]) : Unit { + if value == 1 { + Controlled X([control], register[0]); + } + } + + operation ApplyInner( + ops : ((Qubit, Qubit[]) => Unit)[], + count : Int, + controls : Qubit[], + targets : Qubit[] + ) : Unit { + for idx in 0..count - 1 { + ops[idx](controls[idx], targets); + } + } + + operation ApplyOuter( + ops : ((Qubit, Qubit[]) => Unit)[], + count : Int, + controls : Qubit[], + targets : Qubit[] + ) : Unit { + ApplyInner(ops, count, controls, targets); + } + + operation Main() : Unit { + use qs = Qubit[3]; + let controls = qs[0..1]; + let targets = qs[2...]; + let ops = [ApplyParityOperation(1, _, _), ApplyParityOperation(2, _, _)]; + ApplyOuter(ops, 2, controls, targets); + ResetAll(qs); + } + "#; + check_invariants(source); + check_pipeline(source); +} + /// Regression test for producer-body closure cleanup: a producer function /// that returns a partial-application closure causes convergence failure /// when the closure node survives in the producer body after HOF @@ -1148,6 +1401,62 @@ fn producer_body_closure_cleanup_converges() { ); } +#[test] +fn callable_returning_closure_with_controlled_callable_captures() { + let source = r#" + operation PrepareIdentity(qs : Qubit[]) : Unit is Adj + Ctl {} + + operation SelectIdentity(systems : Qubit[], ancilla : Qubit[]) : Unit is Adj + Ctl {} + + function MakeControlledPrepSelPrepOp( + prepareOp : Qubit[] => Unit is Adj + Ctl, + selectOp : (Qubit[], Qubit[]) => Unit is Adj + Ctl, + numSystemQubits : Int, + numAncillaQubits : Int, + power : Int + ) : (Qubit, Qubit[]) => Unit { + (control, allQubits) => { + let systems = allQubits[0..numSystemQubits - 1]; + let ancilla = allQubits[numSystemQubits...]; + for _ in 0..power - 1 { + Controlled prepareOp([control], systems); + Controlled selectOp([control], (systems, ancilla)); + } + } + } + + operation MakeControlledPrepSelPrepCircuit( + prepareOp : Qubit[] => Unit is Adj + Ctl, + selectOp : (Qubit[], Qubit[]) => Unit is Adj + Ctl, + numSystemQubits : Int, + numAncillaQubits : Int, + power : Int + ) : Unit { + use control = Qubit(); + use systems = Qubit[numSystemQubits + numAncillaQubits]; + let op = MakeControlledPrepSelPrepOp( + prepareOp, + selectOp, + numSystemQubits, + numAncillaQubits, + power + ); + op(control, systems); + } + + operation Main() : Unit { + MakeControlledPrepSelPrepCircuit( + PrepareIdentity, + SelectIdentity, + 1, + 1, + 1 + ); + } + "#; + check_invariants(source); +} + /// Two callable arguments passed to a multi-parameter HOF: one partial /// application closure and one global callable. Both must survive cleanup /// because they are still live as call arguments. @@ -1218,14 +1527,6 @@ fn closure_in_active_call_arg_survives_cleanup() { f(q); g(q); } - operation Apply2_Empty__AdjCtl__closure_(g : (Qubit => Unit is Adj + Ctl), q : Qubit, __capture_0 : Bool) : Unit { - _lambda_4(__capture_0, q); - g(q); - } - operation Apply2_Empty__AdjCtl__X_(g : (Qubit => Unit is Adj + Ctl), q : Qubit) : Unit { - X(q); - g(q); - } operation Apply2_Empty__AdjCtl__closure__X_(q : Qubit, __capture_0 : Bool) : Unit { _lambda_4(__capture_0, q); X(q); @@ -1236,6 +1537,52 @@ fn closure_in_active_call_arg_survives_cleanup() { ); } +#[test] +fn captured_closure_forwarded_to_nested_hof_converges() { + let source = r#" + operation ApplySequential(first : Qubit[] => Unit, second : Qubit[] => Unit, systems : Qubit[]) : Unit { + first(systems); + second(systems); + } + + operation ApplyFirstStep(systems : Qubit[]) : Unit { + for q in systems { + H(q); + } + } + + operation ApplySecondStep(systems : Qubit[]) : Unit { + for q in systems { + X(q); + } + } + + operation ApplyThirdStep(systems : Qubit[]) : Unit { + for q in systems { + Z(q); + } + } + + operation Main() : Unit { + use systems = Qubit[2]; + let sequential = ApplySequential(ApplyFirstStep, ApplySecondStep, _); + ApplySequential(sequential, ApplyThirdStep, systems); + } + "#; + check_invariants(source); + check( + source, + &expect![[r#" + .lambda_6{ApplyFirstStep}{ApplySecondStep}: input_ty=(Qubit)[] + ApplyFirstStep: input_ty=(Qubit)[] + ApplySecondStep: input_ty=(Qubit)[] + ApplySequential{ApplyFirstStep}{ApplySecondStep}: input_ty=(Qubit)[] + ApplySequential{closure}{ApplyThirdStep}{ApplyFirstStep}{ApplySecondStep}: input_ty=(Qubit)[] + ApplyThirdStep: input_ty=(Qubit)[] + Main: input_ty=Unit"#]], + ); +} + /// Regression: when a callable's entire input is a single closure-valued /// parameter and the passed closure captures exactly one variable, the /// specialized callee's input must be flattened to a scalar (the single @@ -1417,33 +1764,9 @@ fn progress_tracking_allows_multi_iteration_convergence() { operation L1_Empty_(op : (Qubit => Unit), q : Qubit) : Unit { op(q); } - operation L3_Empty__Empty__AdjCtl__L2_Empty__Empty__(inner : (((Qubit => Unit), Qubit) => Unit), op : (Qubit => Unit is Adj + Ctl), q : Qubit) : Unit { - L2_Empty__Empty_(inner, op, q); - } - operation L3_Empty__Empty__AdjCtl__L1_Empty__(inner : (((Qubit => Unit), Qubit) => Unit), op : (Qubit => Unit is Adj + Ctl), q : Qubit) : Unit { - L1_Empty_(inner, op, q); - } - operation L3_Empty__Empty__AdjCtl__H_(inner : (((Qubit => Unit), Qubit) => Unit), op : (Qubit => Unit is Adj + Ctl), q : Qubit) : Unit { - H(inner, op, q); - } - operation L3_Empty__Empty__AdjCtl__L2_Empty__Empty___L1_Empty__(op : (Qubit => Unit is Adj + Ctl), q : Qubit) : Unit { - L2_Empty__Empty__L1_Empty__(op, q); - } - operation L3_Empty__Empty__AdjCtl__L2_Empty__Empty___H_(op : (Qubit => Unit is Adj + Ctl), q : Qubit) : Unit { - L2_Empty__Empty_(H, op, q); - } - operation L2_Empty__Empty__L1_Empty__(op : (Qubit => Unit), q : Qubit) : Unit { - L1_Empty_(op, q); - } operation L3_Empty__Empty__AdjCtl__L2_Empty__Empty___L1_Empty___H_(q : Qubit) : Unit { L2_Empty__Empty__L1_Empty___H_(q); } - operation L2_Empty__Empty__L1_Empty__(op : (Qubit => Unit), q : Qubit) : Unit { - L1_Empty_(op, q); - } - operation L2_Empty__Empty__H_(op : (Qubit => Unit), q : Qubit) : Unit { - H(op, q); - } operation L2_Empty__Empty__L1_Empty___H_(q : Qubit) : Unit { L1_Empty__H_(q); } @@ -2406,3 +2729,125 @@ fn defunc_21_level_hof_returns_static_resolution_error() { errors.iter().map(ToString::to_string).collect::>() ); } + +#[test] +fn multiple_forwarded_callable_arrays_return_unsupported_error() { + // Forwarding two or more distinct callable arrays through a single HOF call + // is a shape the transform does not support. This test pins the diagnostic + // so a future change cannot silently start generating incorrect code for + // it. + // + // A two-level HOF forwards two distinct arrays of callables through one + // call. The transform must report exactly one + // `UnsupportedMultipleCallableArrays` diagnostic. It must not fall through + // to the per-row path, which would collapse each array to a single member, + // nor report a spurious `FixpointNotReached`. + let source = r#" + operation ApplyTwoArrays( + firstOps : (Qubit => Unit)[], + secondOps : (Qubit => Unit)[], + q : Qubit + ) : Unit { + for op in firstOps { + op(q); + } + for op in secondOps { + op(q); + } + } + operation ForwardTwoArrays( + firstOps : (Qubit => Unit)[], + secondOps : (Qubit => Unit)[], + q : Qubit + ) : Unit { + ApplyTwoArrays(firstOps, secondOps, q); + } + @EntryPoint() + operation Main() : Unit { + use q = Qubit(); + ForwardTwoArrays([X, Y], [Z, H], q); + } + "#; + + let (mut store, package_id) = compile_to_monomorphized_fir(source); + let mut assigners = PackageAssigners::new(&store, package_id); + let errors = defunctionalize(&mut store, package_id, &mut assigners); + + assert!( + matches!( + errors.as_slice(), + [super::super::Error::UnsupportedMultipleCallableArrays(_)] + ), + "expected exactly one UnsupportedMultipleCallableArrays error, got: {}", + format_defunctionalization_errors(&errors) + ); +} + +#[test] +fn operation_computed_captured_field_declines_to_dynamic_callable() { + // A captured struct field whose value is computed by an operation call + // cannot be specialized. Rebuilding the captured literal in the caller would + // duplicate and reorder that operation call, which is unsound for a call + // with quantum side effects because it cannot be run twice or moved. The + // transform therefore declines the closure to a dynamic call site and + // reports a recoverable `DynamicCallable` diagnostic. On the base profile + // this surfaces as a hard error rather than silently incorrect code. + check_errors( + r#" + struct Wrapper { Op : Qubit => Unit } + operation Choose(flag : Result) : (Qubit => Unit) { + return flag == One ? X | H; + } + operation ApplyWrapped(w : Wrapper, q : Qubit) : Unit { + w.Op(q); + } + operation MakeWrapper(q : Qubit) : Wrapper { + new Wrapper { Op = Choose(MResetZ(q)) } + } + @EntryPoint() + operation Main() : Unit { + use q = Qubit(); + let w = MakeWrapper(q); + ApplyWrapped(w, q); + } + "#, + &expect!["callable argument could not be resolved statically"], + ); +} + +#[test] +fn operation_call_in_captured_compound_literal_without_locals_declines_to_dynamic_callable() { + // A closure returned from `MakeOp` captures a `Payload` struct literal whose + // field is initialized by a runtime operation call (`ReadValue()`), with no + // intervening local binding to anchor that call. The captured value is not a + // statically-known callable, so defunctionalization must decline the + // `ApplyOp(MakeOp(), q)` call site to a dynamic callable — emitting the + // "callable argument could not be resolved statically" diagnostic — rather than + // attempt to specialize the unresolved compound-literal capture. + check_errors( + r#" + struct Payload { Value : Int } + operation ReadValue() : Int { + return 1; + } + operation UsePayload(payload : Payload, q : Qubit) : Unit { + if payload.Value == 1 { + H(q); + } + } + operation ApplyOp(op : Qubit => Unit, q : Qubit) : Unit { + op(q); + } + operation MakeOp() : Qubit => Unit { + let payload = new Payload { Value = ReadValue() }; + return q => UsePayload(payload, q); + } + @EntryPoint() + operation Main() : Unit { + use q = Qubit(); + ApplyOp(MakeOp(), q); + } + "#, + &expect!["callable argument could not be resolved statically"], + ); +} diff --git a/source/compiler/qsc_fir_transforms/src/defunctionalize/tests/invariants.rs b/source/compiler/qsc_fir_transforms/src/defunctionalize/tests/invariants.rs index 920518ae22d..b74bb87bbc9 100644 --- a/source/compiler/qsc_fir_transforms/src/defunctionalize/tests/invariants.rs +++ b/source/compiler/qsc_fir_transforms/src/defunctionalize/tests/invariants.rs @@ -911,47 +911,60 @@ fn five_branch_conditional_callable_resolves_successfully() { ); } +/// A statically-known callable array with more than `MULTI_CAP` (1000) distinct +/// elements exceeds the per-set candidate bound during indexed-dispatch +/// resolution, so the analysis widens to `Dynamic` (top of the lattice) instead +/// of building a per-index dispatch chain. The higher-order `Apply(op, q)` call +/// over the loop element then surfaces the actionable `DynamicCallable` +/// diagnostic. Arrays at or below the cap resolve to a per-index dispatch +/// instead, so exercising the widen-to-`Dynamic` path requires more than 1000 +/// distinct elements. A flat array literal is used (rather than a deeply nested +/// `if`/`elif` chain) to avoid overflowing the parser stack at this width. #[test] -fn nine_branch_conditional_callable_degrades_to_dynamic() { - check_errors( +fn callable_array_exceeding_multi_cap_degrades_to_dynamic() { + use std::fmt::Write as _; + + // One distinct callable per element; more than `MULTI_CAP` (1000) elements + // forces indexed-dispatch resolution to widen the candidate set to + // `Dynamic`. + const ELEMENTS: usize = 1001; + + let mut defs = String::new(); + let mut elems = String::new(); + for i in 0..ELEMENTS { + writeln!(defs, " operation Op{i}(q : Qubit) : Unit {{}}").expect("write succeeds"); + if i > 0 { + elems.push_str(", "); + } + write!(elems, "Op{i}").expect("write succeeds"); + } + + let source = format!( r#" - operation Apply(op : Qubit => Unit, q : Qubit) : Unit { +{defs} + operation Apply(op : Qubit => Unit, q : Qubit) : Unit {{ op(q); - } + }} - operation Main() : Unit { + operation Main() : Unit {{ use q = Qubit(); - let n = 2; - mutable op = H; - if n == 0 { - op = X; - } elif n == 1 { - op = Y; - } elif n == 2 { - op = Z; - } elif n == 3 { - op = S; - } elif n == 4 { - op = T; - } elif n == 5 { - op = Rx(0.0, _); - } elif n == 6 { - op = Ry(0.0, _); - } elif n == 7 { - op = Rz(0.0, _); - } else { - op = SWAP(_, q); - } - Apply(op, q); - } - "#, + let ops = [{elems}]; + for op in ops {{ + Apply(op, q); + }} + }} + "# + ); + + check_errors( + &source, &expect!["callable argument could not be resolved statically"], ); } -/// Direct-path mirror of `nine_branch_conditional_callable_degrades_to_dynamic`: -/// a direct (non-HOF) call `f(q)` whose callee `f` is forced to `Dynamic` by a -/// loop reassignment now surfaces the actionable `DynamicCallable` diagnostic +/// A direct (non-HOF) call `f(q)` whose callee `f` is forced to `Dynamic` by a +/// loop reassignment (loop reassignment is treated as unresolvable regardless +/// of the candidate count) surfaces the actionable `DynamicCallable` diagnostic /// at the call site, rather than only the less-specific `FixpointNotReached`. #[test] fn direct_call_unresolvable_callable_emits_dynamic_callable_diagnostic() { @@ -972,42 +985,49 @@ fn direct_call_unresolvable_callable_emits_dynamic_callable_diagnostic() { ); } -/// Direct-path mirror of `nine_branch_conditional_callable_degrades_to_dynamic`: -/// a direct (non-HOF) call `op(q)` whose local callee accumulates more than -/// `MULTI_CAP` (8) distinct callables via a nested conditional. The -/// `CalleeLattice::Multi` join saturates to `Dynamic` rather than panicking or -/// silently dropping branches, and the direct call surfaces the actionable -/// `DynamicCallable` diagnostic. +/// Direct-path (non-HOF) analogue of +/// `callable_array_exceeding_multi_cap_degrades_to_dynamic`: a statically-known +/// callable array with more than `MULTI_CAP` (1000) distinct elements exceeds +/// the per-set candidate bound during indexed-dispatch resolution, so the +/// analysis widens to `Dynamic` instead of building a per-index dispatch chain. +/// The direct call `op(q)` over the loop element then surfaces the actionable +/// `DynamicCallable` diagnostic. A flat array literal is used (rather than a +/// deeply nested `if`/`elif` chain) to avoid overflowing the parser stack at +/// this width. #[test] -fn direct_nine_branch_conditional_callable_degrades_to_dynamic() { - check_errors( +fn direct_callable_array_exceeding_multi_cap_degrades_to_dynamic() { + use std::fmt::Write as _; + + // One distinct callable per element; more than `MULTI_CAP` (1000) elements + // forces indexed-dispatch resolution to widen the candidate set to + // `Dynamic`. + const ELEMENTS: usize = 1001; + + let mut defs = String::new(); + let mut elems = String::new(); + for i in 0..ELEMENTS { + writeln!(defs, " operation Op{i}(q : Qubit) : Unit {{}}").expect("write succeeds"); + if i > 0 { + elems.push_str(", "); + } + write!(elems, "Op{i}").expect("write succeeds"); + } + + let source = format!( r#" - operation Main() : Unit { +{defs} + operation Main() : Unit {{ use q = Qubit(); - let n = 2; - mutable op = H; - if n == 0 { - op = X; - } elif n == 1 { - op = Y; - } elif n == 2 { - op = Z; - } elif n == 3 { - op = S; - } elif n == 4 { - op = T; - } elif n == 5 { - op = Rx(0.0, _); - } elif n == 6 { - op = Ry(0.0, _); - } elif n == 7 { - op = Rz(0.0, _); - } else { - op = SWAP(_, q); - } - op(q); - } - "#, + let ops = [{elems}]; + for op in ops {{ + op(q); + }} + }} + "# + ); + + check_errors( + &source, &expect!["callable argument could not be resolved statically"], ); } @@ -1210,3 +1230,513 @@ fn newtype_ctor_callable_field_cleanup() { "#]], ); } + +// A select-style operation whose first parameter is a struct (UDT) is +// partially applied into a closure, then forwarded as `selectOp` through a +// factory that dispatches it via `Controlled selectOp([control], (systems, +// ancilla))`. Specialization must thread the captured struct through the +// controlled-dispatch layer so the rewritten call reads +// `Controlled _lambda_8([control], (__capture_0, (systems, ancilla)))` rather +// than dropping the capture and passing `(systems, ancilla)` directly. Dropping +// the capture would leave the call shape inconsistent with the specialized +// callee's input and trip the post-arg_promote call-shape invariant. This test +// drives the full pipeline (`check_pipeline`) so the shape is validated through +// argument promotion, and pairs it with a rewrite snapshot showing the threaded +// struct capture. It exercises the controlled struct-capture-threading path via +// the source `Main` entry; it does not reproduce the injected-closure entry +// rooting that a compiled-from-Python entry expression would produce. +#[test] +fn struct_capture_select_op_threads_through_controlled_dispatch_pipeline() { + let source = r#" + struct PauliSelectParams { + paulis : Pauli[][], + qubitIndices : Int[], + signs : Int[] + } + + operation ApplySelect(params : PauliSelectParams, systems : Qubit[], ancilla : Qubit[]) : Unit is Adj + Ctl { + if Length(params.signs) != 0 { + X(systems[0]); + } + } + + operation ApplyPrepare(systems : Qubit[]) : Unit is Adj + Ctl {} + + function MakeControlledPrepSelPrepOp( + prepareOp : Qubit[] => Unit is Adj + Ctl, + selectOp : (Qubit[], Qubit[]) => Unit is Adj + Ctl, + numSystemQubits : Int, + power : Int + ) : (Qubit, Qubit[]) => Unit { + (control, allQubits) => { + let systems = allQubits[0..numSystemQubits - 1]; + let ancilla = allQubits[numSystemQubits...]; + for _ in 0..power - 1 { + Controlled prepareOp([control], systems); + Controlled selectOp([control], (systems, ancilla)); + } + } + } + + operation MakeControlledPrepSelPrepCircuit( + prepareOp : Qubit[] => Unit is Adj + Ctl, + selectOp : (Qubit[], Qubit[]) => Unit is Adj + Ctl, + numSystemQubits : Int, + power : Int + ) : Unit { + use control = Qubit(); + use systems = Qubit[numSystemQubits + 1]; + let op = MakeControlledPrepSelPrepOp(prepareOp, selectOp, numSystemQubits, power); + op(control, systems); + } + + operation Main() : Unit { + let params = new PauliSelectParams { + paulis = [[PauliX]], + qubitIndices = [0], + signs = [1] + }; + let sel = ApplySelect(params, _, _); + MakeControlledPrepSelPrepCircuit(ApplyPrepare, sel, 1, 1); + } + "#; + check_pipeline(source); + check_rewrite( + source, + &expect![[r#" + BEFORE: + newtype PauliSelectParams = (Pauli[][], Int[], Int[]); + operation ApplySelect(params : __UDT_Item_1__Package_2_, systems : Qubit[], ancilla : Qubit[]) : Unit is Adj + Ctl { + body ... { + if Length(params::signs) != 0 { + X(systems[0]); + } + + } + adjoint ... { + if Length(params::signs) != 0 { + Adjoint X(systems[0]); + } + + } + controlled (ctls, ...) { + if Length(params::signs) != 0 { + Controlled X(ctls, systems[0]); + } + + } + controlled adjoint (ctls, ...) { + if Length(params::signs) != 0 { + Controlled Adjoint X(ctls, systems[0]); + } + + } + } + operation ApplyPrepare(systems : Qubit[]) : Unit is Adj + Ctl { + body ... {} + adjoint ... {} + controlled (ctls, ...) {} + controlled adjoint (ctls, ...) {} + } + function MakeControlledPrepSelPrepOp(prepareOp : (Qubit[] => Unit), selectOp : ((Qubit[], Qubit[]) => Unit), numSystemQubits : Int, power : Int) : ((Qubit, Qubit[]) => Unit) { + / * closure item = 7 captures = [prepareOp, selectOp, numSystemQubits, power] * / _lambda_7 + } + operation MakeControlledPrepSelPrepCircuit(prepareOp : (Qubit[] => Unit), selectOp : ((Qubit[], Qubit[]) => Unit), numSystemQubits : Int, power : Int) : Unit { + let control : Qubit = __quantum__rt__qubit_allocate(); + let systems : Qubit[] = AllocateQubitArray(numSystemQubits + 1); + let op : ((Qubit, Qubit[]) => Unit) = MakeControlledPrepSelPrepOp_AdjCtl__AdjCtl_(prepareOp, selectOp, numSystemQubits, power); + op(control, systems); + ReleaseQubitArray(systems); + __quantum__rt__qubit_release(control); + } + operation Main() : Unit { + let params : __UDT_Item_1__Package_2_ = new PauliSelectParams { + paulis = [[PauliX]], + qubitIndices = [0], + signs = [1] + }; + let sel : ((Qubit[], Qubit[]) => Unit is Adj + Ctl) = { + let arg : __UDT_Item_1__Package_2_ = params; + / * closure item = 8 captures = [arg] * / _lambda_8 + }; + MakeControlledPrepSelPrepCircuit_AdjCtl__AdjCtl_(ApplyPrepare, sel, 1, 1); + } + operation _lambda_7(prepareOp : (Qubit[] => Unit), selectOp : ((Qubit[], Qubit[]) => Unit), numSystemQubits : Int, power : Int, (control : Qubit, allQubits : Qubit[])) : Unit { + { + let systems : Qubit[] = allQubits[0..numSystemQubits - 1]; + let ancilla : Qubit[] = allQubits[numSystemQubits...]; + { + let _range_id_346 : Range = 0..power - 1; + mutable _index_id_349 : Int = _range_id_346::Start; + let _step_id_354 : Int = _range_id_346::Step; + let _end_id_359 : Int = _range_id_346::End; + while _step_id_354 > 0 and _index_id_349 <= _end_id_359 or _step_id_354 < 0 and _index_id_349 >= _end_id_359 { + let _ : Int = _index_id_349; + Controlled prepareOp([control], systems); + Controlled selectOp([control], (systems, ancilla)); + _index_id_349 += _step_id_354; + } + + } + + } + + } + operation _lambda_8(arg : __UDT_Item_1__Package_2_, (hole : Qubit[], hole : Qubit[])) : Unit is Adj + Ctl { + body ... { + ApplySelect(arg, hole, hole) + } + adjoint ... { + Adjoint ApplySelect(arg, hole, hole) + } + controlled (ctls, ...) { + Controlled ApplySelect(ctls, (arg, hole, hole)) + } + controlled adjoint (ctls, ...) { + Controlled Adjoint ApplySelect(ctls, (arg, hole, hole)) + } + } + function MakeControlledPrepSelPrepOp_AdjCtl__AdjCtl_(prepareOp : (Qubit[] => Unit is Adj + Ctl), selectOp : ((Qubit[], Qubit[]) => Unit is Adj + Ctl), numSystemQubits : Int, power : Int) : ((Qubit, Qubit[]) => Unit) { + / * closure item = 10 captures = [prepareOp, selectOp, numSystemQubits, power] * / _lambda_7 + } + operation _lambda_7(prepareOp : (Qubit[] => Unit is Adj + Ctl), selectOp : ((Qubit[], Qubit[]) => Unit is Adj + Ctl), numSystemQubits : Int, power : Int, (control : Qubit, allQubits : Qubit[])) : Unit { + { + let systems : Qubit[] = allQubits[0..numSystemQubits - 1]; + let ancilla : Qubit[] = allQubits[numSystemQubits...]; + { + let _range_id_346 : Range = 0..power - 1; + mutable _index_id_349 : Int = _range_id_346::Start; + let _step_id_354 : Int = _range_id_346::Step; + let _end_id_359 : Int = _range_id_346::End; + while _step_id_354 > 0 and _index_id_349 <= _end_id_359 or _step_id_354 < 0 and _index_id_349 >= _end_id_359 { + let _ : Int = _index_id_349; + Controlled prepareOp([control], systems); + Controlled selectOp([control], (systems, ancilla)); + _index_id_349 += _step_id_354; + } + + } + + } + + } + operation MakeControlledPrepSelPrepCircuit_AdjCtl__AdjCtl_(prepareOp : (Qubit[] => Unit is Adj + Ctl), selectOp : ((Qubit[], Qubit[]) => Unit is Adj + Ctl), numSystemQubits : Int, power : Int) : Unit { + let control : Qubit = __quantum__rt__qubit_allocate(); + let systems : Qubit[] = AllocateQubitArray(numSystemQubits + 1); + let op : ((Qubit, Qubit[]) => Unit) = MakeControlledPrepSelPrepOp_AdjCtl__AdjCtl_(prepareOp, selectOp, numSystemQubits, power); + op(control, systems); + ReleaseQubitArray(systems); + __quantum__rt__qubit_release(control); + } + // entry + Main() + + AFTER: + newtype PauliSelectParams = (Pauli[][], Int[], Int[]); + operation ApplySelect(params : __UDT_Item_1__Package_2_, systems : Qubit[], ancilla : Qubit[]) : Unit is Adj + Ctl { + body ... { + if Length(params::signs) != 0 { + X(systems[0]); + } + + } + adjoint ... { + if Length(params::signs) != 0 { + Adjoint X(systems[0]); + } + + } + controlled (ctls, ...) { + if Length(params::signs) != 0 { + Controlled X(ctls, systems[0]); + } + + } + controlled adjoint (ctls, ...) { + if Length(params::signs) != 0 { + Controlled Adjoint X(ctls, systems[0]); + } + + } + } + operation ApplyPrepare(systems : Qubit[]) : Unit is Adj + Ctl { + body ... {} + adjoint ... {} + controlled (ctls, ...) {} + controlled adjoint (ctls, ...) {} + } + function MakeControlledPrepSelPrepOp(prepareOp : (Qubit[] => Unit), selectOp : ((Qubit[], Qubit[]) => Unit), numSystemQubits : Int, power : Int) : ((Qubit, Qubit[]) => Unit) { + / * closure item = 7 captures = [prepareOp, selectOp, numSystemQubits, power] * / _lambda_7 + } + operation MakeControlledPrepSelPrepCircuit(prepareOp : (Qubit[] => Unit), selectOp : ((Qubit[], Qubit[]) => Unit), numSystemQubits : Int, power : Int) : Unit { + let control : Qubit = __quantum__rt__qubit_allocate(); + let systems : Qubit[] = AllocateQubitArray(numSystemQubits + 1); + let op : ((Qubit, Qubit[]) => Unit) = MakeControlledPrepSelPrepOp_AdjCtl__AdjCtl_(prepareOp, selectOp, numSystemQubits, power); + op(control, systems); + ReleaseQubitArray(systems); + __quantum__rt__qubit_release(control); + } + operation Main() : Unit { + let params : __UDT_Item_1__Package_2_ = new PauliSelectParams { + paulis = [[PauliX]], + qubitIndices = [0], + signs = [1] + }; + MakeControlledPrepSelPrepCircuit_AdjCtl__AdjCtl__ApplyPrepare__closure_(1, 1, params); + } + operation _lambda_7(prepareOp : (Qubit[] => Unit), selectOp : ((Qubit[], Qubit[]) => Unit), numSystemQubits : Int, power : Int, (control : Qubit, allQubits : Qubit[])) : Unit { + { + let systems : Qubit[] = allQubits[0..numSystemQubits - 1]; + let ancilla : Qubit[] = allQubits[numSystemQubits...]; + { + let _range_id_346 : Range = 0..power - 1; + mutable _index_id_349 : Int = _range_id_346::Start; + let _step_id_354 : Int = _range_id_346::Step; + let _end_id_359 : Int = _range_id_346::End; + while _step_id_354 > 0 and _index_id_349 <= _end_id_359 or _step_id_354 < 0 and _index_id_349 >= _end_id_359 { + let _ : Int = _index_id_349; + Controlled prepareOp([control], systems); + Controlled selectOp([control], (systems, ancilla)); + _index_id_349 += _step_id_354; + } + + } + + } + + } + operation _lambda_8(arg : __UDT_Item_1__Package_2_, (hole : Qubit[], hole : Qubit[])) : Unit is Adj + Ctl { + body ... { + ApplySelect(arg, hole, hole) + } + adjoint ... { + Adjoint ApplySelect(arg, hole, hole) + } + controlled (ctls, ...) { + Controlled ApplySelect(ctls, (arg, hole, hole)) + } + controlled adjoint (ctls, ...) { + Controlled Adjoint ApplySelect(ctls, (arg, hole, hole)) + } + } + function MakeControlledPrepSelPrepOp_AdjCtl__AdjCtl_(prepareOp : (Qubit[] => Unit is Adj + Ctl), selectOp : ((Qubit[], Qubit[]) => Unit is Adj + Ctl), numSystemQubits : Int, power : Int) : ((Qubit, Qubit[]) => Unit) { + () + } + operation _lambda_7(prepareOp : (Qubit[] => Unit is Adj + Ctl), selectOp : ((Qubit[], Qubit[]) => Unit is Adj + Ctl), numSystemQubits : Int, power : Int, (control : Qubit, allQubits : Qubit[])) : Unit { + { + let systems : Qubit[] = allQubits[0..numSystemQubits - 1]; + let ancilla : Qubit[] = allQubits[numSystemQubits...]; + { + let _range_id_346 : Range = 0..power - 1; + mutable _index_id_349 : Int = _range_id_346::Start; + let _step_id_354 : Int = _range_id_346::Step; + let _end_id_359 : Int = _range_id_346::End; + while _step_id_354 > 0 and _index_id_349 <= _end_id_359 or _step_id_354 < 0 and _index_id_349 >= _end_id_359 { + let _ : Int = _index_id_349; + Controlled prepareOp([control], systems); + Controlled selectOp([control], (systems, ancilla)); + _index_id_349 += _step_id_354; + } + + } + + } + + } + operation MakeControlledPrepSelPrepCircuit_AdjCtl__AdjCtl_(prepareOp : (Qubit[] => Unit is Adj + Ctl), selectOp : ((Qubit[], Qubit[]) => Unit is Adj + Ctl), numSystemQubits : Int, power : Int) : Unit { + let control : Qubit = __quantum__rt__qubit_allocate(); + let systems : Qubit[] = AllocateQubitArray(numSystemQubits + 1); + _lambda_7(prepareOp, selectOp, numSystemQubits, power, (control, systems)); + ReleaseQubitArray(systems); + __quantum__rt__qubit_release(control); + } + operation MakeControlledPrepSelPrepCircuit_AdjCtl__AdjCtl__ApplyPrepare__closure_(numSystemQubits : Int, power : Int, __capture_0 : __UDT_Item_1__Package_2_) : Unit { + let control : Qubit = __quantum__rt__qubit_allocate(); + let systems : Qubit[] = AllocateQubitArray(numSystemQubits + 1); + _lambda_7_ApplyPrepare__closure_(numSystemQubits, power, (control, systems), __capture_0); + ReleaseQubitArray(systems); + __quantum__rt__qubit_release(control); + } + function MakeControlledPrepSelPrepOp_AdjCtl__AdjCtl__ApplyPrepare__closure_(numSystemQubits : Int, power : Int, __capture_0 : __UDT_Item_1__Package_2_) : ((Qubit, Qubit[]) => Unit) { + / * closure item = 14 captures = [numSystemQubits, power] * / _lambda_7 + } + operation _lambda_7(numSystemQubits : Int, power : Int, (control : Qubit, allQubits : Qubit[])) : Unit { + { + let systems : Qubit[] = allQubits[0..numSystemQubits - 1]; + let ancilla : Qubit[] = allQubits[numSystemQubits...]; + { + let _range_id_346 : Range = 0..power - 1; + mutable _index_id_349 : Int = _range_id_346::Start; + let _step_id_354 : Int = _range_id_346::Step; + let _end_id_359 : Int = _range_id_346::End; + while _step_id_354 > 0 and _index_id_349 <= _end_id_359 or _step_id_354 < 0 and _index_id_349 >= _end_id_359 { + let _ : Int = _index_id_349; + Controlled ApplyPrepare([control], systems); + Controlled _lambda_8([control], (systems, ancilla)); + _index_id_349 += _step_id_354; + } + + } + + } + + } + operation _lambda_7_ApplyPrepare__closure_(numSystemQubits : Int, power : Int, (control : Qubit, allQubits : Qubit[]), __capture_0 : __UDT_Item_1__Package_2_) : Unit { + { + let systems : Qubit[] = allQubits[0..numSystemQubits - 1]; + let ancilla : Qubit[] = allQubits[numSystemQubits...]; + { + let _range_id_346 : Range = 0..power - 1; + mutable _index_id_349 : Int = _range_id_346::Start; + let _step_id_354 : Int = _range_id_346::Step; + let _end_id_359 : Int = _range_id_346::End; + while _step_id_354 > 0 and _index_id_349 <= _end_id_359 or _step_id_354 < 0 and _index_id_349 >= _end_id_359 { + let _ : Int = _index_id_349; + Controlled ApplyPrepare([control], systems); + Controlled _lambda_8([control], (__capture_0, (systems, ancilla))); + _index_id_349 += _step_id_354; + } + + } + + } + + } + // entry + Main() + "#]], + ); +} + +// A direct (non-higher-order) call over a callable local that mixes an +// intrinsic candidate with a partial application dispatches over both +// candidates. The partial application `Rx(0.0, _)` lowers to a closure-tailed +// block bound to `op`; once `op(q)` becomes a branch dispatch, that binding is +// dead. Removing the dead binding keeps closure cleanup from stranding an +// arrow-typed block with no producing tail, which would otherwise violate the +// non-Unit block-tail invariant. This pairs the invariant check with a rewrite +// snapshot showing the dead `op` binding removed and the dispatch inlining the +// specialized callees. +#[test] +fn width2_mixed_direct_dispatch_removes_dead_partial_app_binding() { + let source = r#" + operation Main() : Unit { + use q = Qubit(); + let n = 1; + mutable op = X; + if n == 0 { + op = Rx(0.0, _); + } + op(q); + } + "#; + check_invariants(source); + check_rewrite( + source, + &expect![[r#" + BEFORE: + operation Main() : Unit { + let q : Qubit = __quantum__rt__qubit_allocate(); + let n : Int = 1; + mutable op : (Qubit => Unit is Adj + Ctl) = X; + if n == 0 { + op = { + let arg : Double = 0.; + / * closure item = 2 captures = [arg] * / _lambda_2 + }; + } + + op(q); + __quantum__rt__qubit_release(q); + } + operation _lambda_2(arg : Double, hole : Qubit) : Unit is Adj + Ctl { + body ... { + Rx(arg, hole) + } + adjoint ... { + Adjoint Rx(arg, hole) + } + controlled (ctls, ...) { + Controlled Rx(ctls, (arg, hole)) + } + controlled adjoint (ctls, ...) { + Controlled Adjoint Rx(ctls, (arg, hole)) + } + } + // entry + Main() + + AFTER: + operation Main() : Unit { + let q : Qubit = __quantum__rt__qubit_allocate(); + let n : Int = 1; + if n == 0 {} + + if n == 0 { + _lambda_2(0., q) + } else { + X(q) + }; + __quantum__rt__qubit_release(q); + } + operation _lambda_2(arg : Double, hole : Qubit) : Unit is Adj + Ctl { + body ... { + Rx(arg, hole) + } + adjoint ... { + Adjoint Rx(arg, hole) + } + controlled (ctls, ...) { + Controlled Rx(ctls, (arg, hole)) + } + controlled adjoint (ctls, ...) { + Controlled Adjoint Rx(ctls, (arg, hole)) + } + } + // entry + Main() + "#]], + ); +} + +// Indexed-array analogue of the mixed direct dispatch: a callable-array literal +// mixing an intrinsic with a partial application is indexed inside a loop and +// dispatched directly. Once the indexed read is rewritten into a branch +// dispatch, both the indexed local and the now-dead source array (whose element +// holds a closure) are removed, so no arrow-typed block with a blanked tail +// remains. +#[test] +fn indexed_callable_array_mixed_direct_dispatch_passes_invariants() { + let source = r#" + operation Main() : Unit { + use q = Qubit(); + let ops = [X, Rx(0.0, _)]; + for i in 0..1 { + let op = ops[i]; + op(q); + } + } + "#; + check_invariants(source); +} + +// Pure partial-application direct dispatch: both candidates are partial +// applications, confirming the fix keys on a consumed partial-application +// residual in a reachable block rather than on a mixed candidate set. The +// write-only `op` binding and its reassignment are removed once the dispatch +// consumes them. +#[test] +fn pure_partial_app_direct_dispatch_passes_invariants() { + let source = r#" + operation Main() : Unit { + use q = Qubit(); + let c = true; + mutable op = Rx(0.0, _); + if c { + op = Ry(0.0, _); + } + op(q); + } + "#; + check_invariants(source); +} diff --git a/source/compiler/qsc_fir_transforms/src/defunctionalize/tests/specialization.rs b/source/compiler/qsc_fir_transforms/src/defunctionalize/tests/specialization.rs index 863ea01f31a..561923d0e4f 100644 --- a/source/compiler/qsc_fir_transforms/src/defunctionalize/tests/specialization.rs +++ b/source/compiler/qsc_fir_transforms/src/defunctionalize/tests/specialization.rs @@ -323,6 +323,114 @@ fn specialize_closure_with_captures() { ); } +#[test] +fn closure_callable_capture_specializations_keep_distinct_callees() { + let source = r#" + operation ApplyOp(op : Qubit => Unit, q : Qubit) : Unit { + op(q); + } + + function Wrap(inner : Qubit => Unit) : Qubit => Unit { + q => inner(q) + } + + operation Main() : Unit { + use a = Qubit(); + use b = Qubit(); + ApplyOp(Wrap(H), a); + ApplyOp(Wrap(X), b); + } + "#; + let (fir_store, fir_pkg_id) = compile_and_defunctionalize(source); + let after = crate::pretty::write_package_qsharp_parseable(&fir_store, fir_pkg_id); + + check_rewrite( + source, + &expect![[r#" + BEFORE: + operation ApplyOp(op : (Qubit => Unit), q : Qubit) : Unit { + op(q); + } + function Wrap(inner : (Qubit => Unit)) : (Qubit => Unit) { + / * closure item = 4 captures = [inner] * / _lambda_4 + } + operation Main() : Unit { + let a : Qubit = __quantum__rt__qubit_allocate(); + let b : Qubit = __quantum__rt__qubit_allocate(); + ApplyOp_Empty_(Wrap_AdjCtl_(H), a); + ApplyOp_Empty_(Wrap_AdjCtl_(X), b); + __quantum__rt__qubit_release(b); + __quantum__rt__qubit_release(a); + } + operation _lambda_4(inner : (Qubit => Unit), q : Qubit) : Unit { + inner(q) + } + operation ApplyOp_Empty_(op : (Qubit => Unit), q : Qubit) : Unit { + op(q); + } + function Wrap_AdjCtl_(inner : (Qubit => Unit is Adj + Ctl)) : (Qubit => Unit) { + / * closure item = 7 captures = [inner] * / _lambda_4 + } + operation _lambda_4(inner : (Qubit => Unit is Adj + Ctl), q : Qubit) : Unit { + inner(q) + } + // entry + Main() + + AFTER: + operation ApplyOp(op : (Qubit => Unit), q : Qubit) : Unit { + op(q); + } + function Wrap(inner : (Qubit => Unit)) : (Qubit => Unit) { + / * closure item = 4 captures = [inner] * / _lambda_4 + } + operation Main() : Unit { + let a : Qubit = __quantum__rt__qubit_allocate(); + let b : Qubit = __quantum__rt__qubit_allocate(); + ApplyOp_Empty__H_(a); + ApplyOp_Empty__X_(b); + __quantum__rt__qubit_release(b); + __quantum__rt__qubit_release(a); + } + operation _lambda_4(inner : (Qubit => Unit), q : Qubit) : Unit { + inner(q) + } + operation ApplyOp_Empty_(op : (Qubit => Unit), q : Qubit) : Unit { + op(q); + } + function Wrap_AdjCtl_(inner : (Qubit => Unit is Adj + Ctl)) : (Qubit => Unit) { + inner + } + operation _lambda_4(inner : (Qubit => Unit is Adj + Ctl), q : Qubit) : Unit { + inner(q) + } + function Wrap_AdjCtl__H_() : (Qubit => Unit) { + H + } + operation ApplyOp_Empty__H_(q : Qubit) : Unit { + H(q); + } + function Wrap_AdjCtl__X_() : (Qubit => Unit) { + X + } + operation ApplyOp_Empty__X_(q : Qubit) : Unit { + X(q); + } + // entry + Main() + "#]], + ); + + assert!( + after.contains("H(q);"), + "Wrap(H) should specialize to a concrete H call:\n{after}" + ); + assert!( + after.contains("X(q);"), + "Wrap(X) should specialize to a concrete X call distinct from Wrap(H):\n{after}" + ); +} + #[test] fn specialize_closure_capture_types_preserved() { check_rewrite( @@ -1216,14 +1324,6 @@ fn multiple_callable_parameters_specialize_independently() { f(q); g(q); } - operation ApplyTwo_AdjCtl__AdjCtl__H_(g : (Qubit => Unit is Adj + Ctl), q : Qubit) : Unit { - H(q); - g(q); - } - operation ApplyTwo_AdjCtl__AdjCtl__X_(g : (Qubit => Unit is Adj + Ctl), q : Qubit) : Unit { - X(q); - g(q); - } operation ApplyTwo_AdjCtl__AdjCtl__H__X_(q : Qubit) : Unit { H(q); X(q); @@ -1234,18 +1334,23 @@ fn multiple_callable_parameters_specialize_independently() { ); } -/// Focused coverage for `reindex_sibling_field_access` with more than two -/// callable fields in a single tuple-typed parameter. +/// Three statically-resolved callable fields in a single tuple-typed parameter +/// are removed together in one combined specialization. /// -/// The two-callable-field case only exercises the `Collapse` arm (removing one -/// of two siblings leaves a single element, so the tuple slot collapses). With -/// three callable fields the first removal leaves a two-element tuple, so the -/// later siblings must be *reindexed* (shifted down by one) rather than -/// collapsed — that is the `Reindex` arm. A field-index mix-up here would emit -/// the per-field gates out of order or dispatch the wrong callable, so the -/// snapshot pins `First -> H`, `Second -> X`, `Third -> Y` in that order. -#[test] -fn three_callable_field_tuple_param_reindexes_siblings() { +/// When every field of the tuple-valued parameter is a concrete callable, the +/// group is combine-eligible per `super::is_combined_eligible`, so the whole +/// `ops` slot is dropped in a single pass rather than removed one field at a +/// time across iterations. The snapshot pins the single collapsed +/// specialization `RunOps_AdjCtl__AdjCtl__AdjCtl__H__X__Y_(q)` with the fields +/// inlined in order `First -> H`, `Second -> X`, `Third -> Y`; a field-index +/// mix-up would inline the gates out of order or dispatch the wrong callable. +/// +/// The per-field `reindex_sibling_field_access` path, which shifts surviving +/// siblings down as each is removed, is exercised only when the group is not +/// combine-eligible, for example when the tuple's fields are only partially +/// covered by concrete callables. +#[test] +fn three_callable_field_tuple_param_combines_into_one_spec() { check_rewrite( r#" operation RunOps(ops : (Qubit => Unit, Qubit => Unit, Qubit => Unit), q : Qubit) : Unit { @@ -1299,40 +1404,94 @@ fn three_callable_field_tuple_param_reindexes_siblings() { second(q); third(q); } - operation RunOps_AdjCtl__AdjCtl__AdjCtl__H_(ops : ((Qubit => Unit is Adj + Ctl), (Qubit => Unit is Adj + Ctl)), q : Qubit) : Unit { - let (second : (Qubit => Unit is Adj + Ctl), third : (Qubit => Unit is Adj + Ctl)) = ops; + operation RunOps_AdjCtl__AdjCtl__AdjCtl__H__X__Y_(q : Qubit) : Unit { H(q); + X(q); + Y(q); + } + // entry + Main() + "#]], + ); +} + +/// The combined rewrite reduces a non-inline argument: a single tuple-valued +/// parameter HOF called with a pre-bound tuple local such as `let ops = (H, X, +/// Y); RunOps(ops)` rather than an inline tuple literal. +/// +/// Because the argument is `Var(ops)`, the rewrite cannot drop tuple slots in +/// place; it projects the surviving slots through the local's initializer. Here +/// every field is a global callable removed together, so the reduced call takes +/// no arguments, the now-dead `let ops` binding is pruned, and the collapsed +/// specialization inlines `H, X, Y` in order. A projection error would leave the +/// arrow-typed `let ops` binding behind or pass a stale full-arity argument. +#[test] +fn bound_tuple_arg_combines_into_one_spec() { + check_rewrite( + r#" + operation RunOps(ops : (Qubit => Unit, Qubit => Unit, Qubit => Unit)) : Unit { + use q = Qubit(); + let (first, second, third) = ops; + first(q); + second(q); + third(q); + } + operation Main() : Unit { + let ops = (H, X, Y); + RunOps(ops); + } + "#, + &expect![[r#" + BEFORE: + operation RunOps(ops : ((Qubit => Unit), (Qubit => Unit), (Qubit => Unit))) : Unit { + let q : Qubit = __quantum__rt__qubit_allocate(); + let (first : (Qubit => Unit), second : (Qubit => Unit), third : (Qubit => Unit)) = ops; + first(q); second(q); third(q); + __quantum__rt__qubit_release(q); } - operation RunOps_AdjCtl__AdjCtl__AdjCtl__X_(ops : ((Qubit => Unit is Adj + Ctl), (Qubit => Unit is Adj + Ctl)), q : Qubit) : Unit { - let (second : (Qubit => Unit is Adj + Ctl), third : (Qubit => Unit is Adj + Ctl)) = ops; - X(q); + operation Main() : Unit { + let ops : ((Qubit => Unit is Adj + Ctl), (Qubit => Unit is Adj + Ctl), (Qubit => Unit is Adj + Ctl)) = (H, X, Y); + RunOps_AdjCtl__AdjCtl__AdjCtl_(ops); + } + operation RunOps_AdjCtl__AdjCtl__AdjCtl_(ops : ((Qubit => Unit is Adj + Ctl), (Qubit => Unit is Adj + Ctl), (Qubit => Unit is Adj + Ctl))) : Unit { + let q : Qubit = __quantum__rt__qubit_allocate(); + let (first : (Qubit => Unit is Adj + Ctl), second : (Qubit => Unit is Adj + Ctl), third : (Qubit => Unit is Adj + Ctl)) = ops; + first(q); second(q); third(q); + __quantum__rt__qubit_release(q); } - operation RunOps_AdjCtl__AdjCtl__AdjCtl__Y_(ops : ((Qubit => Unit is Adj + Ctl), (Qubit => Unit is Adj + Ctl)), q : Qubit) : Unit { - let (second : (Qubit => Unit is Adj + Ctl), third : (Qubit => Unit is Adj + Ctl)) = ops; - Y(q); + // entry + Main() + + AFTER: + operation RunOps(ops : ((Qubit => Unit), (Qubit => Unit), (Qubit => Unit))) : Unit { + let q : Qubit = __quantum__rt__qubit_allocate(); + let (first : (Qubit => Unit), second : (Qubit => Unit), third : (Qubit => Unit)) = ops; + first(q); second(q); third(q); + __quantum__rt__qubit_release(q); } - operation RunOps_AdjCtl__AdjCtl__AdjCtl__H__X_(ops : (Qubit => Unit is Adj + Ctl), q : Qubit) : Unit { - let third : (Qubit => Unit is Adj + Ctl) = ops; - H(q); - X(q); - third(q); + operation Main() : Unit { + RunOps_AdjCtl__AdjCtl__AdjCtl__H__X__Y_(); } - operation RunOps_AdjCtl__AdjCtl__AdjCtl__H__Y_(ops : (Qubit => Unit is Adj + Ctl), q : Qubit) : Unit { - let third : (Qubit => Unit is Adj + Ctl) = ops; - H(q); - Y(q); + operation RunOps_AdjCtl__AdjCtl__AdjCtl_(ops : ((Qubit => Unit is Adj + Ctl), (Qubit => Unit is Adj + Ctl), (Qubit => Unit is Adj + Ctl))) : Unit { + let q : Qubit = __quantum__rt__qubit_allocate(); + let (first : (Qubit => Unit is Adj + Ctl), second : (Qubit => Unit is Adj + Ctl), third : (Qubit => Unit is Adj + Ctl)) = ops; + first(q); + second(q); third(q); + __quantum__rt__qubit_release(q); } - operation RunOps_AdjCtl__AdjCtl__AdjCtl__H__X__Y_(q : Qubit) : Unit { + operation RunOps_AdjCtl__AdjCtl__AdjCtl__H__X__Y_() : Unit { + let q : Qubit = __quantum__rt__qubit_allocate(); H(q); X(q); Y(q); + __quantum__rt__qubit_release(q); } // entry Main() @@ -1340,6 +1499,104 @@ fn three_callable_field_tuple_param_reindexes_siblings() { ); } +#[test] +fn indexed_callable_array_preserves_duplicate_global_positions() { + let source = r#" + operation ApplyAt(ops : (Qubit => Unit)[], idx : Int, q : Qubit) : Unit { + ops[idx](q); + } + operation Main() : Unit { + use q = Qubit(); + ApplyAt([H, H, X], 1, q); + } + "#; + + let (fir_store, fir_pkg_id) = compile_and_defunctionalize(source); + let package = fir_store.get(fir_pkg_id); + let mut matching_targets = Vec::new(); + for item in package.items.values() { + let ItemKind::Callable(decl) = &item.kind else { + continue; + }; + if !decl.name.name.starts_with("ApplyAt") || decl.name.name.as_ref() == "ApplyAt" { + continue; + } + let mut targets = Vec::new(); + crate::walk_utils::for_each_expr_in_callable_impl( + package, + &decl.implementation, + &mut |_expr_id, expr| { + if let fir::ExprKind::Call(callee_id, _) = &expr.kind + && let Some(target) = call_target_name(&fir_store, package, *callee_id) + && matches!(target.as_str(), "H" | "X") + { + targets.push(target); + } + }, + ); + targets.sort(); + matching_targets.push((decl.name.name.to_string(), targets)); + } + + assert!( + matching_targets + .iter() + .any(|(_, targets)| targets == &["H", "H", "X"]), + "expected one ApplyAt specialization to dispatch [H, H, X], got {matching_targets:?}" + ); +} + +#[test] +fn struct_copy_surviving_field_is_forwarded_after_callable_field_removal() { + let source = r#" + struct Config { Op : Qubit => Unit, Data : Int } + operation Run(config : Config, q : Qubit) : Unit { + if config.Data == 1 { + config.Op(q); + } + } + operation Main() : Unit { + use q = Qubit(); + let base = new Config { Op = X, Data = 1 }; + Run(new Config { ...base, Op = H }, q); + } + "#; + + let (fir_store, fir_pkg_id) = compile_and_defunctionalize(source); + let package = fir_store.get(fir_pkg_id); + let mut found = false; + for expr in package.exprs.values() { + let fir::ExprKind::Call(callee_id, args_id) = &expr.kind else { + continue; + }; + let Some(target) = call_target_name(&fir_store, package, *callee_id) else { + continue; + }; + if !target.starts_with("Run") || target == "Run" { + continue; + } + + let args = package.get_expr(*args_id); + let fir::ExprKind::Tuple(elements) = &args.kind else { + panic!("specialized Run call should receive Data and q, got {args:?}"); + }; + assert_eq!( + elements.len(), + 2, + "specialized Run call must keep copied Data and q" + ); + assert!( + matches!( + package.get_expr(elements[0]).ty, + qsc_fir::ty::Ty::Prim(qsc_fir::ty::Prim::Int) + ), + "first specialized Run argument must be the surviving copied Int field" + ); + found = true; + } + assert!(found, "expected a specialized Run call for H"); +} + #[test] fn capture_local_ids_are_reasonable() { let (mut fir_store, fir_pkg_id) = compile_to_monomorphized_fir( @@ -2222,6 +2479,70 @@ fn specialize_nested_callable_transitive_alias() { ); } +#[test] +fn specialize_nested_callable_through_aggregate_alias() { + check_rewrite( + r#" + operation Wrapper(pair : (Qubit => Unit, Int), q : Qubit) : Unit { + let alias = pair; + let (op, n) = alias; + op(q); + let _ = n; + } + operation Main() : Unit { + use q = Qubit(); + Wrapper((H, 42), q); + } + "#, + &expect![[r#" + BEFORE: + operation Wrapper(pair : ((Qubit => Unit), Int), q : Qubit) : Unit { + let alias : ((Qubit => Unit), Int) = pair; + let (op : (Qubit => Unit), n : Int) = alias; + op(q); + let _ : Int = n; + } + operation Main() : Unit { + let q : Qubit = __quantum__rt__qubit_allocate(); + Wrapper_AdjCtl_((H, 42), q); + __quantum__rt__qubit_release(q); + } + operation Wrapper_AdjCtl_(pair : ((Qubit => Unit is Adj + Ctl), Int), q : Qubit) : Unit { + let alias : ((Qubit => Unit is Adj + Ctl), Int) = pair; + let (op : (Qubit => Unit is Adj + Ctl), n : Int) = alias; + op(q); + let _ : Int = n; + } + // entry + Main() + + AFTER: + operation Wrapper(pair : ((Qubit => Unit), Int), q : Qubit) : Unit { + let (op : (Qubit => Unit), n : Int) = pair; + op(q); + let _ : Int = n; + } + operation Main() : Unit { + let q : Qubit = __quantum__rt__qubit_allocate(); + Wrapper_AdjCtl__H_(42, q); + __quantum__rt__qubit_release(q); + } + operation Wrapper_AdjCtl_(pair : ((Qubit => Unit is Adj + Ctl), Int), q : Qubit) : Unit { + let (op : (Qubit => Unit is Adj + Ctl), n : Int) = pair; + op(q); + let _ : Int = n; + } + operation Wrapper_AdjCtl__H_(pair : Int, q : Qubit) : Unit { + let n : Int = pair; + H(q); + let _ : Int = n; + } + // entry + Main() + "#]], + ); +} + #[test] fn specialize_nested_callable_invariants() { let source = r#" @@ -3345,3 +3666,1233 @@ fn inline_closure_capture_threads_correct_value() { "#]], ); } + +/// A struct-capturing closure invoked through a `Controlled` dispatch must +/// thread its capture all the way to the controlled call. Here the closure +/// captures a `StatePreparationParams` struct (a partial application of +/// `ApplyStatePreparation`) and is forwarded into an inner closure that issues +/// `Controlled prepareOp([control], systems)` under a loop. +/// +/// The correct post-fix rewrite retargets that controlled call to the concrete +/// operation while threading the captured struct, i.e. +/// `Controlled ApplyStatePreparation([control], (__capture_0, systems))`. This +/// guards against a silent re-drop where the control layer wraps the base input +/// and the capture is lost, leaving `Controlled ApplyStatePreparation([control], systems)`. +#[test] +fn struct_capture_closure_threads_capture_through_controlled_dispatch() { + let source = r#" + struct StatePreparationParams { + rowMap : Int[], + stateVector : Double[], + expansionOps : Int[][], + numQubits : Int + } + + operation ApplyStatePreparation(params : StatePreparationParams, qs : Qubit[]) : Unit is Adj + Ctl { + if Length(params.expansionOps) != 0 { + X(qs[0]); + } + } + + operation SelectIdentity(systems : Qubit[], ancilla : Qubit[]) : Unit is Adj + Ctl {} + + function MakeControlledPrepSelPrepOp( + prepareOp : Qubit[] => Unit is Adj + Ctl, + selectOp : (Qubit[], Qubit[]) => Unit is Adj + Ctl, + numSystemQubits : Int, + power : Int + ) : (Qubit, Qubit[]) => Unit { + (control, allQubits) => { + let systems = allQubits[0..numSystemQubits - 1]; + let ancilla = allQubits[numSystemQubits...]; + for _ in 0..power - 1 { + Controlled prepareOp([control], systems); + Controlled selectOp([control], (systems, ancilla)); + } + } + } + + operation MakeControlledPrepSelPrepCircuit( + prepareOp : Qubit[] => Unit is Adj + Ctl, + selectOp : (Qubit[], Qubit[]) => Unit is Adj + Ctl, + numSystemQubits : Int, + power : Int + ) : Unit { + use control = Qubit(); + use systems = Qubit[numSystemQubits + 1]; + let op = MakeControlledPrepSelPrepOp(prepareOp, selectOp, numSystemQubits, power); + op(control, systems); + } + + operation Main() : Unit { + let params = new StatePreparationParams { + rowMap = [0], + stateVector = [1.0, 0.0], + expansionOps = [], + numQubits = 1 + }; + let prep = ApplyStatePreparation(params, _); + MakeControlledPrepSelPrepCircuit(prep, SelectIdentity, 1, 1); + } + "#; + check_errors(source, &expect!["(no error)"]); + check_rewrite( + source, + &expect![[r#" + BEFORE: + newtype StatePreparationParams = (Int[], Double[], Int[][], Int); + operation ApplyStatePreparation(params : __UDT_Item_1__Package_2_, qs : Qubit[]) : Unit is Adj + Ctl { + body ... { + if Length(params::expansionOps) != 0 { + X(qs[0]); + } + + } + adjoint ... { + if Length(params::expansionOps) != 0 { + Adjoint X(qs[0]); + } + + } + controlled (ctls, ...) { + if Length(params::expansionOps) != 0 { + Controlled X(ctls, qs[0]); + } + + } + controlled adjoint (ctls, ...) { + if Length(params::expansionOps) != 0 { + Controlled Adjoint X(ctls, qs[0]); + } + + } + } + operation SelectIdentity(systems : Qubit[], ancilla : Qubit[]) : Unit is Adj + Ctl { + body ... {} + adjoint ... {} + controlled (ctls, ...) {} + controlled adjoint (ctls, ...) {} + } + function MakeControlledPrepSelPrepOp(prepareOp : (Qubit[] => Unit), selectOp : ((Qubit[], Qubit[]) => Unit), numSystemQubits : Int, power : Int) : ((Qubit, Qubit[]) => Unit) { + / * closure item = 7 captures = [prepareOp, selectOp, numSystemQubits, power] * / _lambda_7 + } + operation MakeControlledPrepSelPrepCircuit(prepareOp : (Qubit[] => Unit), selectOp : ((Qubit[], Qubit[]) => Unit), numSystemQubits : Int, power : Int) : Unit { + let control : Qubit = __quantum__rt__qubit_allocate(); + let systems : Qubit[] = AllocateQubitArray(numSystemQubits + 1); + let op : ((Qubit, Qubit[]) => Unit) = MakeControlledPrepSelPrepOp_AdjCtl__AdjCtl_(prepareOp, selectOp, numSystemQubits, power); + op(control, systems); + ReleaseQubitArray(systems); + __quantum__rt__qubit_release(control); + } + operation Main() : Unit { + let params : __UDT_Item_1__Package_2_ = new StatePreparationParams { + rowMap = [0], + stateVector = [1., 0.], + expansionOps = [], + numQubits = 1 + }; + let prep : (Qubit[] => Unit is Adj + Ctl) = { + let arg : __UDT_Item_1__Package_2_ = params; + / * closure item = 8 captures = [arg] * / _lambda_8 + }; + MakeControlledPrepSelPrepCircuit_AdjCtl__AdjCtl_(prep, SelectIdentity, 1, 1); + } + operation _lambda_7(prepareOp : (Qubit[] => Unit), selectOp : ((Qubit[], Qubit[]) => Unit), numSystemQubits : Int, power : Int, (control : Qubit, allQubits : Qubit[])) : Unit { + { + let systems : Qubit[] = allQubits[0..numSystemQubits - 1]; + let ancilla : Qubit[] = allQubits[numSystemQubits...]; + { + let _range_id_341 : Range = 0..power - 1; + mutable _index_id_344 : Int = _range_id_341::Start; + let _step_id_349 : Int = _range_id_341::Step; + let _end_id_354 : Int = _range_id_341::End; + while _step_id_349 > 0 and _index_id_344 <= _end_id_354 or _step_id_349 < 0 and _index_id_344 >= _end_id_354 { + let _ : Int = _index_id_344; + Controlled prepareOp([control], systems); + Controlled selectOp([control], (systems, ancilla)); + _index_id_344 += _step_id_349; + } + + } + + } + + } + operation _lambda_8(arg : __UDT_Item_1__Package_2_, hole : Qubit[]) : Unit is Adj + Ctl { + body ... { + ApplyStatePreparation(arg, hole) + } + adjoint ... { + Adjoint ApplyStatePreparation(arg, hole) + } + controlled (ctls, ...) { + Controlled ApplyStatePreparation(ctls, (arg, hole)) + } + controlled adjoint (ctls, ...) { + Controlled Adjoint ApplyStatePreparation(ctls, (arg, hole)) + } + } + function MakeControlledPrepSelPrepOp_AdjCtl__AdjCtl_(prepareOp : (Qubit[] => Unit is Adj + Ctl), selectOp : ((Qubit[], Qubit[]) => Unit is Adj + Ctl), numSystemQubits : Int, power : Int) : ((Qubit, Qubit[]) => Unit) { + / * closure item = 10 captures = [prepareOp, selectOp, numSystemQubits, power] * / _lambda_7 + } + operation _lambda_7(prepareOp : (Qubit[] => Unit is Adj + Ctl), selectOp : ((Qubit[], Qubit[]) => Unit is Adj + Ctl), numSystemQubits : Int, power : Int, (control : Qubit, allQubits : Qubit[])) : Unit { + { + let systems : Qubit[] = allQubits[0..numSystemQubits - 1]; + let ancilla : Qubit[] = allQubits[numSystemQubits...]; + { + let _range_id_341 : Range = 0..power - 1; + mutable _index_id_344 : Int = _range_id_341::Start; + let _step_id_349 : Int = _range_id_341::Step; + let _end_id_354 : Int = _range_id_341::End; + while _step_id_349 > 0 and _index_id_344 <= _end_id_354 or _step_id_349 < 0 and _index_id_344 >= _end_id_354 { + let _ : Int = _index_id_344; + Controlled prepareOp([control], systems); + Controlled selectOp([control], (systems, ancilla)); + _index_id_344 += _step_id_349; + } + + } + + } + + } + operation MakeControlledPrepSelPrepCircuit_AdjCtl__AdjCtl_(prepareOp : (Qubit[] => Unit is Adj + Ctl), selectOp : ((Qubit[], Qubit[]) => Unit is Adj + Ctl), numSystemQubits : Int, power : Int) : Unit { + let control : Qubit = __quantum__rt__qubit_allocate(); + let systems : Qubit[] = AllocateQubitArray(numSystemQubits + 1); + let op : ((Qubit, Qubit[]) => Unit) = MakeControlledPrepSelPrepOp_AdjCtl__AdjCtl_(prepareOp, selectOp, numSystemQubits, power); + op(control, systems); + ReleaseQubitArray(systems); + __quantum__rt__qubit_release(control); + } + // entry + Main() + + AFTER: + newtype StatePreparationParams = (Int[], Double[], Int[][], Int); + operation ApplyStatePreparation(params : __UDT_Item_1__Package_2_, qs : Qubit[]) : Unit is Adj + Ctl { + body ... { + if Length(params::expansionOps) != 0 { + X(qs[0]); + } + + } + adjoint ... { + if Length(params::expansionOps) != 0 { + Adjoint X(qs[0]); + } + + } + controlled (ctls, ...) { + if Length(params::expansionOps) != 0 { + Controlled X(ctls, qs[0]); + } + + } + controlled adjoint (ctls, ...) { + if Length(params::expansionOps) != 0 { + Controlled Adjoint X(ctls, qs[0]); + } + + } + } + operation SelectIdentity(systems : Qubit[], ancilla : Qubit[]) : Unit is Adj + Ctl { + body ... {} + adjoint ... {} + controlled (ctls, ...) {} + controlled adjoint (ctls, ...) {} + } + function MakeControlledPrepSelPrepOp(prepareOp : (Qubit[] => Unit), selectOp : ((Qubit[], Qubit[]) => Unit), numSystemQubits : Int, power : Int) : ((Qubit, Qubit[]) => Unit) { + / * closure item = 7 captures = [prepareOp, selectOp, numSystemQubits, power] * / _lambda_7 + } + operation MakeControlledPrepSelPrepCircuit(prepareOp : (Qubit[] => Unit), selectOp : ((Qubit[], Qubit[]) => Unit), numSystemQubits : Int, power : Int) : Unit { + let control : Qubit = __quantum__rt__qubit_allocate(); + let systems : Qubit[] = AllocateQubitArray(numSystemQubits + 1); + let op : ((Qubit, Qubit[]) => Unit) = MakeControlledPrepSelPrepOp_AdjCtl__AdjCtl_(prepareOp, selectOp, numSystemQubits, power); + op(control, systems); + ReleaseQubitArray(systems); + __quantum__rt__qubit_release(control); + } + operation Main() : Unit { + let params : __UDT_Item_1__Package_2_ = new StatePreparationParams { + rowMap = [0], + stateVector = [1., 0.], + expansionOps = [], + numQubits = 1 + }; + MakeControlledPrepSelPrepCircuit_AdjCtl__AdjCtl__closure__SelectIdentity_(1, 1, params); + } + operation _lambda_7(prepareOp : (Qubit[] => Unit), selectOp : ((Qubit[], Qubit[]) => Unit), numSystemQubits : Int, power : Int, (control : Qubit, allQubits : Qubit[])) : Unit { + { + let systems : Qubit[] = allQubits[0..numSystemQubits - 1]; + let ancilla : Qubit[] = allQubits[numSystemQubits...]; + { + let _range_id_341 : Range = 0..power - 1; + mutable _index_id_344 : Int = _range_id_341::Start; + let _step_id_349 : Int = _range_id_341::Step; + let _end_id_354 : Int = _range_id_341::End; + while _step_id_349 > 0 and _index_id_344 <= _end_id_354 or _step_id_349 < 0 and _index_id_344 >= _end_id_354 { + let _ : Int = _index_id_344; + Controlled prepareOp([control], systems); + Controlled selectOp([control], (systems, ancilla)); + _index_id_344 += _step_id_349; + } + + } + + } + + } + operation _lambda_8(arg : __UDT_Item_1__Package_2_, hole : Qubit[]) : Unit is Adj + Ctl { + body ... { + ApplyStatePreparation(arg, hole) + } + adjoint ... { + Adjoint ApplyStatePreparation(arg, hole) + } + controlled (ctls, ...) { + Controlled ApplyStatePreparation(ctls, (arg, hole)) + } + controlled adjoint (ctls, ...) { + Controlled Adjoint ApplyStatePreparation(ctls, (arg, hole)) + } + } + function MakeControlledPrepSelPrepOp_AdjCtl__AdjCtl_(prepareOp : (Qubit[] => Unit is Adj + Ctl), selectOp : ((Qubit[], Qubit[]) => Unit is Adj + Ctl), numSystemQubits : Int, power : Int) : ((Qubit, Qubit[]) => Unit) { + () + } + operation _lambda_7(prepareOp : (Qubit[] => Unit is Adj + Ctl), selectOp : ((Qubit[], Qubit[]) => Unit is Adj + Ctl), numSystemQubits : Int, power : Int, (control : Qubit, allQubits : Qubit[])) : Unit { + { + let systems : Qubit[] = allQubits[0..numSystemQubits - 1]; + let ancilla : Qubit[] = allQubits[numSystemQubits...]; + { + let _range_id_341 : Range = 0..power - 1; + mutable _index_id_344 : Int = _range_id_341::Start; + let _step_id_349 : Int = _range_id_341::Step; + let _end_id_354 : Int = _range_id_341::End; + while _step_id_349 > 0 and _index_id_344 <= _end_id_354 or _step_id_349 < 0 and _index_id_344 >= _end_id_354 { + let _ : Int = _index_id_344; + Controlled prepareOp([control], systems); + Controlled selectOp([control], (systems, ancilla)); + _index_id_344 += _step_id_349; + } + + } + + } + + } + operation MakeControlledPrepSelPrepCircuit_AdjCtl__AdjCtl_(prepareOp : (Qubit[] => Unit is Adj + Ctl), selectOp : ((Qubit[], Qubit[]) => Unit is Adj + Ctl), numSystemQubits : Int, power : Int) : Unit { + let control : Qubit = __quantum__rt__qubit_allocate(); + let systems : Qubit[] = AllocateQubitArray(numSystemQubits + 1); + _lambda_7(prepareOp, selectOp, numSystemQubits, power, (control, systems)); + ReleaseQubitArray(systems); + __quantum__rt__qubit_release(control); + } + operation MakeControlledPrepSelPrepCircuit_AdjCtl__AdjCtl__closure__SelectIdentity_(numSystemQubits : Int, power : Int, __capture_0 : __UDT_Item_1__Package_2_) : Unit { + let control : Qubit = __quantum__rt__qubit_allocate(); + let systems : Qubit[] = AllocateQubitArray(numSystemQubits + 1); + _lambda_7_closure__SelectIdentity_(numSystemQubits, power, (control, systems), __capture_0); + ReleaseQubitArray(systems); + __quantum__rt__qubit_release(control); + } + function MakeControlledPrepSelPrepOp_AdjCtl__AdjCtl__closure__SelectIdentity_(numSystemQubits : Int, power : Int, __capture_0 : __UDT_Item_1__Package_2_) : ((Qubit, Qubit[]) => Unit) { + / * closure item = 14 captures = [numSystemQubits, power] * / _lambda_7 + } + operation _lambda_7(numSystemQubits : Int, power : Int, (control : Qubit, allQubits : Qubit[])) : Unit { + { + let systems : Qubit[] = allQubits[0..numSystemQubits - 1]; + let ancilla : Qubit[] = allQubits[numSystemQubits...]; + { + let _range_id_341 : Range = 0..power - 1; + mutable _index_id_344 : Int = _range_id_341::Start; + let _step_id_349 : Int = _range_id_341::Step; + let _end_id_354 : Int = _range_id_341::End; + while _step_id_349 > 0 and _index_id_344 <= _end_id_354 or _step_id_349 < 0 and _index_id_344 >= _end_id_354 { + let _ : Int = _index_id_344; + Controlled _lambda_8([control], systems); + Controlled SelectIdentity([control], (systems, ancilla)); + _index_id_344 += _step_id_349; + } + + } + + } + + } + operation _lambda_7_closure__SelectIdentity_(numSystemQubits : Int, power : Int, (control : Qubit, allQubits : Qubit[]), __capture_0 : __UDT_Item_1__Package_2_) : Unit { + { + let systems : Qubit[] = allQubits[0..numSystemQubits - 1]; + let ancilla : Qubit[] = allQubits[numSystemQubits...]; + { + let _range_id_341 : Range = 0..power - 1; + mutable _index_id_344 : Int = _range_id_341::Start; + let _step_id_349 : Int = _range_id_341::Step; + let _end_id_354 : Int = _range_id_341::End; + while _step_id_349 > 0 and _index_id_344 <= _end_id_354 or _step_id_349 < 0 and _index_id_344 >= _end_id_354 { + let _ : Int = _index_id_344; + Controlled _lambda_8([control], (__capture_0, systems)); + Controlled SelectIdentity([control], (systems, ancilla)); + _index_id_344 += _step_id_349; + } + + } + + } + + } + // entry + Main() + "#]], + ); +} + +/// A closure that captures a struct built from a factory function's own +/// parameters must be rebuilt from caller-scope values when it is specialized +/// into a different callable. +/// +/// `Main` calls the factory `MakeStatePreparationOp`, which builds a +/// `StatePreparationParams` struct from its parameters and returns a +/// partial-application closure capturing that struct. The closure is forwarded +/// through the `MakeControlledPrepSelPrepCircuit` wrapper. +/// +/// Because the captured struct references the factory's parameters, it cannot +/// be copied as-is into `Main`, which does not bind those parameters. +/// Specialization must rebind each struct field to the argument the factory was +/// called with — `[0]`, `[1.0, 0.0]`, `[]`, and `1` — so the struct is rooted +/// entirely in caller-scope values. +/// +/// The test expects no errors and passing `PostDefunc` invariants. +#[test] +fn producer_scope_struct_capture_reconstructed_in_caller() { + let source = r#" + struct StatePreparationParams { + rowMap : Int[], + stateVector : Double[], + expansionOps : Int[][], + numQubits : Int + } + + operation ApplyStatePreparation(params : StatePreparationParams, qs : Qubit[]) : Unit is Adj + Ctl { + if Length(params.expansionOps) != 0 { + X(qs[0]); + } + } + + operation SelectIdentity(systems : Qubit[], ancilla : Qubit[]) : Unit is Adj + Ctl {} + + function MakeStatePreparationOp( + rowMap : Int[], + stateVector : Double[], + expansionOps : Int[][], + numQubits : Int + ) : Qubit[] => Unit is Adj + Ctl { + let params = new StatePreparationParams { + rowMap = rowMap, + stateVector = stateVector, + expansionOps = expansionOps, + numQubits = numQubits + }; + ApplyStatePreparation(params, _) + } + + function MakeControlledPrepSelPrepOp( + prepareOp : Qubit[] => Unit is Adj + Ctl, + selectOp : (Qubit[], Qubit[]) => Unit is Adj + Ctl, + numSystemQubits : Int, + power : Int + ) : (Qubit, Qubit[]) => Unit { + (control, allQubits) => { + let systems = allQubits[0..numSystemQubits - 1]; + let ancilla = allQubits[numSystemQubits...]; + for _ in 0..power - 1 { + Controlled prepareOp([control], systems); + Controlled selectOp([control], (systems, ancilla)); + } + } + } + + operation MakeControlledPrepSelPrepCircuit( + prepareOp : Qubit[] => Unit is Adj + Ctl, + selectOp : (Qubit[], Qubit[]) => Unit is Adj + Ctl, + numSystemQubits : Int, + power : Int + ) : Unit { + use control = Qubit(); + use systems = Qubit[numSystemQubits + 1]; + let op = MakeControlledPrepSelPrepOp(prepareOp, selectOp, numSystemQubits, power); + op(control, systems); + } + + operation Main() : Unit { + let prep = MakeStatePreparationOp([0], [1.0, 0.0], [], 1); + MakeControlledPrepSelPrepCircuit(prep, SelectIdentity, 1, 1); + } + "#; + check_errors(source, &expect!["(no error)"]); + // The invariant check is the point of this test: the captured struct must be + // rebuilt from caller-scope values, or the `PostDefunc` local-variable + // consistency check fails. + check_invariants(source); +} + +/// A captured struct whose field is a computed value referencing the factory's +/// parameters through pure function calls and operators must still specialize. +/// +/// Here `numQubits` is `Length(stateVector) + Length(rowMap)`. Rebuilding the +/// captured struct in `Main` requires rebinding the parameter references inside +/// the computed field to the caller-scope arguments `[1.0, 0.0]` and `[0]`. +/// Because `Length` is a pure function and `+` has no side effects, the field +/// can be safely reconstructed from caller values. +/// +/// The test expects genuine specialization: no errors and passing `PostDefunc` +/// invariants. A decline to a dynamic call would instead emit a +/// `DynamicCallable` error and fail the assertion. +#[test] +fn producer_scope_struct_capture_computed_field_specializes() { + let source = r#" + struct StatePreparationParams { + rowMap : Int[], + stateVector : Double[], + expansionOps : Int[][], + numQubits : Int + } + + operation ApplyStatePreparation(params : StatePreparationParams, qs : Qubit[]) : Unit is Adj + Ctl { + if params.numQubits != 0 { + X(qs[0]); + } + } + + operation SelectIdentity(systems : Qubit[], ancilla : Qubit[]) : Unit is Adj + Ctl {} + + function MakeStatePreparationOp( + rowMap : Int[], + stateVector : Double[], + expansionOps : Int[][] + ) : Qubit[] => Unit is Adj + Ctl { + let params = new StatePreparationParams { + rowMap = rowMap, + stateVector = stateVector, + expansionOps = expansionOps, + numQubits = Length(stateVector) + Length(rowMap) + }; + ApplyStatePreparation(params, _) + } + + function MakeControlledPrepSelPrepOp( + prepareOp : Qubit[] => Unit is Adj + Ctl, + selectOp : (Qubit[], Qubit[]) => Unit is Adj + Ctl, + numSystemQubits : Int, + power : Int + ) : (Qubit, Qubit[]) => Unit { + (control, allQubits) => { + let systems = allQubits[0..numSystemQubits - 1]; + let ancilla = allQubits[numSystemQubits...]; + for _ in 0..power - 1 { + Controlled prepareOp([control], systems); + Controlled selectOp([control], (systems, ancilla)); + } + } + } + + operation MakeControlledPrepSelPrepCircuit( + prepareOp : Qubit[] => Unit is Adj + Ctl, + selectOp : (Qubit[], Qubit[]) => Unit is Adj + Ctl, + numSystemQubits : Int, + power : Int + ) : Unit { + use control = Qubit(); + use systems = Qubit[numSystemQubits + 1]; + let op = MakeControlledPrepSelPrepOp(prepareOp, selectOp, numSystemQubits, power); + op(control, systems); + } + + operation Main() : Unit { + let prep = MakeStatePreparationOp([0], [1.0, 0.0], []); + MakeControlledPrepSelPrepCircuit(prep, SelectIdentity, 1, 1); + } + "#; + check_errors(source, &expect!["(no error)"]); + // Passing `check_invariants` proves genuine specialization. It runs + // defunctionalization, asserts there are no defunctionalization errors, and + // checks `PostDefunc` consistency; a decline to dynamic would emit a + // `DynamicCallable` error and fail the assertion. + check_invariants(source); +} + +/// A captured struct whose field is computed by an operation call must not be +/// rebuilt in the caller; specialization declines to a dynamic call instead. +/// +/// Here `numQubits` is `CountQubits(rowMap)`, where `CountQubits` is an +/// operation. Relocating an operation call into caller-scope argument +/// construction could change observable ordering or duplicate the call, so +/// specialization refuses to reconstruct the field. Instead it declines the +/// closure to a dynamic call site and emits the recoverable `DynamicCallable` +/// diagnostic. +/// +/// This confirms the purity check does not over-decline: the pure-function +/// computed field above still specializes, while this operation-valued field +/// declines cleanly rather than panicking. +#[test] +fn producer_scope_struct_capture_operation_field_declines_to_dynamic() { + let source = r#" + struct StatePreparationParams { + rowMap : Int[], + numQubits : Int + } + + operation CountQubits(arr : Int[]) : Int { + return Length(arr); + } + + operation ApplyStatePreparation(params : StatePreparationParams, qs : Qubit[]) : Unit is Adj + Ctl { + if params.numQubits != 0 { + X(qs[0]); + } + } + + operation SelectIdentity(systems : Qubit[], ancilla : Qubit[]) : Unit is Adj + Ctl {} + + operation MakeStatePreparationOp(rowMap : Int[]) : Qubit[] => Unit is Adj + Ctl { + let params = new StatePreparationParams { + rowMap = rowMap, + numQubits = CountQubits(rowMap) + }; + ApplyStatePreparation(params, _) + } + + function MakeControlledPrepSelPrepOp( + prepareOp : Qubit[] => Unit is Adj + Ctl, + selectOp : (Qubit[], Qubit[]) => Unit is Adj + Ctl, + numSystemQubits : Int, + power : Int + ) : (Qubit, Qubit[]) => Unit { + (control, allQubits) => { + let systems = allQubits[0..numSystemQubits - 1]; + let ancilla = allQubits[numSystemQubits...]; + for _ in 0..power - 1 { + Controlled prepareOp([control], systems); + Controlled selectOp([control], (systems, ancilla)); + } + } + } + + operation MakeControlledPrepSelPrepCircuit( + prepareOp : Qubit[] => Unit is Adj + Ctl, + selectOp : (Qubit[], Qubit[]) => Unit is Adj + Ctl, + numSystemQubits : Int, + power : Int + ) : Unit { + use control = Qubit(); + use systems = Qubit[numSystemQubits + 1]; + let op = MakeControlledPrepSelPrepOp(prepareOp, selectOp, numSystemQubits, power); + op(control, systems); + } + + operation Main() : Unit { + let prep = MakeStatePreparationOp([0]); + MakeControlledPrepSelPrepCircuit(prep, SelectIdentity, 1, 1); + } + "#; + // The operation-call computed field cannot be safely rebuilt in the caller, + // so the closure declines to a dynamic call site and emits the recoverable + // `DynamicCallable` diagnostic rather than panicking. + check_errors( + source, + &expect![[r#" + callable argument could not be resolved statically + callable argument could not be resolved statically"#]], + ); +} + +/// A mixed branch-split call can combine a dispatched callable field with a +/// single-valued producer closure while leaving another field of the same tuple +/// parameter live. +/// +/// The combined specialization must remove only the callable fields from +/// `pair`; dropping the whole tuple parameter deletes the destructuring that +/// binds `target` and leaves the inlined calls using an unbound local. The +/// snapshot pins the surviving `target` binding in both dispatch leaves. +#[test] +fn mixed_branch_split_partial_tuple_field_coverage_preserves_surviving_field() { + check_rewrite( + r#" + operation Run(pair : (Qubit => Unit, Qubit => Unit, Qubit)) : Unit { + let (first, second, target) = pair; + first(target); + second(target); + } + operation Main() : Unit { + use q = Qubit(); + let choose = M(q) == One; + let first = if choose { H } else { X }; + let angle = 0.25; + Run((first, target => Rz(angle, target), q)); + } + "#, + &expect![[r#" + BEFORE: + operation Run(pair : ((Qubit => Unit), (Qubit => Unit), Qubit)) : Unit { + let (first : (Qubit => Unit), second : (Qubit => Unit), target : Qubit) = pair; + first(target); + second(target); + } + operation Main() : Unit { + let q : Qubit = __quantum__rt__qubit_allocate(); + let choose : Bool = M(q) == One; + let first : (Qubit => Unit is Adj + Ctl) = if choose { + H + } else { + X + }; + let angle : Double = 0.25; + Run_AdjCtl__Empty_(first, / * closure item = 3 captures = [angle] * / _lambda_3, q); + __quantum__rt__qubit_release(q); + } + operation _lambda_3(angle : Double, target : Qubit) : Unit { + Rz(angle, target) + } + operation Run_AdjCtl__Empty_(pair : ((Qubit => Unit is Adj + Ctl), (Qubit => Unit), Qubit)) : Unit { + let (first : (Qubit => Unit is Adj + Ctl), second : (Qubit => Unit), target : Qubit) = pair; + first(target); + second(target); + } + // entry + Main() + + AFTER: + operation Run(pair : ((Qubit => Unit), (Qubit => Unit), Qubit)) : Unit { + let (first : (Qubit => Unit), second : (Qubit => Unit), target : Qubit) = pair; + first(target); + second(target); + } + operation Main() : Unit { + let q : Qubit = __quantum__rt__qubit_allocate(); + let choose : Bool = M(q) == One; + let angle : Double = 0.25; + if choose { + Run_AdjCtl__Empty__H__closure_(q, angle) + } else { + Run_AdjCtl__Empty__X__closure_(q, angle) + }; + __quantum__rt__qubit_release(q); + } + operation _lambda_3(angle : Double, target : Qubit) : Unit { + Rz(angle, target) + } + operation Run_AdjCtl__Empty_(pair : ((Qubit => Unit is Adj + Ctl), (Qubit => Unit), Qubit)) : Unit { + let (first : (Qubit => Unit is Adj + Ctl), second : (Qubit => Unit), target : Qubit) = pair; + first(target); + second(target); + } + operation Run_AdjCtl__Empty__H__closure_(pair : Qubit, __capture_0 : Double) : Unit { + let target : Qubit = pair; + H(target); + _lambda_3(__capture_0, target); + } + operation Run_AdjCtl__Empty__X__closure_(pair : Qubit, __capture_0 : Double) : Unit { + let target : Qubit = pair; + X(target); + _lambda_3(__capture_0, target); + } + // entry + Main() + "#]], + ); +} + +/// A mixed branch-split group with a `Dynamic` sibling is intentionally not a +/// combined per-candidate specialization. +/// +/// The unresolved `third` argument must surface as `DynamicCallable`. The +/// producer closure sibling may still get a transient per-row spec while that +/// diagnostic is collected, but tracking it as consumed would clear its body or +/// trip the internal consistency panic. This test uses more than `MULTI_CAP` +/// array elements to force the sibling to `Dynamic` and asserts the pass returns +/// diagnostics instead of panicking. +#[test] +fn mixed_branch_split_dynamic_sibling_reports_error_without_panic() { + use std::fmt::Write as _; + + const ELEMENTS: usize = 1001; + + let mut defs = String::new(); + let mut elems = String::new(); + for i in 0..ELEMENTS { + writeln!(defs, " operation Op{i}(q : Qubit) : Unit {{}}").expect("write succeeds"); + if i > 0 { + elems.push_str(", "); + } + write!(elems, "Op{i}").expect("write succeeds"); + } + + let source = format!( + r#" +{defs} + operation Run( + first : Qubit => Unit, + second : Qubit => Unit, + third : Qubit => Unit, + q : Qubit + ) : Unit {{ + first(q); + second(q); + third(q); + }} + operation Main() : Unit {{ + use q = Qubit(); + let choose = M(q) == One; + let first = if choose {{ Op0 }} else {{ Op1 }}; + let angle = 0.25; + let ops = [{elems}]; + for third in ops {{ + Run(first, target => Rz(angle, target), third, q); + }} + }} + "# + ); + + check_errors( + &source, + &expect![[r#" + callable argument could not be resolved statically + callable argument could not be resolved statically"#]], + ); +} + +#[test] +fn single_element_callable_array_into_struct_field_survives_as_array() { + // A single-element callable array threaded into a callee that indexes it + // (`arr[0]`) and stores the result in a struct field must survive + // specialization as a one-element array literal. Collapsing the forwarded + // array to the scalar callable would leave `arr[0]` indexing a non-array + // value, so the specialized body keeps `[AddOne][0]`. + check_rewrite( + r#" + struct Holder { Cb : (Int => Int) } + operation Pick(arr : (Int => Int)[]) : Holder { + let f = arr[0]; + new Holder { Cb = f } + } + operation Main() : Int { + let ops : (Int => Int)[] = [AddOne]; + let h = Pick(ops); + h.Cb(3) + } + operation AddOne(x : Int) : Int { x + 1 } + "#, + &expect![[r#" + BEFORE: + newtype Holder = ((Int => Int), ); + operation Pick(arr : (Int => Int)[]) : __UDT_Item_1__Package_2_ { + let f : (Int => Int) = arr[0]; + new Holder { + Cb = f + } + + } + operation Main() : Int { + let ops : (Int => Int)[] = [AddOne]; + let h : __UDT_Item_1__Package_2_ = Pick_Empty_(ops); + h::Cb(3) + } + operation AddOne(x : Int) : Int { + x + 1 + } + operation Pick_Empty_(arr : (Int => Int)[]) : __UDT_Item_1__Package_2_ { + let f : (Int => Int) = arr[0]; + new Holder { + Cb = f + } + + } + // entry + Main() + + AFTER: + newtype Holder = ((Int => Int), ); + operation Pick(arr : (Int => Int)[]) : __UDT_Item_1__Package_2_ { + let f : (Int => Int) = arr[0]; + new Holder { + Cb = f + } + + } + operation Main() : Int { + let ops : (Int => Int)[] = [AddOne]; + let h : __UDT_Item_1__Package_2_ = Pick_Empty__AddOne_(); + h::Cb(3) + } + operation AddOne(x : Int) : Int { + x + 1 + } + operation Pick_Empty_(arr : (Int => Int)[]) : __UDT_Item_1__Package_2_ { + let f : (Int => Int) = arr[0]; + new Holder { + Cb = f + } + + } + operation Pick_Empty__AddOne_() : __UDT_Item_1__Package_2_ { + let f : (Int => Int) = [AddOne][0]; + new Holder { + Cb = f + } + + } + // entry + Main() + "#]], + ); +} + +/// A single-element tuple parameter `(Qubit => Unit,)` whose only field is a +/// capturing producer closure routes through the per-row singular path. Removing +/// the consumed field empties the parameter's tuple, so the specialized input +/// drops the emptied slot and keeps only the threaded capture. The rebuilt call +/// argument must likewise supply only the capture and never prepend the emptied +/// slot, so the specialized input pattern and the call argument stay arity +/// matched. +#[test] +fn single_element_producer_tuple_param_drops_slot_and_threads_capture() { + check_rewrite( + r#" + operation Rotate(angle : Double, q : Qubit) : Unit { Rx(angle, q); } + function Make(angle : Double) : (Qubit => Unit) { return Rotate(angle, _); } + operation ApplyTup(ops : (Qubit => Unit,)) : Unit { + use q = Qubit(); + let (a,) = ops; + a(q); + a(q); + } + @EntryPoint() + operation Main() : Unit { + ApplyTup((Make(0.5),)); + } + "#, + &expect![[r#" + BEFORE: + operation Rotate(angle : Double, q : Qubit) : Unit { + Rx(angle, q); + } + function Make(angle : Double) : (Qubit => Unit) { + return { + let arg : Double = angle; + / * closure item = 5 captures = [arg] * / _lambda_5 + }; + } + operation ApplyTup(ops : ((Qubit => Unit), )) : Unit { + let q : Qubit = __quantum__rt__qubit_allocate(); + let (a : (Qubit => Unit), ) = ops; + a(q); + a(q); + __quantum__rt__qubit_release(q); + } + operation Main() : Unit { + ApplyTup_Empty_(Make(0.5), ); + } + operation _lambda_5(arg : Double, hole : Qubit) : Unit { + Rotate(arg, hole) + } + operation ApplyTup_Empty_(ops : ((Qubit => Unit), )) : Unit { + let q : Qubit = __quantum__rt__qubit_allocate(); + let (a : (Qubit => Unit), ) = ops; + a(q); + a(q); + __quantum__rt__qubit_release(q); + } + // entry + Main() + + AFTER: + operation Rotate(angle : Double, q : Qubit) : Unit { + Rx(angle, q); + } + function Make(angle : Double) : (Qubit => Unit) { + return { + let arg : Double = angle; + () + }; + } + operation ApplyTup(ops : ((Qubit => Unit), )) : Unit { + let q : Qubit = __quantum__rt__qubit_allocate(); + let (a : (Qubit => Unit), ) = ops; + a(q); + a(q); + __quantum__rt__qubit_release(q); + } + operation Main() : Unit { + ApplyTup_Empty__closure_(0.5, ); + } + operation _lambda_5(arg : Double, hole : Qubit) : Unit { + Rotate(arg, hole) + } + operation ApplyTup_Empty_(ops : ((Qubit => Unit), )) : Unit { + let q : Qubit = __quantum__rt__qubit_allocate(); + let (a : (Qubit => Unit), ) = ops; + a(q); + a(q); + __quantum__rt__qubit_release(q); + } + operation ApplyTup_Empty__closure_(__capture_0 : Double, ) : Unit { + let q : Qubit = __quantum__rt__qubit_allocate(); + _lambda_5(__capture_0, q); + _lambda_5(__capture_0, q); + __quantum__rt__qubit_release(q); + } + // entry + Main() + "#]], + ); +} + +/// A `for` loop over a callable array desugars to a `Length(array)` call and an +/// indexed read of the array. `Length` is an intrinsic that consumes its +/// argument as data and never invokes it, so it must not be treated as a +/// higher-order function: the array argument has to survive so `Length` and the +/// indexed read stay well-formed. Here the loop element `op` (candidates +/// `[H, X]`) is dispatched into the two-parameter `ApplyTwo` alongside a +/// single-valued global sibling `Y` in a different slot. The rewrite keeps both +/// dispatch candidates, threads the sibling into each specialized leaf, and +/// leaves the `Length(_array_id)` call unspecialized. +#[test] +fn callable_array_loop_dispatch_with_global_sibling_preserves_length_call() { + let source = r#" + operation ApplyTwo(f : Qubit => Unit, g : Qubit => Unit, q : Qubit) : Unit { + f(q); + g(q); + } + operation Main() : Unit { + use q = Qubit(); + let ops = [H, X]; + for op in ops { + ApplyTwo(op, Y, q); + } + } + "#; + check_invariants(source); + check_rewrite( + source, + &expect![[r#" + BEFORE: + operation ApplyTwo(f : (Qubit => Unit), g : (Qubit => Unit), q : Qubit) : Unit { + f(q); + g(q); + } + operation Main() : Unit { + let q : Qubit = __quantum__rt__qubit_allocate(); + let ops : (Qubit => Unit is Adj + Ctl)[] = [H, X]; + let _generated_ident_84 : Unit = { + let _array_id_51 : (Qubit => Unit is Adj + Ctl)[] = ops; + let _len_id_55 : Int = Length(_array_id_51); + mutable _index_id_60 : Int = 0; + while _index_id_60 < _len_id_55 { + let op : (Qubit => Unit is Adj + Ctl) = _array_id_51[_index_id_60]; + ApplyTwo_AdjCtl__AdjCtl_(op, Y, q); + _index_id_60 += 1; + } + + }; + __quantum__rt__qubit_release(q); + _generated_ident_84 + } + operation ApplyTwo_AdjCtl__AdjCtl_(f : (Qubit => Unit is Adj + Ctl), g : (Qubit => Unit is Adj + Ctl), q : Qubit) : Unit { + f(q); + g(q); + } + // entry + Main() + + AFTER: + operation ApplyTwo(f : (Qubit => Unit), g : (Qubit => Unit), q : Qubit) : Unit { + f(q); + g(q); + } + operation Main() : Unit { + let q : Qubit = __quantum__rt__qubit_allocate(); + let ops : (Qubit => Unit is Adj + Ctl)[] = [H, X]; + let _generated_ident_84 : Unit = { + let _array_id_51 : (Qubit => Unit is Adj + Ctl)[] = ops; + let _len_id_55 : Int = Length(_array_id_51); + mutable _index_id_60 : Int = 0; + while _index_id_60 < _len_id_55 { + if _index_id_60 == 0 { + ApplyTwo_AdjCtl__AdjCtl__H__Y_(q) + } else { + ApplyTwo_AdjCtl__AdjCtl__X__Y_(q) + }; + _index_id_60 += 1; + } + + }; + __quantum__rt__qubit_release(q); + _generated_ident_84 + } + operation ApplyTwo_AdjCtl__AdjCtl_(f : (Qubit => Unit is Adj + Ctl), g : (Qubit => Unit is Adj + Ctl), q : Qubit) : Unit { + f(q); + g(q); + } + operation ApplyTwo_AdjCtl__AdjCtl__H_(g : (Qubit => Unit is Adj + Ctl), q : Qubit) : Unit { + H(q); + g(q); + } + operation ApplyTwo_AdjCtl__AdjCtl__X_(g : (Qubit => Unit is Adj + Ctl), q : Qubit) : Unit { + X(q); + g(q); + } + operation ApplyTwo_AdjCtl__AdjCtl__Y_(f : (Qubit => Unit is Adj + Ctl), q : Qubit) : Unit { + f(q); + Y(q); + } + operation ApplyTwo_AdjCtl__AdjCtl__H__Y_(q : Qubit) : Unit { + H(q); + Y(q); + } + operation ApplyTwo_AdjCtl__AdjCtl__X__Y_(q : Qubit) : Unit { + X(q); + Y(q); + } + // entry + Main() + "#]], + ); +} + +/// A callable array is forwarded into a higher-order function that receives it +/// as a plain array parameter and iterates over it, mirroring the shape of the +/// Deutsch-Jozsa sample where a list of oracles is looped over and each is run +/// through a driver operation. The forwarding operation `RunEach` takes the +/// callable array by value and its own `for` loop desugars to a `Length` call +/// plus an indexed read. The array must survive intact so `Length` and the +/// index stay well-formed, and the inner `Run(op, q)` dispatch is specialized +/// per candidate. +#[test] +fn callable_array_forwarded_to_iterating_hof_preserves_length_call() { + let source = r#" + operation Run(op : Qubit => Unit, q : Qubit) : Unit { + op(q); + } + operation RunEach(ops : (Qubit => Unit)[], q : Qubit) : Unit { + for op in ops { + Run(op, q); + } + } + operation Main() : Unit { + use q = Qubit(); + RunEach([H, X], q); + } + "#; + check_invariants(source); + check_rewrite( + source, + &expect![[r#" + BEFORE: + operation Run(op : (Qubit => Unit), q : Qubit) : Unit { + op(q); + } + operation RunEach(ops : (Qubit => Unit)[], q : Qubit) : Unit { + { + let _array_id_55 : (Qubit => Unit)[] = ops; + let _len_id_59 : Int = Length(_array_id_55); + mutable _index_id_64 : Int = 0; + while _index_id_64 < _len_id_59 { + let op : (Qubit => Unit) = _array_id_55[_index_id_64]; + Run_Empty_(op, q); + _index_id_64 += 1; + } + + } + + } + operation Main() : Unit { + let q : Qubit = __quantum__rt__qubit_allocate(); + RunEach_AdjCtl_([H, X], q); + __quantum__rt__qubit_release(q); + } + operation Run_Empty_(op : (Qubit => Unit), q : Qubit) : Unit { + op(q); + } + operation RunEach_AdjCtl_(ops : (Qubit => Unit is Adj + Ctl)[], q : Qubit) : Unit { + { + let _array_id_55 : (Qubit => Unit is Adj + Ctl)[] = ops; + let _len_id_59 : Int = Length(_array_id_55); + mutable _index_id_64 : Int = 0; + while _index_id_64 < _len_id_59 { + let op : (Qubit => Unit is Adj + Ctl) = _array_id_55[_index_id_64]; + Run_Empty_(op, q); + _index_id_64 += 1; + } + + } + + } + // entry + Main() + + AFTER: + operation Run(op : (Qubit => Unit), q : Qubit) : Unit { + op(q); + } + operation RunEach(ops : (Qubit => Unit)[], q : Qubit) : Unit { + { + let _array_id_55 : (Qubit => Unit)[] = ops; + let _len_id_59 : Int = Length(_array_id_55); + mutable _index_id_64 : Int = 0; + while _index_id_64 < _len_id_59 { + let op : (Qubit => Unit) = _array_id_55[_index_id_64]; + Run_Empty_(op, q); + _index_id_64 += 1; + } + + } + + } + operation Main() : Unit { + let q : Qubit = __quantum__rt__qubit_allocate(); + RunEach_AdjCtl__H__X_(q); + __quantum__rt__qubit_release(q); + } + operation Run_Empty_(op : (Qubit => Unit), q : Qubit) : Unit { + op(q); + } + operation RunEach_AdjCtl_(ops : (Qubit => Unit is Adj + Ctl)[], q : Qubit) : Unit { + { + let _array_id_55 : (Qubit => Unit is Adj + Ctl)[] = ops; + let _len_id_59 : Int = Length(_array_id_55); + mutable _index_id_64 : Int = 0; + while _index_id_64 < _len_id_59 { + let op : (Qubit => Unit is Adj + Ctl) = _array_id_55[_index_id_64]; + Run_Empty_(op, q); + _index_id_64 += 1; + } + + } + + } + operation RunEach_AdjCtl__H__X_(q : Qubit) : Unit { + { + let _array_id_55 : (Qubit => Unit is Adj + Ctl)[] = [H, X]; + let _len_id_59 : Int = Length(_array_id_55); + mutable _index_id_64 : Int = 0; + while _index_id_64 < _len_id_59 { + if _index_id_64 == 0 { + Run_Empty__H_(q) + } else { + Run_Empty__X_(q) + }; + _index_id_64 += 1; + } + + } + + } + operation Run_Empty__H_(q : Qubit) : Unit { + H(q); + } + operation Run_Empty__X_(q : Qubit) : Unit { + X(q); + } + // entry + Main() + "#]], + ); +} diff --git a/source/compiler/qsc_fir_transforms/src/defunctionalize/types.rs b/source/compiler/qsc_fir_transforms/src/defunctionalize/types.rs index 5e410267f8b..bf76a8e4371 100644 --- a/source/compiler/qsc_fir_transforms/src/defunctionalize/types.rs +++ b/source/compiler/qsc_fir_transforms/src/defunctionalize/types.rs @@ -80,6 +80,24 @@ pub struct CallSite { pub call_pkg_id: PackageId, /// The HOF being called. pub hof_item_id: ItemId, + /// The outer input-parameter slot of the HOF this call site resolves. + /// Copied from the originating [`CallableParam::top_level_param`] so that + /// specialize and rewrite can recover the exact parameter for each row + /// instead of collapsing every arrow parameter onto the lowest index. Which + /// parameter a call site resolves is independent of the `condition`, which + /// selects among the branch-dispatch candidates for that one parameter. + pub top_level_param: usize, + /// The tuple-field path relative to `top_level_param`, copied from the + /// originating [`CallableParam::field_path`]. Empty for a separate + /// top-level arrow parameter; non-empty for an arrow field nested inside a + /// single tuple parameter. + pub field_path: Vec, + /// Whether the owning HOF's input pattern is a tuple, copied from the + /// originating [`CallableParam::hof_input_is_tuple`]. Distinguishes a + /// multi-parameter HOF, whose arrow input is a tuple of parameters, from a + /// single tuple-valued parameter, whose arrow input is that tuple. This + /// changes where a nested `field_path` indexes. + pub hof_input_is_tuple: bool, /// Resolved callable argument. pub callable_arg: ConcreteCallable, /// Expression for the callable argument. @@ -139,15 +157,27 @@ pub struct CapturedVar { /// An optional initializer expression to reuse when the original local is /// scoped to a block that rewrite will erase. pub expr: Option, + /// Caller-scope substitutions to apply when `expr` is a producer-scope + /// compound literal (a struct/tuple/array constructor whose sub-exprs + /// reference the producing function's parameters). + /// + /// Each `(local, caller_expr)` entry maps a producer-parameter + /// [`LocalVarId`] appearing inside `expr` to the caller-scope argument + /// [`ExprId`] bound to that parameter at the call site. Rewrite deep-clones + /// `expr` and rebinds each recorded inner `Var(Res::Local(local))` leaf to + /// `caller_expr`, so the literal is reconstructed entirely from caller-scope + /// values instead of splicing unbound producer-scope locals into the + /// caller. Empty for scalar captures and for captures that need no remap. + pub caller_substitutions: Vec<(LocalVarId, ExprId)>, } /// Maximum number of concrete callables tracked in a `Multi` lattice element /// before degrading to `Dynamic`. -pub(super) const MULTI_CAP: usize = 8; +pub(super) const MULTI_CAP: usize = 1000; /// Reaching-definitions lattice for callable variables. /// Tracks the set of possible concrete callables at each program point. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub enum CalleeLattice { /// No value assigned yet (before first definition). Bottom, @@ -180,8 +210,8 @@ impl CalleeLattice { /// - `Bottom ⊔ x = x` /// - `Single(a) ⊔ Single(a) = Single(a)` (when equal) /// - `Single(a) ⊔ Single(b) = Multi([a, b])` - /// - `Multi(s) ⊔ Single(a) = Multi(s ∪ {a})` (cap at `MULTI_CAP` → Dynamic) - /// - `Multi(s1) ⊔ Multi(s2) = Multi(s1 ∪ s2)` (cap at `MULTI_CAP` → Dynamic) + /// - `Multi(s) ⊔ Single(a) = Multi(s ∪ {a})` (cap at `MULTI_CAP` => Dynamic) + /// - `Multi(s1) ⊔ Multi(s2) = Multi(s1 ∪ s2)` (cap at `MULTI_CAP` => Dynamic) /// - `Dynamic ⊔ _ = Dynamic` #[must_use] pub fn join(self, other: Self) -> Self { @@ -227,7 +257,10 @@ impl CalleeLattice { /// `condition`. /// /// - `Single(a)` vs distinct `Single(b)`: `[(a,[condition]), (b,[])]`. - /// - `Single(a)` vs `Multi(s)`: prepend `(a,[condition])`; `s` unchanged. + /// - `Single(a)` vs `Multi(s)`: prepend `(a,[condition])` unconditionally; + /// `s` unchanged. The true-branch arm is kept even when `a` already + /// appears inside `s` under a different guard — collapsing it would drop + /// the `condition` arm. /// - `Multi(s)` vs `Single(b)`: prepend `condition` onto every entry of /// `s`, then append `(b,[])` as the trailing default. /// - `Multi(s1)` vs `Multi(s2)`: if the callable sets are identical (the @@ -252,10 +285,13 @@ impl CalleeLattice { } (Self::Single(a), Self::Multi(mut s)) => { // Prepend the conditioned true-branch entry; `s` supplies the - // implicit `!condition` via sequential ordering. - if !s.iter().any(|(cc, _)| *cc == a) { - s.insert(0, (a, vec![condition])); - } + // implicit `!condition` via sequential ordering. Prepended + // unconditionally: even when `a` already appears inside `s` + // under a different guard, the true-branch arm is a distinct + // dispatch case — deduplicating it against the inner occurrence + // would drop the `condition` arm and reroute that path through + // `s`'s guards instead of unconditionally selecting `a`. + s.insert(0, (a, vec![condition])); if s.len() > MULTI_CAP { Self::Dynamic } else { @@ -279,15 +315,16 @@ impl CalleeLattice { } } // Multi from both branches (nested dispatch on each side). Identical - // callable sets mean the variable was not modified in the branch — - // keep `s1` to stay byte-stable; otherwise merge the two chains. + // dispatch chains — same callables *and* same guards — mean the + // variable was not modified in the branch, so keep `s1` to stay + // byte-stable; otherwise merge the two chains. Comparing callable + // identity alone is unsound: two branches can reassign the local to + // the same set of callables under *different* inner guards (e.g. + // `if rb {X} else {Z}` vs `if rc {X} else {Z}`), and collapsing to + // `s1` would drop the outer condition and reroute the false-branch + // path through the true branch's guards. (Self::Multi(s1), Self::Multi(s2)) => { - let same_callables = s1.len() == s2.len() - && s1 - .iter() - .zip(s2.iter()) - .all(|((cc1, _), (cc2, _))| cc1 == cc2); - if same_callables { + if s1 == s2 { Self::Multi(s1) } else { // Prepend `condition` onto every `s1` guard list; keep `s2` @@ -360,6 +397,7 @@ pub enum ConcreteCallableKey { Closure { target: StoreItemId, functor: FunctorApp, + occurrence: Option, }, } @@ -400,21 +438,40 @@ pub enum Error { /// concrete set of callables, typically because the number of conditional /// branches exceeds `MULTI_CAP`, a conditional has mismatched Multi /// variants, or a mutable callable variable is reassigned in a loop. + /// + /// This diagnostic is also emitted when a captured compound literal — a + /// struct, tuple, or array — cannot be safely rebuilt in the caller's + /// scope. For example, a captured struct field whose value comes from an + /// operation call cannot be duplicated or reordered out of the scope that + /// produced it. Declining such a closure to a dynamic call site keeps the + /// original dispatch and produces this recoverable diagnostic instead of + /// generating incorrect code, which would be a hard error on the base + /// profile. #[error("callable argument could not be resolved statically")] #[diagnostic(code("Qsc.Defunctionalize.DynamicCallable"))] #[diagnostic(help("ensure all callable arguments are known at compile time"))] DynamicCallable(#[label] Span), - /// Emitted when specializing a HOF would re-enter the same - /// `(HOF, concrete-argument)` combination during a single pass — for - /// example, a HOF that calls itself with the same callable argument it - /// received. The recursion guard in `specialize` rejects the duplicate - /// entry rather than looping indefinitely. - #[error("specialization leads to infinite recursion")] - #[diagnostic(code("Qsc.Defunctionalize.RecursiveSpecialization"))] - RecursiveSpecialization(#[label] Span), + /// Emitted when a higher-order function forwards two or more distinct + /// arrays of callables through a single call. The callables are statically + /// resolved, but the combined removal models only one forwarded callable + /// array per call; the multiple-array shape would otherwise fall through to + /// the per-row path and silently collapse each multi-candidate array to a + /// single member. Failing closed here keeps the transform from emitting + /// incorrect output for a shape it does not yet support. + /// + /// Forwarding two or more callable arrays through one call is always + /// declined with this diagnostic rather than partially specialized, so this + /// unsupported shape can never turn into incorrect code. + #[error("higher-order function forwards more than one callable array, which is not supported")] + #[diagnostic(code("Qsc.Defunctionalize.UnsupportedMultipleCallableArrays"))] + #[diagnostic(help( + "pass at most one array-of-callables argument to a higher-order function; combine the \ + arrays or specialize the callers so each forwards a single callable array" + ))] + UnsupportedMultipleCallableArrays(#[label] Span), - /// Emitted when the analysis → specialize → rewrite fixpoint loop exits + /// Emitted when the analysis => specialize => rewrite fixpoint loop exits /// without eliminating every reachable closure or arrow-typed parameter. /// The first field is the iteration count actually reached and the /// second is the number of remaining callable values. Suppressed when diff --git a/source/compiler/qsc_fir_transforms/src/defunctionalize/types/tests.rs b/source/compiler/qsc_fir_transforms/src/defunctionalize/types/tests.rs index 6b80c03ed30..977a79ba83b 100644 --- a/source/compiler/qsc_fir_transforms/src/defunctionalize/types/tests.rs +++ b/source/compiler/qsc_fir_transforms/src/defunctionalize/types/tests.rs @@ -39,6 +39,36 @@ fn join_with_condition_single_multi_inserts_into_set() { } } +#[test] +fn join_with_condition_single_multi_shared_callable_keeps_both_arms() { + // `if cOuter { W } else { if b {W} else {Z} }`: `W` reaches the local from + // the true branch and also from the false-branch inner conditional under a + // different guard. The merge must keep both `W` arms — collapsing the + // true-branch `W` against the inner occurrence would drop the `cOuter` arm + // and reroute that path through `b`'s guard instead of unconditionally + // selecting `W`. + let inner_b = ExprId::from(20u32); + let cc_w = global(1); + let cc_z = global(4); + + let lhs = CalleeLattice::Single(cc_w.clone()); + let rhs = CalleeLattice::Multi(vec![(cc_w.clone(), vec![inner_b]), (cc_z.clone(), vec![])]); + + let result = lhs.join_with_condition(rhs, cond()); + + match result { + CalleeLattice::Multi(entries) => { + assert_eq!(entries.len(), 3); + // The true-branch `W` is prepended with the outer condition and is + // not deduplicated against the false-branch `W`. + assert_eq!(entries[0], (cc_w.clone(), vec![cond()])); + assert_eq!(entries[1], (cc_w, vec![inner_b])); + assert_eq!(entries[2], (cc_z, vec![])); + } + other => panic!("expected Multi, got {other:?}"), + } +} + #[test] fn join_with_condition_multi_single_inserts_into_set() { let a = global(1); diff --git a/source/compiler/qsc_fir_transforms/src/fir_builder.rs b/source/compiler/qsc_fir_transforms/src/fir_builder.rs index 9694487b281..cb2447d3b97 100644 --- a/source/compiler/qsc_fir_transforms/src/fir_builder.rs +++ b/source/compiler/qsc_fir_transforms/src/fir_builder.rs @@ -25,17 +25,21 @@ //! passes should route every `Expr`/`Stmt` allocation through the helpers //! below. +#[cfg(test)] +mod tests; + use crate::EMPTY_EXEC_RANGE; +use qsc_data_structures::functors::FunctorApp; use qsc_data_structures::span::Span; use qsc_fir::assigner::Assigner; use qsc_fir::fir::{ - BinOp, Block, BlockId, CallableDecl, Expr, ExprId, ExprKind, Field, FieldPath, Ident, ItemKind, - LocalItemId, LocalVarId, Mutability, Package, PackageId, PackageLookup, Pat, PatId, PatKind, - Res, SpecDecl, SpecImpl, Stmt, StmtId, StmtKind, StoreItemId, UnOp, + BinOp, Block, BlockId, CallableDecl, Expr, ExprId, ExprKind, Field, FieldPath, Functor, Ident, + ItemId, ItemKind, Lit, LocalItemId, LocalVarId, Mutability, Package, PackageId, PackageLookup, + Pat, PatId, PatKind, Res, SpecDecl, SpecImpl, Stmt, StmtId, StmtKind, StoreItemId, UnOp, }; use rustc_hash::FxHashSet; -use qsc_fir::ty::{Prim, Ty}; +use qsc_fir::ty::{Arrow, Prim, Ty}; use std::rc::Rc; /// Allocates an `Expr` with the given kind and inserts it into the package. @@ -100,6 +104,205 @@ pub(crate) fn alloc_field_expr( ) } +/// Allocates a `Field(record, Path(indices))` expression whose projection +/// path may descend multiple tuple levels in one node. +/// +/// Companion to [`alloc_field_expr`], which projects a single level. +pub(crate) fn alloc_field_path_expr( + package: &mut Package, + assigner: &mut Assigner, + record_id: ExprId, + indices: Vec, + ty: Ty, + span: Span, +) -> ExprId { + alloc_expr( + package, + assigner, + ty, + ExprKind::Field(record_id, Field::Path(FieldPath { indices })), + span, + ) +} + +/// Allocates a `Var(Res::Item(item_id))` expression referencing a global item. +pub(crate) fn alloc_item_var_expr( + package: &mut Package, + assigner: &mut Assigner, + item_id: ItemId, + ty: Ty, + span: Span, +) -> ExprId { + alloc_expr( + package, + assigner, + ty, + ExprKind::Var(Res::Item(item_id), Vec::new()), + span, + ) +} + +/// Allocates a `Call(callee, args)` expression. +pub(crate) fn alloc_call_expr( + package: &mut Package, + assigner: &mut Assigner, + callee_id: ExprId, + args_id: ExprId, + ty: Ty, + span: Span, +) -> ExprId { + alloc_expr( + package, + assigner, + ty, + ExprKind::Call(callee_id, args_id), + span, + ) +} + +/// Allocates an integer literal expression with `Int` type. +pub(crate) fn alloc_int_lit( + package: &mut Package, + assigner: &mut Assigner, + value: i64, + span: Span, +) -> ExprId { + alloc_expr( + package, + assigner, + Ty::Prim(Prim::Int), + ExprKind::Lit(Lit::Int(value)), + span, + ) +} + +/// Strips a single controlled-functor input layer from an arrow type, +/// returning the inner, less-controlled arrow type. +/// +/// `Controlled` turns an operation `I => O` into `(Qubit[], I) => O`, wrapping +/// the input in a `(Qubit[], _)` tuple. This peels one such layer by replacing +/// the arrow input with the second element of that tuple. +/// +/// # Panics +/// +/// Panics if `ty` is not a controlled arrow — a `Ty::Arrow` whose input is a +/// tuple of at least two elements `(Qubit[], _)`. This helper is only ever +/// asked to peel a control layer that the caller's `FunctorApp` already claims +/// exists, so any other shape is an internal compiler bug (a +/// `functor.controlled` count that disagrees with the wrapped type) rather than +/// recoverable input. +fn strip_controlled_input_layer(ty: &Ty) -> Ty { + let Ty::Arrow(arrow) = ty else { + panic!("expected a controlled arrow type to strip a control layer from, found {ty:?}"); + }; + let Ty::Tuple(items) = arrow.input.as_ref() else { + panic!( + "expected a controlled arrow input tuple `(Qubit[], _)`, found input {:?}", + arrow.input + ); + }; + assert!( + items.len() >= 2, + "expected a controlled arrow input tuple `(Qubit[], _)` with at least two elements, found {items:?}" + ); + Ty::Arrow(Box::new(Arrow { + kind: arrow.kind, + input: Box::new(items[1].clone()), + output: arrow.output.clone(), + functors: arrow.functors, + })) +} + +/// Computes the arrow type at each controlled depth of a functor-wrapper +/// chain, from the outermost node down to the base. +/// +/// The returned vector has `controlled + 1` entries: index `0` is `outer_ty` +/// (the fully controlled, outermost node), and each subsequent entry strips one +/// `(Qubit[], _)` input layer, so the last entry is the un-controlled base +/// type. Adjoint is intentionally not modeled here because it preserves the +/// arrow type. +fn controlled_layer_types(outer_ty: &Ty, controlled: u8) -> Vec { + let mut layer_tys = Vec::with_capacity(usize::from(controlled) + 1); + layer_tys.push(outer_ty.clone()); + for _ in 0..controlled { + let inner = strip_controlled_input_layer(layer_tys.last().expect("seeded with outer_ty")); + layer_tys.push(inner); + } + layer_tys +} + +/// Wraps `base_id` in a chain of functor applications (`Adj` then `controlled` +/// layers of `Ctl`) as described by `functor`, allocating one `UnOp` `Expr` +/// per layer. Returns the id of the outermost expression, which equals +/// `base_id` when `functor` requests no functors. +/// +/// `ty` is the arrow type of the outermost (fully functor-wrapped) expression. +/// Each `Controlled` layer wraps the callable's input in another `(Qubit[], _)` +/// tuple, so the layers do not share one type: the outermost `Ctl` node carries +/// `ty`, every inner node strips one control layer, and the base node plus any +/// `Adj` node carry the un-controlled base type `base_id` is assumed to already +/// carry that base type. +pub(crate) fn wrap_in_functors( + package: &mut Package, + assigner: &mut Assigner, + base_id: ExprId, + functor: FunctorApp, + ty: &Ty, + span: Span, +) -> ExprId { + // `layer_tys[0]` is the outermost type; `layer_tys[controlled]` is the base. + let layer_tys = controlled_layer_types(ty, functor.controlled); + let controlled = usize::from(functor.controlled); + + let mut current_id = base_id; + if functor.adjoint { + // Adjoint preserves the arrow type, so this node shares the base type. + current_id = alloc_expr( + package, + assigner, + layer_tys[controlled].clone(), + ExprKind::UnOp(UnOp::Functor(Functor::Adj), current_id), + span, + ); + } + for depth in 1..=controlled { + // The node at control depth `depth` (counted up from the base) carries + // the type with `depth` control layers applied. + current_id = alloc_expr( + package, + assigner, + layer_tys[controlled - depth].clone(), + ExprKind::UnOp(UnOp::Functor(Functor::Ctl), current_id), + span, + ); + } + current_id +} + +/// Allocates a base expression of `base_kind` and wraps it in the functor +/// chain described by `functor` (see [`wrap_in_functors`]). Returns the id of +/// the outermost expression. +/// +/// `ty` is the arrow type of the outermost (fully functor-wrapped) expression. +/// The base node is allocated with the un-controlled base type derived by +/// stripping `functor.controlled` control layers from `ty`; `wrap_in_functors` +/// then re-adds one `(Qubit[], _)` input layer per `Ctl` node back up to `ty`. +pub(crate) fn alloc_functor_wrapped_expr( + package: &mut Package, + assigner: &mut Assigner, + base_kind: ExprKind, + functor: FunctorApp, + ty: &Ty, + span: Span, +) -> ExprId { + let mut base_ty = ty.clone(); + for _ in 0..functor.controlled { + base_ty = strip_controlled_input_layer(&base_ty); + } + let base_id = alloc_expr(package, assigner, base_ty, base_kind, span); + wrap_in_functors(package, assigner, base_id, functor, ty, span) +} + /// Allocates a `BinOp(op, lhs, rhs)` expression. pub(crate) fn alloc_bin_op_expr( package: &mut Package, @@ -208,7 +411,6 @@ pub(crate) fn alloc_unit_expr( } /// Allocates a `Tuple(exprs)` expression. -#[allow(dead_code)] pub(crate) fn alloc_tuple_expr( package: &mut Package, assigner: &mut Assigner, @@ -323,6 +525,26 @@ pub(crate) fn alloc_bind_pat( (local_id, pat_id) } +/// Allocates a `Pat` with `PatKind::Discard` and inserts it into the package. +pub(crate) fn alloc_discard_pat( + package: &mut Package, + assigner: &mut Assigner, + ty: Ty, + span: Span, +) -> PatId { + let pat_id = assigner.next_pat(); + package.pats.insert( + pat_id, + Pat { + id: pat_id, + span, + ty, + kind: PatKind::Discard, + }, + ); + pat_id +} + /// Creates a local variable declaration and returns its `(LocalVarId, StmtId)`. /// /// Combines [`alloc_bind_pat`] + [`alloc_local_stmt`]. diff --git a/source/compiler/qsc_fir_transforms/src/fir_builder/tests.rs b/source/compiler/qsc_fir_transforms/src/fir_builder/tests.rs new file mode 100644 index 00000000000..4216be163a4 --- /dev/null +++ b/source/compiler/qsc_fir_transforms/src/fir_builder/tests.rs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::*; +use crate::PipelineStage; +use crate::test_utils::compile_and_run_pipeline_to; + +// A higher-order operation that applies `Controlled` twice to its callable +// parameter. After defunctionalization the `op` parameter is replaced by the +// concrete `Foo`, so the body's `Controlled Controlled op(...)` callee is +// rebuilt by `alloc_functor_wrapped_expr`/`wrap_in_functors` into a two-layer +// `Ctl(Ctl(Var(Foo)))` wrapper. +const NESTED_CONTROLLED_HOF: &str = r" + operation Foo(q : Qubit) : Unit is Ctl { + X(q); + } + operation ApplyControlledTwice(op : Qubit => Unit is Ctl, cs1 : Qubit[], cs2 : Qubit[], target : Qubit) : Unit { + Controlled Controlled op(cs1, (cs2, target)); + } + operation Main() : Unit { + use cs1 = Qubit(); + use cs2 = Qubit(); + use target = Qubit(); + ApplyControlledTwice(Foo, [cs1], [cs2], target); + } +"; + +// Each `Controlled` wraps a callable's input in one more `(Qubit[], _)` +// register and leaves its output unchanged, so a `Controlled Controlled Foo` +// callee has a distinct arrow type at each of its three levels: +// +// base (Var Foo) Qubit => Unit +// inner Ctl (Qubit[], Qubit) => Unit +// outer Ctl (Qubit[], (Qubit[], Qubit)) => Unit +// +// This test peels the synthesized chain level by level and checks that each +// `Controlled` layer adds exactly one register over the level beneath it. The +// `Defunc` stage is used because that is where the chain is synthesized; later +// passes normalize call-argument types and would mask the defect. +#[test] +fn nested_controlled_callee_adds_one_control_layer_per_wrapper() { + let (store, pkg_id) = compile_and_run_pipeline_to(NESTED_CONTROLLED_HOF, PipelineStage::Defunc); + let package = store.get(pkg_id); + + // Locate the synthesized `Ctl(Ctl(Var(Foo)))` callee and peel it level by level. + let outer_ctl = find_nested_controlled_item_callee(package) + .expect("defunctionalization should synthesize a `Controlled Controlled Foo` callee"); + let inner_ctl = expect_controlled_wrapper(package, outer_ctl); + let base = expect_controlled_wrapper(package, inner_ctl); + + // The base is a direct reference to the concrete item, carrying Foo's + // un-controlled `Qubit => Unit` signature. + let base_expr = package.get_expr(base); + assert!( + matches!(base_expr.kind, ExprKind::Var(Res::Item(_), _)), + "base callee should be a direct item reference to Foo" + ); + let Ty::Arrow(base_arrow) = &base_expr.ty else { + panic!( + "base callee should be arrow-typed, found {:?}", + base_expr.ty + ); + }; + assert_eq!( + *base_arrow.input, + Ty::Prim(Prim::Qubit), + "Foo takes a single Qubit" + ); + assert_eq!(*base_arrow.output, Ty::UNIT, "Foo returns Unit"); + + // Each enclosing `Controlled` adds exactly one `(Qubit[], _)` register. + let base_ty = base_expr.ty.clone(); + let expected_inner_ty = add_control_layer(&base_ty); + let expected_outer_ty = add_control_layer(&expected_inner_ty); + + assert_eq!( + package.get_expr(inner_ctl).ty, + expected_inner_ty, + "inner Controlled node must add one control layer over the base" + ); + assert_eq!( + package.get_expr(outer_ctl).ty, + expected_outer_ty, + "outer Controlled node must add a second control layer" + ); +} + +/// Finds the outermost node of a `Ctl(Ctl(Var(Res::Item)))` callee chain — the +/// shape defunctionalization synthesizes for a twice-controlled item call. +fn find_nested_controlled_item_callee(package: &Package) -> Option { + package.exprs.iter().find_map(|(id, expr)| { + let inner_ctl = controlled_wrapper_inner(expr)?; + let base = controlled_wrapper_inner(package.get_expr(inner_ctl))?; + matches!(package.get_expr(base).kind, ExprKind::Var(Res::Item(_), _)).then_some(id) + }) +} + +/// Returns the wrapped inner expr id when `expr` is a `Controlled` functor +/// wrapper, otherwise `None`. +fn controlled_wrapper_inner(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::UnOp(UnOp::Functor(Functor::Ctl), inner) => Some(*inner), + _ => None, + } +} + +/// Asserts the expression at `id` is a `Controlled` functor wrapper and returns +/// its inner expr id. +fn expect_controlled_wrapper(package: &Package, id: ExprId) -> ExprId { + match controlled_wrapper_inner(package.get_expr(id)) { + Some(inner) => inner, + None => panic!("expected a Controlled functor wrapper at {id:?}"), + } +} + +/// Adds one `Controlled` input layer to an arrow type: `I => O` becomes +/// `(Qubit[], I) => O`. This is the forward mirror of the production +/// `strip_controlled_input_layer` helper, written independently so the test +/// does not lean on the code it validates. +fn add_control_layer(ty: &Ty) -> Ty { + let Ty::Arrow(arrow) = ty else { + panic!("expected an arrow type, found {ty:?}"); + }; + Ty::Arrow(Box::new(Arrow { + kind: arrow.kind, + input: Box::new(Ty::Tuple(vec![ + Ty::Array(Box::new(Ty::Prim(Prim::Qubit))), + (*arrow.input).clone(), + ])), + output: arrow.output.clone(), + functors: arrow.functors, + })) +} diff --git a/source/compiler/qsc_fir_transforms/src/gc_unreachable.rs b/source/compiler/qsc_fir_transforms/src/gc_unreachable.rs index e371fb0acca..b2ca2269345 100644 --- a/source/compiler/qsc_fir_transforms/src/gc_unreachable.rs +++ b/source/compiler/qsc_fir_transforms/src/gc_unreachable.rs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -//! FIR arena garbage collection — runs after argument promotion, before item -//! DCE (and again after item DCE). +//! FIR arena garbage collection — runs immediately after item-level DCE, as +//! the last cleanup before exec graph rebuild. //! //! Tombstones blocks, stmts, exprs, and pats in a package's `IndexMap` arenas //! that are no longer reachable from any callable spec body or the entry diff --git a/source/compiler/qsc_fir_transforms/src/item_dce.rs b/source/compiler/qsc_fir_transforms/src/item_dce.rs index 15fa33cc475..6b9a655bfde 100644 --- a/source/compiler/qsc_fir_transforms/src/item_dce.rs +++ b/source/compiler/qsc_fir_transforms/src/item_dce.rs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -//! Item-level dead code elimination — runs after GC, before exec graph -//! rebuild. +//! Item-level dead code elimination — runs after the tuple-decompose and +//! argument-promotion fixed point, before node-level GC and exec graph rebuild. //! //! Removes items from [`Package::items`](qsc_fir::fir::Package) that became //! unreachable after monomorphization and defunctionalization (original @@ -17,9 +17,9 @@ //! walk, whereas `gc_unreachable` works on a single package's arena nodes. //! - **`StmtKind::Item` edge case.** Removing an item whose declaring //! `StmtKind::Item` stmt sits in a still-reachable block would trip -//! `invariants::check_id_references`. The pipeline mitigates by re-running -//! `gc_unreachable` after item DCE when anything was removed, tombstoning the -//! deleted items' arena nodes. The `StmtKind::Item` stmts survive as harmless +//! `invariants::check_id_references`. The pipeline mitigates by running +//! `gc_unreachable` immediately after item DCE, tombstoning the deleted +//! items' arena nodes. The `StmtKind::Item` stmts survive as harmless //! dangling references (allowed post-DCE; ignored by `exec_graph_rebuild`). //! - Accepts entry-rooted or seed-expanded (pinned-callable) reachability. diff --git a/source/compiler/qsc_fir_transforms/src/lib.rs b/source/compiler/qsc_fir_transforms/src/lib.rs index f16bdbd0216..8b43f9a7777 100644 --- a/source/compiler/qsc_fir_transforms/src/lib.rs +++ b/source/compiler/qsc_fir_transforms/src/lib.rs @@ -256,7 +256,6 @@ pub enum PipelineStage { /// /// In every fatal case the intermediate FIR intentionally violates downstream /// invariants, so running later passes would produce misleading failures. -#[allow(clippy::too_many_lines)] fn run_pipeline_to_impl( store: &mut PackageStore, package_id: PackageId, @@ -323,7 +322,56 @@ fn run_pipeline_to_impl( &skipped, ); - let defunc_diagnostics = defunctionalize::defunctionalize(store, package_id, &mut assigners); + let defunc_lowering_done = run_defunc_and_lowering_stages( + store, + package_id, + stage, + &mut result, + &mut assigners, + &skipped, + ); + if defunc_lowering_done { + return result; + } + + if run_arg_promote_stages( + store, + package_id, + stage, + &mut result, + &mut assigners, + &skipped, + ) { + return result; + } + + finalize_pipeline( + store, + package_id, + stage, + &mut result, + pinned_items, + &skipped, + ); + result +} + +/// Runs the defunctionalization and structural lowering stages: defunc, UDT +/// erasure, tuple-comparison lowering, and tuple decomposition, checking the +/// matching invariant after each. +/// +/// Returns `true` when the requested `stage` is reached (or a fatal +/// defunctionalization error occurs), signalling the caller to stop and return +/// the accumulated `result`. +fn run_defunc_and_lowering_stages( + store: &mut PackageStore, + package_id: PackageId, + stage: PipelineStage, + result: &mut PipelineResult, + assigners: &mut PackageAssigners, + skipped: &FxHashSet, +) -> bool { + let defunc_diagnostics = defunctionalize::defunctionalize(store, package_id, assigners); let (warnings, fatal_errors): (Vec<_>, Vec<_>) = defunc_diagnostics .into_iter() .partition(defunctionalize::Error::is_warning); @@ -335,103 +383,123 @@ fn run_pipeline_to_impl( .extend(warnings.into_iter().map(PipelineError::from)); if !fatal_errors.is_empty() { result.errors = fatal_errors.into_iter().map(PipelineError::from).collect(); - return result; + return true; } invariants::check_with_skip( store, package_id, invariants::InvariantLevel::PostDefunc, - &skipped, + skipped, ); if matches!(stage, PipelineStage::Defunc) { - return result; + return true; } - udt_erase::erase_udts(store, package_id, &mut assigners); + udt_erase::erase_udts(store, package_id, assigners); invariants::check_with_skip( store, package_id, invariants::InvariantLevel::PostUdtErase, - &skipped, + skipped, ); if matches!(stage, PipelineStage::UdtErase) { - return result; + return true; } - tuple_compare_lower::lower_tuple_comparisons(store, package_id, &mut assigners); + tuple_compare_lower::lower_tuple_comparisons(store, package_id, assigners); invariants::check_with_skip( store, package_id, invariants::InvariantLevel::PostTupleCompLower, - &skipped, + skipped, ); if matches!(stage, PipelineStage::TupleCompLower) { - return result; + return true; } - tuple_decompose::tuple_decompose(store, package_id, &mut assigners); + tuple_decompose::tuple_decompose(store, package_id, assigners); invariants::check_with_skip( store, package_id, invariants::InvariantLevel::PostTupleDecompose, - &skipped, + skipped, ); - if matches!(stage, PipelineStage::TupleDecompose) { - return result; - } + matches!(stage, PipelineStage::TupleDecompose) +} - arg_promote::arg_promote(store, package_id, &mut assigners); +/// Runs the argument-promotion stages: the initial `arg_promote`, the +/// tuple-decompose/arg-promote fixed point, and the one-shot post-loop +/// call-argument-type normalization, checking `PostArgPromote` after each. +/// +/// Returns `true` when the requested `stage` is reached, signalling the caller +/// to stop and return the accumulated `result`. +fn run_arg_promote_stages( + store: &mut PackageStore, + package_id: PackageId, + stage: PipelineStage, + result: &mut PipelineResult, + assigners: &mut PackageAssigners, + skipped: &FxHashSet, +) -> bool { + arg_promote::arg_promote(store, package_id, assigners); invariants::check_with_skip( store, package_id, invariants::InvariantLevel::PostArgPromote, - &skipped, + skipped, ); if matches!(stage, PipelineStage::ArgPromote) { - return result; + return true; } - tuple_decompose_arg_promote_fixed_point( - store, - package_id, - &mut result, - &mut assigners, - &skipped, - ); + tuple_decompose_arg_promote_fixed_point(store, package_id, result, assigners, skipped); // Call-argument-type normalization is idempotent and candidate-neutral, so // it is hoisted to run exactly once after the loop converges rather than // per round (per-round runs cause `(T,)` wrapping churn that pollutes // change detection). - arg_promote::normalize_reachable_call_arg_types(store, package_id, &mut assigners); + arg_promote::normalize_reachable_call_arg_types(store, package_id, assigners); invariants::check_with_skip( store, package_id, invariants::InvariantLevel::PostArgPromote, - &skipped, + skipped, ); - if matches!(stage, PipelineStage::TupleDecompose2) { - return result; - } + matches!(stage, PipelineStage::TupleDecompose2) +} +/// Runs the backend stages after all structural transforms: pinned-item +/// validation, item dead-code elimination, execution-graph rebuild, and the +/// final `PostAll` invariant walk. +/// +/// Mutates `result` in place; a fatal pinned-item validation error stops the +/// backend early with the errors recorded on `result`. +fn finalize_pipeline( + store: &mut PackageStore, + package_id: PackageId, + stage: PipelineStage, + result: &mut PipelineResult, + pinned_items: &[StoreItemId], + skipped: &FxHashSet, +) { // Item DCE: remove unreachable callable items and dead type items. // Callers may pin items via `pinned_items` to keep them (and their // transitive dependencies) alive through DCE and exec-graph-rebuild. let pinned_errors = validate_pinned_items(store, pinned_items); if !pinned_errors.is_empty() { result.errors = pinned_errors; - return result; + return; } run_item_dce_and_gc(store, package_id, pinned_items); invariants::check_with_skip( store, package_id, invariants::InvariantLevel::PostItemDce, - &skipped, + skipped, ); if matches!(stage, PipelineStage::ItemDce) { - return result; + return; } // Exec graphs are rebuilt unconditionally for every reachable spec in every @@ -441,7 +509,7 @@ fn run_pipeline_to_impl( // graphs are validated by the `PostAll` invariant walk below. exec_graph_rebuild::rebuild_exec_graphs(store, package_id, pinned_items); if matches!(stage, PipelineStage::ExecGraphRebuild) { - return result; + return; } // PostAll uses entry-only reachability. Pinned items (original target kept @@ -450,9 +518,8 @@ fn run_pipeline_to_impl( store, package_id, invariants::InvariantLevel::PostAll, - &skipped, + skipped, ); - result } /// Fixed-point loop over tuple-decompose and argument promotion. `arg_promote` diff --git a/source/compiler/qsc_fir_transforms/src/monomorphize.rs b/source/compiler/qsc_fir_transforms/src/monomorphize.rs index 73e72452f7f..4ea7829fe57 100644 --- a/source/compiler/qsc_fir_transforms/src/monomorphize.rs +++ b/source/compiler/qsc_fir_transforms/src/monomorphize.rs @@ -12,14 +12,15 @@ //! # What to know before diving in //! //! - **Establishes [`crate::invariants::InvariantLevel::PostMono`]:** no -//! `Ty::Param` and no non-empty `ExprKind::Var` generic-argument lists -//! remain in reachable code. +//! `Ty::Param`, no `FunctorSet::Param`, no non-empty `ExprKind::Var` +//! generic-argument lists, and no reachable generic callable items remain +//! in reachable code. //! - **Three phases:** *Discovery* collects concrete generic references; //! *Specialization* drives a worklist that clones each body, substitutes //! type params, and feeds back transitive generic references it finds; -//! *Rewrite* redirects call sites across every reachable package and (via -//! `collect_rewrite_scope_for_package`) walks closure items so generic call -//! sites in lifted lambdas are not missed. +//! *Rewrite* redirects call sites and closure targets across every reachable +//! package and (via `collect_rewrite_scope_for_package`) walks closure items +//! so generic sites in lifted lambdas are not missed. //! - **Special cases:** identity instantiations (`[Param(0), ...]`) are //! skipped (they would duplicate the original); intrinsics get their //! argument lists cleared in place with no new callable; generic references @@ -38,15 +39,16 @@ use crate::fir_builder::{functored_specs, reachable_local_callables}; use crate::package_assigners::PackageAssigners; use crate::reachability::{collect_reachable_from_entry, collect_reachable_package_closure}; use crate::walk_utils::{ - collect_expr_ids_in_entry_and_local_callables, collect_expr_ids_in_local_callables, - extend_expr_ids_in_local_callables, + CallableNode, collect_expr_ids_in_entry_and_local_callables, + collect_expr_ids_in_local_callables, extend_expr_ids_in_local_callables, + for_each_node_from_expr_root, for_each_node_in_callable, }; use qsc_fir::fir::{ BlockId, CallableDecl, CallableImpl, ExprId, ExprKind, Ident, Item, ItemId, ItemKind, LocalItemId, LocalVarId, Package, PackageId, PackageLookup, PackageStore, PatId, PatKind, Res, StmtId, StmtKind, StoreItemId, Visibility, }; -use qsc_fir::ty::{Arrow, FunctorSet, GenericArg, ParamId, Ty}; +use qsc_fir::ty::{Arrow, FunctorSet, GenericArg, ParamId, Ty, TypeParameter}; use rustc_hash::{FxHashMap, FxHashSet}; use std::collections::VecDeque; use std::rc::Rc; @@ -128,13 +130,13 @@ fn assert_no_reachable_generic(store: &PackageStore, entry_pkg_id: PackageId) { } } -/// Rewrites generic call sites in every reachable package. +/// Rewrites generic call sites and generic closure targets in every reachable package. /// /// For each package in the entry-reachable closure, collects the rewrite /// scope (reachable callables, the entry expression for the entry package, /// the new specializations owned by that package, and their transitive /// closures) and redirects `ExprKind::Var(Item(generic), [concrete])` sites -/// to the matching specialization. +/// plus `ExprKind::Closure` targets to the matching specialization. fn rewrite_all_packages( store: &mut PackageStore, entry_pkg_id: PackageId, @@ -164,10 +166,108 @@ fn rewrite_all_packages( let empty: Vec = Vec::new(); for &pkg_id in &packages { + // This package's own fresh specializations (empty for packages that + // received none). They must be threaded through explicitly because they + // are not yet reachable from entry — nothing points at them until the + // call sites below are redirected. let new_specs = new_specs_by_pkg.get(&pkg_id).unwrap_or(&empty); + + // Gather the set of expressions to rewrite while the store is still + // borrowed immutably. This has to happen before `get_mut` below, because + // computing the scope reads across packages (reachable callables, the + // entry expression, and the transitive closure items), which cannot + // coexist with the mutable borrow the rewrites require. let expr_ids = collect_rewrite_scope_for_package(store, entry_pkg_id, pkg_id, &reachable, new_specs); - rewrite_call_sites(store.get_mut(pkg_id), pkg_id, &lookup, &expr_ids); + + // Now take the mutable borrow of just this package and apply both + // rewrites to it. + let package = store.get_mut(pkg_id); + + // Redirect direct call sites first: this is what actually makes the new + // specializations reachable from entry. + rewrite_call_sites(package, pkg_id, &lookup, &expr_ids); + + // Then repoint generic closure targets. This runs after the call-site + // rewrite (and drives its own worklist over the now-reachable + // specializations) so closures nested in freshly reachable bodies are + // not missed. + rewrite_closure_targets_in_package( + package, + pkg_id, + entry_pkg_id, + &reachable, + new_specs, + &lookup, + ); + } +} + +/// Redirects every generic `ExprKind::Closure` target in a package to its +/// concrete specialization, following closures nested inside other closures. +/// +/// A closure whose target callable is generic (e.g. a lambda over `Identity<'T>`) +/// must be repointed at the monomorphized clone, just like a direct call site. +/// Because a specialized body can itself contain fresh closures that are not yet +/// reachable from entry, this drives a worklist that discovers newly referenced +/// closure targets as it rewrites, so no nested generic closure is missed. +/// +/// # Transformation +/// +/// ```text +/// // before: closure targets the generic item +/// Closure([...captures], Identity) // Identity is generic <'T> +/// // after: repointed at the concrete clone for the inferred args +/// Closure([...captures], Identity) +/// ``` +/// +/// The seed worklist is every reachable callable in the package plus the newly +/// created specializations owned by it (which are not reachable from entry until +/// call sites are redirected). For the entry package the entry expression is +/// rewritten directly and its closure targets are added as extra seeds. +fn rewrite_closure_targets_in_package( + package: &mut Package, + pkg_id: PackageId, + entry_pkg_id: PackageId, + reachable: &FxHashSet, + new_spec_items: &[LocalItemId], + lookup: &FxHashMap, +) { + // Seed the worklist with every callable reachable in this package, plus the + // specializations just created for it (still unreachable from entry until + // call sites are redirected, so they must be added explicitly). + let mut worklist: Vec = reachable_local_callables(package, pkg_id, reachable) + .map(|(id, _)| id) + .collect(); + worklist.extend_from_slice(new_spec_items); + + // The entry expression lives outside any callable item, so rewrite it here + // and feed the closure items it references back into the worklist. + if pkg_id == entry_pkg_id + && let Some(entry_id) = package.entry + { + worklist.extend(collect_closure_targets_in_expr_root(package, entry_id)); + rewrite_closure_targets_in_expr_root(package, pkg_id, entry_id, lookup); + } + + // Process each callable once. Rewriting a body can surface further closure + // items (closures nested in closures), which are appended to the worklist + // and picked up on a later iteration until the set closes. + let mut seen = FxHashSet::default(); + while let Some(item_id) = worklist.pop() { + if !seen.insert(item_id) { + continue; + } + let Some(item) = package.items.get(item_id) else { + continue; + }; + let ItemKind::Callable(decl) = &item.kind else { + continue; + }; + // Discover any closure targets this body references, then repoint the + // generic ones at their concrete specializations. + worklist.extend(collect_closure_targets_in_callable(package, decl)); + rewrite_closure_targets_in_callable(package, pkg_id, item_id, lookup); } } @@ -240,7 +340,7 @@ fn discover_instantiations( // Walk the entry expression. if let Some(entry_id) = package.entry { - collect_generic_refs_in_expr(package, entry_id, &mut found, &mut seen_keys); + collect_generic_refs_in_expr(package_id, package, entry_id, &mut found, &mut seen_keys); } // Walk every reachable callable body. @@ -253,7 +353,13 @@ fn discover_instantiations( continue; }; if let ItemKind::Callable(decl) = &item.kind { - collect_generic_refs_in_callable(pkg, decl, &mut found, &mut seen_keys); + collect_generic_refs_in_callable( + item_id.package, + pkg, + decl, + &mut found, + &mut seen_keys, + ); } } @@ -303,52 +409,403 @@ fn mono_name(decl: &CallableDecl, args: &[GenericArg]) -> Rc { Rc::from(name.as_str()) } -/// Walks a callable's body collecting every `(StoreItemId, Vec)` -/// pair referenced by `ExprKind::Var(Res::Item(..), args)` with non-empty -/// generic arguments, deduplicated via `mono_key` in `seen`. +/// Walks a callable's body collecting every concrete generic reference from +/// `ExprKind::Var(Res::Item(..), args)` sites and `ExprKind::Closure` targets, +/// deduplicated via `mono_key` in `seen`. fn collect_generic_refs_in_callable( + pkg_id: PackageId, pkg: &Package, decl: &CallableDecl, found: &mut Vec<(StoreItemId, Vec)>, seen: &mut FxHashSet, ) { - crate::walk_utils::for_each_expr_in_callable_impl( - pkg, - &decl.implementation, - &mut |_eid, expr| { - if let ExprKind::Var(Res::Item(item_id), generic_args) = &expr.kind - && !generic_args.is_empty() - { - let store_id = StoreItemId::from((item_id.package, item_id.item)); - let key = mono_key(store_id, generic_args); - if seen.insert(key) { - found.push((store_id, generic_args.clone())); - } - } - }, - ); + let (expr_ids, local_tys) = collect_expr_ids_and_local_tys_in_callable(pkg, decl); + collect_generic_refs_in_expr_ids(pkg_id, pkg, &expr_ids, &local_tys, found, seen); } /// Walks a single expression subtree collecting `(StoreItemId, Vec)` /// pairs the same way as [`collect_generic_refs_in_callable`], used for the /// package entry expression. fn collect_generic_refs_in_expr( + pkg_id: PackageId, pkg: &Package, expr_id: ExprId, found: &mut Vec<(StoreItemId, Vec)>, seen: &mut FxHashSet, ) { - crate::walk_utils::for_each_expr(pkg, expr_id, &mut |_eid, expr| { - if let ExprKind::Var(Res::Item(item_id), generic_args) = &expr.kind - && !generic_args.is_empty() - { - let store_id = StoreItemId::from((item_id.package, item_id.item)); - let key = mono_key(store_id, generic_args); - if seen.insert(key) { - found.push((store_id, generic_args.clone())); + let (expr_ids, local_tys) = collect_expr_ids_and_local_tys_in_expr_root(pkg, expr_id); + collect_generic_refs_in_expr_ids(pkg_id, pkg, &expr_ids, &local_tys, found, seen); +} + +/// Scans a flat list of expression ids for the two kinds of generic reference +/// this pass must specialize, recording each unique `(callable, args)` pair. +/// +/// A `Var(Res::Item(id), args)` with a non-empty `args` list is a direct +/// generic reference (a call or a first-class use). A `Closure(captures, item)` +/// may target a generic lambda whose concrete type arguments are not written +/// out anywhere, so they are reconstructed from the capture and arrow types via +/// [`infer_closure_generic_args`]. Both kinds are deduplicated through `seen`. +fn collect_generic_refs_in_expr_ids( + pkg_id: PackageId, + pkg: &Package, + expr_ids: &[ExprId], + local_tys: &FxHashMap, + found: &mut Vec<(StoreItemId, Vec)>, + seen: &mut FxHashSet, +) { + for &expr_id in expr_ids { + let expr = pkg.get_expr(expr_id); + match &expr.kind { + // A named reference that carries generic args (e.g. `Identity`): + // record it directly. + ExprKind::Var(Res::Item(item_id), generic_args) if !generic_args.is_empty() => { + let store_id = StoreItemId::from((item_id.package, item_id.item)); + record_generic_ref(store_id, generic_args.clone(), found, seen); } + // A closure may point at a generic lambda without spelling out its + // args; recover them from the closure's concrete type before + // recording. A non-generic (or un-inferable) closure is skipped. + ExprKind::Closure(captures, local_item_id) => { + let store_id = StoreItemId::from((pkg_id, *local_item_id)); + if let Some(generic_args) = + infer_closure_generic_args(pkg, *local_item_id, captures, &expr.ty, local_tys) + { + record_generic_ref(store_id, generic_args, found, seen); + } + } + _ => {} } + } +} + +/// Records a `(callable, args)` pair into `found`, keyed and deduplicated by its +/// [`mono_key`] so the same instantiation is never queued for specialization +/// twice. +fn record_generic_ref( + store_id: StoreItemId, + generic_args: Vec, + found: &mut Vec<(StoreItemId, Vec)>, + seen: &mut FxHashSet, +) { + let key = mono_key(store_id, &generic_args); + if seen.insert(key) { + found.push((store_id, generic_args)); + } +} + +/// Walks a callable's body once, returning every expression id it contains and +/// a map from each binding's `LocalVarId` to its type. +/// +/// The local-type map lets [`infer_closure_generic_args`] resolve a closure's +/// capture types, since captures are referenced by `LocalVarId` rather than by +/// carrying their own type. +fn collect_expr_ids_and_local_tys_in_callable( + pkg: &Package, + decl: &CallableDecl, +) -> (Vec, FxHashMap) { + let mut expr_ids = Vec::new(); + let mut local_tys = FxHashMap::default(); + for_each_node_in_callable(pkg, decl, &mut |node| match node { + CallableNode::Expr(expr_id) => expr_ids.push(expr_id), + CallableNode::Pat(pat_id) => collect_pat_bind_ty(pkg, pat_id, &mut local_tys), + CallableNode::Block(_) | CallableNode::Stmt(_) => {} }); + (expr_ids, local_tys) +} + +/// Same as [`collect_expr_ids_and_local_tys_in_callable`] but walks a single +/// root expression subtree, used for the package entry expression. +fn collect_expr_ids_and_local_tys_in_expr_root( + pkg: &Package, + expr_id: ExprId, +) -> (Vec, FxHashMap) { + let mut expr_ids = Vec::new(); + let mut local_tys = FxHashMap::default(); + for_each_node_from_expr_root(pkg, expr_id, &mut |node| match node { + CallableNode::Expr(expr_id) => expr_ids.push(expr_id), + CallableNode::Pat(pat_id) => collect_pat_bind_ty(pkg, pat_id, &mut local_tys), + CallableNode::Block(_) | CallableNode::Stmt(_) => {} + }); + (expr_ids, local_tys) +} + +/// Records a single `LocalVarId -> Ty` entry when `pat_id` is a `Bind` pattern. +/// Tuple and discard patterns bind no single local directly, so they add +/// nothing (their leaf `Bind`s are visited separately by the walker). +fn collect_pat_bind_ty(pkg: &Package, pat_id: PatId, local_tys: &mut FxHashMap) { + let pat = pkg.get_pat(pat_id); + if let PatKind::Bind(ident) = &pat.kind { + local_tys.insert(ident.id, pat.ty.clone()); + } +} + +/// Returns the target item id of every `Closure` reachable from a root +/// expression (the entry expression), used to seed the rewrite worklist. +fn collect_closure_targets_in_expr_root(pkg: &Package, expr_id: ExprId) -> Vec { + let (expr_ids, _) = collect_expr_ids_and_local_tys_in_expr_root(pkg, expr_id); + collect_closure_targets_in_expr_ids(pkg, &expr_ids) +} + +/// Returns the target item id of every `Closure` in a callable body, used to +/// discover lambdas nested inside a body so the worklist can visit them too. +fn collect_closure_targets_in_callable(pkg: &Package, decl: &CallableDecl) -> Vec { + let (expr_ids, _) = collect_expr_ids_and_local_tys_in_callable(pkg, decl); + collect_closure_targets_in_expr_ids(pkg, &expr_ids) +} + +/// Filters a flat expression list down to the target item ids of its `Closure` +/// nodes. +fn collect_closure_targets_in_expr_ids(pkg: &Package, expr_ids: &[ExprId]) -> Vec { + expr_ids + .iter() + .filter_map(|&expr_id| match pkg.get_expr(expr_id).kind { + ExprKind::Closure(_, local_item_id) => Some(local_item_id), + _ => None, + }) + .collect() +} + +/// Repoints the generic `Closure` targets in a single callable body at their +/// concrete specializations (per [`rewrite_closure_targets_in_expr_ids`]). +fn rewrite_closure_targets_in_callable( + pkg: &mut Package, + pkg_id: PackageId, + item_id: LocalItemId, + lookup: &FxHashMap, +) { + let Some(item) = pkg.items.get(item_id) else { + return; + }; + let ItemKind::Callable(decl) = &item.kind else { + return; + }; + let (expr_ids, local_tys) = collect_expr_ids_and_local_tys_in_callable(pkg, decl); + rewrite_closure_targets_in_expr_ids(pkg, pkg_id, &expr_ids, &local_tys, lookup); +} + +/// Repoints the generic `Closure` targets in a root expression (the entry +/// expression) at their concrete specializations. +fn rewrite_closure_targets_in_expr_root( + pkg: &mut Package, + pkg_id: PackageId, + expr_id: ExprId, + lookup: &FxHashMap, +) { + let (expr_ids, local_tys) = collect_expr_ids_and_local_tys_in_expr_root(pkg, expr_id); + rewrite_closure_targets_in_expr_ids(pkg, pkg_id, &expr_ids, &local_tys, lookup); +} + +/// Repoints each generic `Closure` target in `expr_ids` at the matching +/// concrete specialization found in `lookup`. +/// +/// A closure carries no explicit generic-argument list, so the target's +/// concrete args are inferred from the closure's capture and arrow types, +/// keyed with [`mono_key`], and looked up. A closure is left unchanged when its +/// target is not generic, no matching specialization exists, or the +/// specialization lives in another package (a `Closure` target id is only +/// meaningful inside its own package's arena). +fn rewrite_closure_targets_in_expr_ids( + pkg: &mut Package, + pkg_id: PackageId, + expr_ids: &[ExprId], + local_tys: &FxHashMap, + lookup: &FxHashMap, +) { + for &expr_id in expr_ids { + // Only closures are candidates; skip everything else. + let Some((captures, local_item_id, expr_ty)) = closure_parts(pkg, expr_id) else { + continue; + }; + // Recover the concrete generic args from the closure's type; a + // non-generic or un-inferable closure has none and is left alone. + let Some(generic_args) = + infer_closure_generic_args(pkg, local_item_id, &captures, &expr_ty, local_tys) + else { + continue; + }; + // Find the specialization created for this exact instantiation. + let key = mono_key(StoreItemId::from((pkg_id, local_item_id)), &generic_args); + let Some(new_item_id) = lookup.get(&key).copied() else { + continue; + }; + // A closure target id is package-local, so a foreign specialization + // cannot be named here; leave it for that package's own rewrite pass. + if new_item_id.package != pkg_id { + continue; + } + // Redirect the closure to the concrete clone. + let expr = pkg.exprs.get_mut(expr_id).expect("expr should exist"); + if let ExprKind::Closure(_, target) = &mut expr.kind { + *target = new_item_id.item; + } + } +} + +/// Extracts the `(captures, target item, closure type)` of a `Closure` +/// expression, or `None` for any other kind. The parts are cloned out so the +/// shared borrow ends before the package is mutated in place by the caller. +fn closure_parts(pkg: &Package, expr_id: ExprId) -> Option<(Vec, LocalItemId, Ty)> { + let expr = pkg.get_expr(expr_id); + if let ExprKind::Closure(captures, local_item_id) = &expr.kind { + Some((captures.clone(), *local_item_id, expr.ty.clone())) + } else { + None + } +} + +/// Reconstructs the concrete generic arguments of a closure whose target is a +/// generic lambda, by unifying the lambda's declared (generic) signature +/// against the closure's actual capture and arrow types. +/// +/// A closure expression carries no explicit generic-argument list, but its FIR +/// type is fully concrete and its captures thread in the outer values. The +/// lambda item's input pattern is `(captures..., original_input)`, so the +/// actual input is rebuilt as a tuple of the capture types followed by the +/// closure arrow's input, then unified position-by-position against the +/// lambda's formal input, output, and functor set to solve each parameter. +/// +/// # Example +/// ```text +/// // generic lambda item: <'T>(cap : 'T, x : 'T) -> 'T +/// // closure expr type: (Int) -> Int, capturing `cap : Int` +/// // rebuilt actual input: (Int, Int) => unify 'T := Int +/// // inferred args: ['T = Int] +/// ``` +/// +/// Returns `None` when the target is non-generic, its type is not an arrow, a +/// capture type is unknown, or unification fails or leaves a parameter unsolved. +fn infer_closure_generic_args( + pkg: &Package, + local_item_id: LocalItemId, + captures: &[LocalVarId], + closure_ty: &Ty, + local_tys: &FxHashMap, +) -> Option> { + // Only a generic callable target has args to infer. + let ItemKind::Callable(decl) = &pkg.get_item(local_item_id).kind else { + return None; + }; + if decl.generics.is_empty() { + return None; + } + // The closure's own type must be an arrow to unify against. + let Ty::Arrow(closure_arrow) = closure_ty else { + return None; + }; + + // Rebuild the lambda's actual input as `(capture_tys..., closure_input)` to + // match the lambda item's `(captures..., original_input)` parameter shape. + // Bail if any capture's type is unknown. + let mut arg_map = FxHashMap::default(); + let capture_tys: Vec<_> = captures + .iter() + .map(|local| local_tys.get(local).cloned()) + .collect::>()?; + let actual_input = Ty::Tuple( + capture_tys + .into_iter() + .chain(std::iter::once((*closure_arrow.input).clone())) + .collect(), + ); + + // Unify formal-vs-actual input, output, and functors; each step solves more + // parameters into `arg_map`. Any shape mismatch aborts the inference. + if !infer_generic_ty_args(&pkg.get_pat(decl.input).ty, &actual_input, &mut arg_map) { + return None; + } + if !infer_generic_ty_args(&decl.output, &closure_arrow.output, &mut arg_map) { + return None; + } + if !infer_generic_functor_args( + FunctorSet::Value(decl.functors), + closure_arrow.functors, + &mut arg_map, + ) { + return None; + } + + // Emit one arg per declared parameter in order; if any went unsolved (or + // resolved to the wrong kind), the whole inference fails. + decl.generics + .iter() + .enumerate() + .map( + |(idx, param)| match (param, arg_map.get(&ParamId::from(idx))) { + (TypeParameter::Ty { .. }, Some(GenericArg::Ty(ty))) => { + Some(GenericArg::Ty(ty.clone())) + } + (TypeParameter::Functor(_), Some(GenericArg::Functor(functors))) => { + Some(GenericArg::Functor(*functors)) + } + _ => None, + }, + ) + .collect() +} + +/// Structurally unifies a `formal` (possibly generic) type against a concrete +/// `actual` type, recording every solved type/functor parameter in `arg_map`. +/// +/// A bare `Ty::Param` binds directly to whatever `actual` is; compound types +/// (array, arrow, tuple) recurse into their matching components; leaf types +/// must be equal. Returns `false` on any shape mismatch or a conflicting +/// re-binding of an already-solved parameter. +fn infer_generic_ty_args( + formal: &Ty, + actual: &Ty, + arg_map: &mut FxHashMap, +) -> bool { + match (formal, actual) { + (Ty::Param(param), _) => { + record_inferred_arg(*param, GenericArg::Ty(actual.clone()), arg_map) + } + (Ty::Array(formal), Ty::Array(actual)) => infer_generic_ty_args(formal, actual, arg_map), + (Ty::Arrow(formal), Ty::Arrow(actual)) => { + formal.kind == actual.kind + && infer_generic_ty_args(&formal.input, &actual.input, arg_map) + && infer_generic_ty_args(&formal.output, &actual.output, arg_map) + && infer_generic_functor_args(formal.functors, actual.functors, arg_map) + } + (Ty::Tuple(formal), Ty::Tuple(actual)) if formal.len() == actual.len() => formal + .iter() + .zip(actual) + .all(|(formal, actual)| infer_generic_ty_args(formal, actual, arg_map)), + (Ty::Prim(formal), Ty::Prim(actual)) => formal == actual, + (Ty::Udt(formal), Ty::Udt(actual)) => formal == actual, + (Ty::Infer(formal), Ty::Infer(actual)) => formal == actual, + (Ty::Err, Ty::Err) => true, + _ => false, + } +} + +/// Unifies a `formal` functor set against a concrete `actual` one: a +/// `FunctorSet::Param` binds to `actual`, otherwise the two must be equal. +fn infer_generic_functor_args( + formal: FunctorSet, + actual: FunctorSet, + arg_map: &mut FxHashMap, +) -> bool { + match formal { + FunctorSet::Param(param) => { + record_inferred_arg(param, GenericArg::Functor(actual), arg_map) + } + _ => formal == actual, + } +} + +/// Records a solved parameter binding, returning `false` if the parameter was +/// already bound to a different argument (an inconsistent unification). +fn record_inferred_arg( + param: ParamId, + arg: GenericArg, + arg_map: &mut FxHashMap, +) -> bool { + if let Some(existing) = arg_map.get(¶m) { + existing == &arg + } else { + arg_map.insert(param, arg); + true + } } /// Returns `true` when all generic args map to their own parameter position — @@ -389,16 +846,17 @@ fn ty_contains_param(ty: &Ty) -> bool { } } -/// Walks a cloned callable body and collects every -/// `ExprKind::Var(Res::Item(id), args)` where `args` is non-empty and fully -/// concrete (no remaining `Ty::Param` or `FunctorSet::Param`). +/// Walks a cloned callable body and collects every fully concrete generic +/// reference from `ExprKind::Var(Res::Item(id), args)` sites and +/// `ExprKind::Closure` targets. fn scan_for_concrete_generic_refs( + pkg_id: PackageId, pkg: &Package, decl: &CallableDecl, ) -> Vec<(StoreItemId, Vec)> { let mut found = Vec::new(); let mut seen = FxHashSet::default(); - collect_generic_refs_in_callable(pkg, decl, &mut found, &mut seen); + collect_generic_refs_in_callable(pkg_id, pkg, decl, &mut found, &mut seen); found.retain(|(_, args)| is_fully_concrete(args)); found } @@ -478,7 +936,6 @@ fn create_specializations( specializations } -#[allow(clippy::too_many_lines)] /// Clones a single `(callable, args)` pair into its owning package, substitutes /// type parameters, and returns the new item id plus any concrete generic /// references discovered in the cloned body that require their own @@ -558,6 +1015,23 @@ fn specialize_one( }; owning_pkg.items.insert(new_local_id, new_item); + let refs_out = collect_new_generic_refs(owning_pkg, owning_pkg_id, new_local_id); + + (new_item_id, refs_out) +} + +/// Scans a freshly created monomorphized callable for concrete generic +/// references that require their own specializations. +/// +/// References to items already non-generic in the owning package (for example +/// self-references from a recursive callable remapped by `set_self_item_remap`) +/// are dropped; every other concrete reference is returned for the caller to +/// enqueue. +fn collect_new_generic_refs( + owning_pkg: &Package, + owning_pkg_id: PackageId, + new_local_id: LocalItemId, +) -> Vec<(StoreItemId, Vec)> { // Scan the newly created callable for additional concrete generic // references that need their own specializations. Skip references to // items in the owning package that are already non-generic (e.g., @@ -566,7 +1040,7 @@ fn specialize_one( let mut refs_out = Vec::new(); let created_item = owning_pkg.items.get(new_local_id).expect("just inserted"); if let ItemKind::Callable(created_decl) = &created_item.kind { - let new_refs = scan_for_concrete_generic_refs(owning_pkg, created_decl); + let new_refs = scan_for_concrete_generic_refs(owning_pkg_id, owning_pkg, created_decl); for (ref_id, ref_args) in new_refs { if ref_id.package == owning_pkg_id && let Some(ref_item) = owning_pkg.items.get(ref_id.item) @@ -578,8 +1052,7 @@ fn specialize_one( refs_out.push((ref_id, ref_args)); } } - - (new_item_id, refs_out) + refs_out } /// Builds a standalone `Package` holding all nodes transitively referenced diff --git a/source/compiler/qsc_fir_transforms/src/monomorphize/tests.rs b/source/compiler/qsc_fir_transforms/src/monomorphize/tests.rs index 3b5601300b0..d5db54f4515 100644 --- a/source/compiler/qsc_fir_transforms/src/monomorphize/tests.rs +++ b/source/compiler/qsc_fir_transforms/src/monomorphize/tests.rs @@ -959,6 +959,71 @@ fn mono_closure_in_generic() { ); } +#[test] +fn mono_closure_targeting_generic_callable_is_retargeted() { + let (mut store, pkg_id) = crate::test_utils::compile_to_fir(indoc! {r#" + function First<'T>(x : 'T, y : 'T) : 'T { x } + function Main() : Int { + let captured = 42; + let first = First(captured, _); + first(7) + } + "#}); + + let generic_target = store + .get(pkg_id) + .items + .iter() + .find_map(|(item_id, item)| match &item.kind { + ItemKind::Callable(decl) if decl.name.name.as_ref() == "First" => Some(item_id), + _ => None, + }) + .expect("generic callable target should exist"); + + let package = store.get_mut(pkg_id); + let closure_expr_id = package + .exprs + .iter() + .find_map(|(expr_id, expr)| match &expr.kind { + ExprKind::Closure(captures, _) if !captures.is_empty() => Some(expr_id), + _ => None, + }) + .expect("partial application should lower to a capturing closure"); + let ExprKind::Closure(_, target) = &mut package + .exprs + .get_mut(closure_expr_id) + .expect("closure expression should exist") + .kind + else { + panic!("closure expression should remain a closure"); + }; + // Source partial application lowers through a wrapper lambda, so retarget + // the closure directly to exercise monomorphization's generic closure path. + *target = generic_target; + + let mut assigners = PackageAssigners::new(&store, pkg_id); + monomorphize(&mut store, pkg_id, &mut assigners); + + let package = store.get(pkg_id); + let mut closure_target_names = package + .exprs + .iter() + .filter_map(|(_, expr)| { + let ExprKind::Closure(_, target) = expr.kind else { + return None; + }; + let ItemKind::Callable(decl) = &package.get_item(target).kind else { + panic!("closure target should resolve to a callable"); + }; + Some(decl.name.name.to_string()) + }) + .collect::>(); + closure_target_names.sort(); + + assert_eq!(closure_target_names, vec!["First".to_string()]); + crate::invariants::check(&store, pkg_id, crate::invariants::InvariantLevel::PostMono); +} + #[test] fn mono_cross_package_length() { // Length is a cross-package intrinsic generic callable in std. diff --git a/source/compiler/qsc_fir_transforms/src/return_unify/lower.rs b/source/compiler/qsc_fir_transforms/src/return_unify/lower.rs index 4949278dd98..1e761315ddf 100644 --- a/source/compiler/qsc_fir_transforms/src/return_unify/lower.rs +++ b/source/compiler/qsc_fir_transforms/src/return_unify/lower.rs @@ -132,7 +132,24 @@ pub(super) struct SynthSlots { /// trailing expression. Returns the [`SynthSlots`] handles so the simplify /// phase can fold the canonical flag/slot shapes back into structured control /// flow. -#[allow(clippy::too_many_lines)] +/// +/// # Transformation +/// +/// ```text +/// // before: two exits — the early return and the trailing value +/// { +/// if cond { return x; } +/// y +/// } +/// // after: one exit — declare flag + slot, guard the suffix, read the slot +/// { +/// mutable __has_returned = false; +/// mutable __return_slot = ; +/// if cond { set __return_slot = x; set __has_returned = true; } +/// if not __has_returned { set __return_slot = y; } +/// __return_slot +/// } +/// ``` #[allow(clippy::too_many_arguments)] pub(super) fn transform_block_with_flags( package: &mut Package, @@ -144,9 +161,13 @@ pub(super) fn transform_block_with_flags( arrow_default_cache: &mut ArrowDefaultCache, return_slot_strategy: ReturnSlotStrategy, ) -> SynthSlots { + // Declare the `__has_returned` flag (starts `false`) at the block head. It + // records whether an early return has fired. let (has_returned_var_id, has_returned_decl_stmt) = create_mutable_bool_var(package, assigner, symbols::HAS_RETURNED, false); + // Declare the return slot that will hold the block's eventual return value, + // seeded with a default appropriate for `return_ty`. let (return_slot, ret_val_decl_stmt) = create_return_slot_decl( package, assigner, @@ -157,11 +178,15 @@ pub(super) fn transform_block_with_flags( return_slot_strategy, ); + // Snapshot the original statements, then start the new body with the two + // fresh declarations before any rewritten statement. let original_stmts = package.get_block(block_id).stmts.clone(); let mut new_stmts: Vec = Vec::new(); new_stmts.push(has_returned_decl_stmt); new_stmts.push(ret_val_decl_stmt); + // Bundle the flag/slot handles so the per-statement rewrite can reference + // them without passing each one separately. let flag_context = FlagContext { package_id, has_returned_var_id, @@ -169,6 +194,9 @@ pub(super) fn transform_block_with_flags( return_ty, udt_pure_tys, }; + // Rewrite the body statements: returns become flag/slot writes and every + // statement after the first return is guarded behind the flag. The trailing + // value is handled lazily so it, too, is skipped once a return has fired. new_stmts.extend(transform_block_stmts_with_flags( package, assigner, @@ -180,6 +208,8 @@ pub(super) fn transform_block_with_flags( }, )); + // Append the synthesized trailing expression that reads the slot back out, + // giving the block its single, unified exit value. let (trailing, trailing_result) = create_flag_trailing_expr_for_slot(package, assigner, &mut new_stmts, &flag_context); @@ -187,10 +217,14 @@ pub(super) fn transform_block_with_flags( new_stmts.push(trailing_stmt); } + // Swap in the rewritten statement list and keep the block's type as the + // declared return type. let block = package.blocks.get_mut(block_id).expect("block not found"); block.stmts = new_stmts; block.ty = return_ty.clone(); + // Hand back the synthesized local ids so the simplify phase can recognize + // and fold these canonical flag/slot shapes. SynthSlots { has_returned: has_returned_var_id, return_slot, @@ -240,7 +274,50 @@ pub(super) struct FlagContext<'a> { pub(super) udt_pure_tys: &'a UdtPureTyCache, } -#[allow(clippy::too_many_lines)] +/// Threads the `__has_returned` flag through a sequence of statements so that, +/// once an early `return` has fired, no later statement in the block runs. +/// +/// This is the per-statement workhorse behind [`transform_block_with_flags`]. +/// It walks the statements in order while remembering whether a return-bearing +/// statement has already been seen. The first return-bearing statement has its +/// `return`s rewritten into flag/slot writes; every statement after it is +/// wrapped in `if not __has_returned { … }` so it becomes a no-op on the paths +/// where the early return already ran. This guarding stands in for the PHI +/// merge a backend like LLVM would insert at the join point. +/// +/// Two cases break out of the straight-line guarding and instead emit a single +/// *lazy continuation* — the entire remaining suffix nested inside one +/// `if not __has_returned { … }` block: +/// - When [`continuation_suffix_requires_split`] reports that the suffix can't +/// be guarded statement-by-statement (for example because a later binding is +/// read by the trailing value and must stay in one scope). +/// - When the final trailing expression is reached under +/// [`FinalTrailingExprStrategy::Lazy`], or under `Preserve` but the trailing +/// expression itself still contains a `return`. +/// +/// Under `FinalTrailingExprStrategy::Preserve` with no nested return, the +/// trailing expression is left untouched so a block that already produces its +/// value can keep it verbatim. +/// +/// # Transformation +/// +/// Given a block body like: +/// +/// ```text +/// foo(); +/// if cond { return x; } +/// bar(); +/// baz() +/// ``` +/// +/// the returns become flag/slot writes and each following statement is guarded: +/// +/// ```text +/// foo(); +/// if cond { set __return_slot = x; set __has_returned = true; } +/// if not __has_returned { bar(); } +/// if not __has_returned { baz() } else { __return_slot } +/// ``` fn transform_block_stmts_with_flags( package: &mut Package, assigner: &mut Assigner, @@ -249,10 +326,16 @@ fn transform_block_stmts_with_flags( arrow_default_cache: &mut ArrowDefaultCache, output: FlagBlockOutput, ) -> Vec { + // The rewritten statement list we build up, and a running flag that flips + // to `true` the moment we pass a statement that can perform an early return. let mut new_stmts: Vec = Vec::new(); let mut seen_return_bearing_stmt = false; for (index, &stmt_id) in original_stmts.iter().enumerate() { + // Classify this statement before deciding how to handle it: + // - does it hold a `while` loop that itself contains a `return`? + // - does it contain a `return` anywhere? + // - is it the block's final trailing expression (the block's value)? let has_return_in_while = match &package.get_stmt(stmt_id).kind { StmtKind::Expr(e) | StmtKind::Semi(e) => contains_return_in_while_expr(package, *e), _ => false, @@ -262,6 +345,10 @@ fn transform_block_stmts_with_flags( && index == original_stmts.len() - 1 && matches!(package.get_stmt(stmt_id).kind, StmtKind::Expr(_)); + // We are already past an early return, and the rest of the block can't + // be guarded one statement at a time (e.g. a later binding is needed by + // the trailing value). Emit the whole remaining suffix as a single lazy + // `if not __has_returned { … }` block and stop. if seen_return_bearing_stmt && continuation_suffix_requires_split( package, @@ -283,11 +370,15 @@ fn transform_block_stmts_with_flags( break; } + // We are past an early return and this is the block's trailing value. + // How we finish depends on the caller's strategy for the trailing expr. if seen_return_bearing_stmt && is_final_trailing_expr { match output .final_trailing_expr_strategy() .expect("final trailing strategy should be set for value output") { + // Lazy: wrap the trailing expression (and anything left) in one + // guarded block that falls back to the return slot value. FinalTrailingExprStrategy::Lazy => { let lazy_continuation = create_lazy_flag_continuation_stmt( package, @@ -300,6 +391,9 @@ fn transform_block_stmts_with_flags( new_stmts.push(lazy_continuation); break; } + // Preserve, but the trailing expression still has its own + // `return` inside it: it can't be kept verbatim, so guard it + // lazily like the Lazy case. FinalTrailingExprStrategy::Preserve if has_return => { let lazy_continuation = create_lazy_flag_continuation_stmt( package, @@ -312,6 +406,8 @@ fn transform_block_stmts_with_flags( new_stmts.push(lazy_continuation); break; } + // Preserve with no nested return: the block already produces its + // value here, so keep the trailing expression exactly as-is. FinalTrailingExprStrategy::Preserve => { new_stmts.push(stmt_id); continue; @@ -319,59 +415,112 @@ fn transform_block_stmts_with_flags( } } - if has_return_in_while { - transform_while_stmt( - package, - assigner, - stmt_id, - flag_context, - arrow_default_cache, - ); - new_stmts.push(stmt_id); - seen_return_bearing_stmt = true; - } else if has_return && !seen_return_bearing_stmt { - replace_returns_with_flags( - package, - assigner, - stmt_id, - flag_context, - arrow_default_cache, - ); - new_stmts.push(stmt_id); - seen_return_bearing_stmt = true; - } else if has_return { - replace_returns_with_flags( - package, - assigner, - stmt_id, - flag_context, - arrow_default_cache, - ); - let guarded = guard_stmt_with_flag( - package, - assigner, - flag_context, - stmt_id, - arrow_default_cache, - ); - new_stmts.push(guarded); - } else if seen_return_bearing_stmt { - let guarded = guard_stmt_with_flag( - package, - assigner, - flag_context, - stmt_id, - arrow_default_cache, - ); - new_stmts.push(guarded); - } else { - new_stmts.push(stmt_id); - } + // The common path: rewrite this one statement (replace its returns with + // flag writes and/or guard it behind the flag as needed) and record + // whether we have now passed a return-bearing statement. + seen_return_bearing_stmt = transform_and_push_flag_stmt( + package, + assigner, + stmt_id, + flag_context, + arrow_default_cache, + &mut new_stmts, + has_return_in_while, + has_return, + seen_return_bearing_stmt, + ); } new_stmts } +/// Rewrites a single statement for the flag-threaded block and appends it to +/// `new_stmts`, returning the updated `seen_return_bearing_stmt` state. +/// +/// A `while` bearing a return is rewritten in place; the first return-bearing +/// statement has its returns replaced with flag writes; later statements are +/// guarded by the flag (standing in for LLVM's PHI merge). A statement that is +/// neither return-bearing nor after a return is passed through unchanged. +#[allow(clippy::too_many_arguments)] +fn transform_and_push_flag_stmt( + package: &mut Package, + assigner: &mut Assigner, + stmt_id: StmtId, + flag_context: &FlagContext<'_>, + arrow_default_cache: &mut ArrowDefaultCache, + new_stmts: &mut Vec, + has_return_in_while: bool, + has_return: bool, + seen_return_bearing_stmt: bool, +) -> bool { + if has_return_in_while { + // A `while` that can early-return is rewritten in place (flag-guarded + // loop condition + return replacement in its body) and kept as-is. + transform_while_stmt( + package, + assigner, + stmt_id, + flag_context, + arrow_default_cache, + ); + new_stmts.push(stmt_id); + true + } else if has_return && !seen_return_bearing_stmt { + // The first return-bearing statement: turn its `return`s into flag/slot + // writes. It runs unconditionally (nothing before it could have + // returned), so no flag guard is needed. + replace_returns_with_flags( + package, + assigner, + stmt_id, + flag_context, + arrow_default_cache, + ); + new_stmts.push(stmt_id); + true + } else if has_return { + // A later return-bearing statement: replace its returns with flag + // writes AND guard the whole statement, since an earlier return may + // already have fired. + replace_returns_with_flags( + package, + assigner, + stmt_id, + flag_context, + arrow_default_cache, + ); + let guarded = guard_stmt_with_flag( + package, + assigner, + flag_context, + stmt_id, + arrow_default_cache, + ); + new_stmts.push(guarded); + seen_return_bearing_stmt + } else if seen_return_bearing_stmt { + // No return of its own, but it sits after one, so guard it behind the + // flag so it is skipped on the early-return path. + let guarded = guard_stmt_with_flag( + package, + assigner, + flag_context, + stmt_id, + arrow_default_cache, + ); + new_stmts.push(guarded); + seen_return_bearing_stmt + } else { + // Nothing special: no return here and none before it, so keep it + // untouched. + new_stmts.push(stmt_id); + seen_return_bearing_stmt + } +} + +/// Builds a lazy-continuation *statement* wrapping the remaining suffix in one +/// flag-guarded block (see [`create_lazy_flag_continuation_expr`]), choosing an +/// expression- or semicolon-statement to match the block's value/unit output. fn create_lazy_flag_continuation_stmt( package: &mut Package, assigner: &mut Assigner, @@ -389,15 +538,38 @@ fn create_lazy_flag_continuation_stmt( output, ); match output { + // A value-producing block ends in an expression statement (no trailing + // semicolon) so the guarded `if` becomes the block's value. FlagBlockOutput::ReturnValue { .. } => { alloc_expr_stmt(package, assigner, lazy_continuation, Span::default()) } + // A unit block ends in a semicolon statement. FlagBlockOutput::Unit => { alloc_semi_stmt(package, assigner, lazy_continuation, Span::default()) } } } +/// Builds the lazy-continuation *expression*: the entire remaining suffix of a +/// block nested inside `if not __has_returned { } else { }`. +/// +/// Rather than guard each remaining statement individually, everything after an +/// early return is bundled into one guarded block. On the not-yet-returned path +/// the suffix runs; on the already-returned path control falls to the `else`, +/// which reads the return slot so the block still produces the return value. +/// +/// # Transformation +/// +/// ```text +/// // suffix after an early return, value-producing block: +/// if not __has_returned { +/// +/// // slot-read-or-fail appended if the suffix +/// // produced no value of the return type +/// } else { +/// __return_slot // already returned: yield the stored value +/// } +/// ``` fn create_lazy_flag_continuation_expr( package: &mut Package, assigner: &mut Assigner, @@ -406,6 +578,8 @@ fn create_lazy_flag_continuation_expr( arrow_default_cache: &mut ArrowDefaultCache, output: FlagBlockOutput, ) -> ExprId { + // Recursively flag-thread the suffix statements. `output.lazy()` keeps the + // nested block on the lazy trailing-expression strategy too. let mut continuation_stmts = transform_block_stmts_with_flags( package, assigner, @@ -416,7 +590,11 @@ fn create_lazy_flag_continuation_expr( ); let (continuation_ty, else_expr) = match output { FlagBlockOutput::ReturnValue { .. } => { + // The guarded block must yield a value of the return type. If the + // transformed suffix doesn't already end in one, make it. if !has_value_trailing_stmt(package, &continuation_stmts, flag_context.return_ty) { + // Drop a trailing side-effect-free unit statement first so it + // doesn't sit awkwardly before the value we're about to append. if let Some(&last_id) = continuation_stmts.last() && let StmtKind::Expr(e) = package.get_stmt(last_id).kind && package.get_expr(e).ty == Ty::UNIT @@ -424,6 +602,8 @@ fn create_lazy_flag_continuation_expr( { continuation_stmts.pop(); } + // Append an expression that reads the slot, or fails at runtime + // if this path is reached without a value having been set. let missing_value = create_return_slot_read_or_fail_expr( package, assigner, @@ -439,6 +619,7 @@ fn create_lazy_flag_continuation_expr( )); } + // The `else` branch (already-returned path) simply reads the slot. let ret_var = create_return_slot_read_expr( package, assigner, @@ -447,8 +628,10 @@ fn create_lazy_flag_continuation_expr( ); (flag_context.return_ty.clone(), Some(ret_var)) } + // A unit block needs no value and no `else` branch. FlagBlockOutput::Unit => (Ty::UNIT, None), }; + // Wrap the suffix statements in a block expression of the chosen type. let continuation_block = alloc_block( package, assigner, @@ -463,6 +646,8 @@ fn create_lazy_flag_continuation_expr( continuation_ty.clone(), Span::default(), ); + // Guard on `not __has_returned` so the suffix runs only when no early return + // has fired; otherwise control takes the `else` (slot read) path. let not_flag = create_not_var_expr(package, assigner, flag_context.has_returned_var_id); alloc_if_expr( @@ -476,6 +661,8 @@ fn create_lazy_flag_continuation_expr( ) } +/// Reports whether the last statement is an expression statement whose type is +/// the block's return type — i.e. the block already ends in a return value. fn has_value_trailing_stmt(package: &Package, stmts: &[StmtId], return_ty: &Ty) -> bool { stmts.last().is_some_and(|&stmt_id| { matches!( @@ -485,6 +672,8 @@ fn has_value_trailing_stmt(package: &Package, stmts: &[StmtId], return_ty: &Ty) }) } +/// Entry point for rewriting a statement that holds a `while`-with-return: +/// unwraps the statement's expression and hands it to [`transform_while_in_expr`]. fn transform_while_stmt( package: &mut Package, assigner: &mut Assigner, diff --git a/source/compiler/qsc_fir_transforms/src/return_unify/normalize.rs b/source/compiler/qsc_fir_transforms/src/return_unify/normalize.rs index b3e9aa613c9..8a6eb1067b9 100644 --- a/source/compiler/qsc_fir_transforms/src/return_unify/normalize.rs +++ b/source/compiler/qsc_fir_transforms/src/return_unify/normalize.rs @@ -63,15 +63,15 @@ mod shape_tests; use qsc_fir::{ assigner::Assigner, fir::{ - BinOp, Expr, ExprId, ExprKind, Ident, Mutability, Package, PackageId, PackageLookup, Pat, - PatId, PatKind, Res, Stmt, StmtId, StmtKind, StringComponent, + BinOp, ExprId, ExprKind, Mutability, Package, PackageId, PackageLookup, StmtId, StmtKind, + StringComponent, }, ty::{Prim, Ty}, }; -use crate::{ - EMPTY_EXEC_RANGE, - fir_builder::{alloc_block, alloc_bool_lit, alloc_expr, alloc_expr_stmt, alloc_semi_stmt}, +use crate::fir_builder::{ + alloc_block, alloc_bool_lit, alloc_discard_pat, alloc_expr, alloc_expr_stmt, alloc_local_stmt, + alloc_local_var, alloc_local_var_expr, alloc_semi_stmt, }; use qsc_data_structures::span::Span; use std::rc::Rc; @@ -746,27 +746,15 @@ fn create_discard_let_stmt( expr_id: ExprId, ) -> StmtId { let ty = package.get_expr(expr_id).ty.clone(); - let pat_id: PatId = assigner.next_pat(); - package.pats.insert( + let pat_id = alloc_discard_pat(package, assigner, ty, Span::default()); + alloc_local_stmt( + package, + assigner, + Mutability::Immutable, pat_id, - Pat { - id: pat_id, - span: Span::default(), - ty, - kind: PatKind::Discard, - }, - ); - let stmt_id = assigner.next_stmt(); - package.stmts.insert( - stmt_id, - Stmt { - id: stmt_id, - span: Span::default(), - kind: StmtKind::Local(Mutability::Immutable, pat_id, expr_id), - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); - stmt_id + expr_id, + Span::default(), + ) } /// Pins a statement-carrying `inner` (Block/If/While with internal Returns) @@ -795,43 +783,17 @@ fn bind_inner_and_return( inner: ExprId, ) -> Vec { let inner_ty = package.get_expr(inner).ty.clone(); - let local_var_id = assigner.next_local(); - let pat_id = assigner.next_pat(); - package.pats.insert( - pat_id, - Pat { - id: pat_id, - span: Span::default(), - ty: inner_ty.clone(), - kind: PatKind::Bind(Ident { - id: local_var_id, - span: Span::default(), - name: Rc::from(super::symbols::RET_HOIST), - }), - }, - ); - let local_stmt_id = assigner.next_stmt(); - package.stmts.insert( - local_stmt_id, - Stmt { - id: local_stmt_id, - span: Span::default(), - kind: StmtKind::Local(Mutability::Immutable, pat_id, inner), - exec_graph_range: EMPTY_EXEC_RANGE, - }, + let (local_var_id, local_stmt_id) = alloc_local_var( + package, + assigner, + super::symbols::RET_HOIST, + &inner_ty, + inner, + Mutability::Immutable, ); - let var_expr_id = assigner.next_expr(); - package.exprs.insert( - var_expr_id, - Expr { - id: var_expr_id, - span: Span::default(), - ty: inner_ty, - kind: ExprKind::Var(Res::Local(local_var_id), Vec::new()), - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); + let var_expr_id = + alloc_local_var_expr(package, assigner, local_var_id, inner_ty, Span::default()); // Rewrite the existing Return expression in place so it now wraps the // Var, then wrap it in a fresh Semi statement. diff --git a/source/compiler/qsc_fir_transforms/src/return_unify/slot.rs b/source/compiler/qsc_fir_transforms/src/return_unify/slot.rs index f8b066e6dd2..3e53227c174 100644 --- a/source/compiler/qsc_fir_transforms/src/return_unify/slot.rs +++ b/source/compiler/qsc_fir_transforms/src/return_unify/slot.rs @@ -3,21 +3,18 @@ //! Return-slot and defaultability policy for return unification. -use crate::{ - EMPTY_EXEC_RANGE, - fir_builder::{ - alloc_assign_expr, alloc_block, alloc_expr, alloc_expr_stmt, alloc_if_expr, - alloc_local_var, alloc_local_var_expr, - }, +use crate::fir_builder::{ + alloc_assign_expr, alloc_block, alloc_discard_pat, alloc_expr, alloc_expr_stmt, alloc_if_expr, + alloc_local_var, alloc_local_var_expr, }; use num_bigint::BigInt; use qsc_data_structures::span::Span; use qsc_fir::{ assigner::Assigner, fir::{ - CallableDecl, CallableImpl, Expr, ExprId, ExprKind, Ident, ItemId, ItemKind, Lit, - LocalItemId, LocalVarId, Mutability, Package, PackageId, Pat, PatKind, Res, Result, StmtId, - StoreItemId, StringComponent, + CallableDecl, CallableImpl, ExprId, ExprKind, Ident, ItemId, ItemKind, Lit, LocalItemId, + LocalVarId, Mutability, Package, PackageId, Res, Result, StmtId, StoreItemId, + StringComponent, }, ty::{Prim, Ty}, }; @@ -516,18 +513,13 @@ pub(super) fn create_default_value( arrow_default_cache, )?; - let expr_id = assigner.next_expr(); - package.exprs.insert( - expr_id, - Expr { - id: expr_id, - span: Span::default(), - ty: ty.clone(), - kind, - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); - Some(expr_id) + Some(alloc_expr( + package, + assigner, + ty.clone(), + kind, + Span::default(), + )) } fn create_default_value_kind( @@ -719,16 +711,7 @@ fn synthesize_fail_callable( Span::default(), ); - let input_pat_id = assigner.next_pat(); - package.pats.insert( - input_pat_id, - Pat { - id: input_pat_id, - span: Span::default(), - ty: input_ty.clone(), - kind: PatKind::Discard, - }, - ); + let input_pat_id = alloc_discard_pat(package, assigner, input_ty.clone(), Span::default()); let body_spec = qsc_fir::fir::SpecDecl { span: Span::default(), diff --git a/source/compiler/qsc_fir_transforms/src/sample_pipeline_tests/grover.rs b/source/compiler/qsc_fir_transforms/src/sample_pipeline_tests/grover.rs index e93f1b6a2ae..684bb22ab30 100644 --- a/source/compiler/qsc_fir_transforms/src/sample_pipeline_tests/grover.rs +++ b/source/compiler/qsc_fir_transforms/src/sample_pipeline_tests/grover.rs @@ -333,27 +333,27 @@ fn grover_sample_full_pipeline_reachable_items() { operation CollectControls(ctls : Qubit[], aux : Qubit[], adjustment : Int) : Unit is Adj { body ... { { - let _range_id_48878 : Range = 0..2..Length(ctls) - 2; - mutable _index_id_48881 : Int = _range_id_48878::Start; - let _step_id_48886 : Int = _range_id_48878::Step; - let _end_id_48891 : Int = _range_id_48878::End; - while _step_id_48886 > 0 and _index_id_48881 <= _end_id_48891 or _step_id_48886 < 0 and _index_id_48881 >= _end_id_48891 { - let i : Int = _index_id_48881; + let _range_id_48949 : Range = 0..2..Length(ctls) - 2; + mutable _index_id_48952 : Int = _range_id_48949::Start; + let _step_id_48957 : Int = _range_id_48949::Step; + let _end_id_48962 : Int = _range_id_48949::End; + while _step_id_48957 > 0 and _index_id_48952 <= _end_id_48962 or _step_id_48957 < 0 and _index_id_48952 >= _end_id_48962 { + let i : Int = _index_id_48952; CCNOT(ctls[i], ctls[i + 1], aux[i / 2]); - _index_id_48881 += _step_id_48886; + _index_id_48952 += _step_id_48957; } } { - let _range_id_48921 : Range = 0..Length(ctls) / 2 - 2 - adjustment; - mutable _index_id_48924 : Int = _range_id_48921::Start; - let _step_id_48929 : Int = _range_id_48921::Step; - let _end_id_48934 : Int = _range_id_48921::End; - while _step_id_48929 > 0 and _index_id_48924 <= _end_id_48934 or _step_id_48929 < 0 and _index_id_48924 >= _end_id_48934 { - let i : Int = _index_id_48924; + let _range_id_48992 : Range = 0..Length(ctls) / 2 - 2 - adjustment; + mutable _index_id_48995 : Int = _range_id_48992::Start; + let _step_id_49000 : Int = _range_id_48992::Step; + let _end_id_49005 : Int = _range_id_48992::End; + while _step_id_49000 > 0 and _index_id_48995 <= _end_id_49005 or _step_id_49000 < 0 and _index_id_48995 >= _end_id_49005 { + let i : Int = _index_id_48995; CCNOT(aux[i * 2], aux[i * 2 + 1], aux[i + Length(ctls) / 2]); - _index_id_48924 += _step_id_48929; + _index_id_48995 += _step_id_49000; } } @@ -363,14 +363,14 @@ fn grover_sample_full_pipeline_reachable_items() { { let _range : Range = 0..Length(ctls) / 2 - 2 - adjustment; { - let _range_id_48964 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; - mutable _index_id_48967 : Int = _range_id_48964::Start; - let _step_id_48972 : Int = _range_id_48964::Step; - let _end_id_48977 : Int = _range_id_48964::End; - while _step_id_48972 > 0 and _index_id_48967 <= _end_id_48977 or _step_id_48972 < 0 and _index_id_48967 >= _end_id_48977 { - let i : Int = _index_id_48967; + let _range_id_49035 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; + mutable _index_id_49038 : Int = _range_id_49035::Start; + let _step_id_49043 : Int = _range_id_49035::Step; + let _end_id_49048 : Int = _range_id_49035::End; + while _step_id_49043 > 0 and _index_id_49038 <= _end_id_49048 or _step_id_49043 < 0 and _index_id_49038 >= _end_id_49048 { + let i : Int = _index_id_49038; Adjoint CCNOT(aux[i * 2], aux[i * 2 + 1], aux[i + Length(ctls) / 2]); - _index_id_48967 += _step_id_48972; + _index_id_49038 += _step_id_49043; } } @@ -380,14 +380,14 @@ fn grover_sample_full_pipeline_reachable_items() { { let _range : Range = 0..2..Length(ctls) - 2; { - let _range_id_49007 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; - mutable _index_id_49010 : Int = _range_id_49007::Start; - let _step_id_49015 : Int = _range_id_49007::Step; - let _end_id_49020 : Int = _range_id_49007::End; - while _step_id_49015 > 0 and _index_id_49010 <= _end_id_49020 or _step_id_49015 < 0 and _index_id_49010 >= _end_id_49020 { - let i : Int = _index_id_49010; + let _range_id_49078 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; + mutable _index_id_49081 : Int = _range_id_49078::Start; + let _step_id_49086 : Int = _range_id_49078::Step; + let _end_id_49091 : Int = _range_id_49078::End; + while _step_id_49086 > 0 and _index_id_49081 <= _end_id_49091 or _step_id_49086 < 0 and _index_id_49081 >= _end_id_49091 { + let i : Int = _index_id_49081; Adjoint CCNOT(ctls[i], ctls[i + 1], aux[i / 2]); - _index_id_49010 += _step_id_49015; + _index_id_49081 += _step_id_49086; } } @@ -500,7 +500,7 @@ fn grover_sample_full_pipeline_reachable_items() { CCH(ctls[0], ctls[1], qubit); } else { let aux : Qubit[] = AllocateQubitArray(Length(ctls) - 1 - Length(ctls) % 2); - let _generated_ident_53981 : Unit = { + let _generated_ident_54052 : Unit = { { CollectControls(ctls, aux, 0); } @@ -521,7 +521,7 @@ fn grover_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(aux); - _generated_ident_53981 + _generated_ident_54052 } } @@ -546,7 +546,7 @@ fn grover_sample_full_pipeline_reachable_items() { CCH(ctls[0], ctls[1], qubit); } else { let aux : Qubit[] = AllocateQubitArray(Length(ctls) - 1 - Length(ctls) % 2); - let _generated_ident_53995 : Unit = { + let _generated_ident_54066 : Unit = { { CollectControls(ctls, aux, 0); } @@ -567,7 +567,7 @@ fn grover_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(aux); - _generated_ident_53995 + _generated_ident_54066 } } @@ -594,7 +594,7 @@ fn grover_sample_full_pipeline_reachable_items() { CRz(ctls[0], theta, qubit); } else { let aux : Qubit[] = AllocateQubitArray(Length(ctls) - 1); - let _generated_ident_54051 : Unit = { + let _generated_ident_54122 : Unit = { { CollectControls(ctls, aux, 0); AdjustForSingleControl(ctls, aux); @@ -611,7 +611,7 @@ fn grover_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(aux); - _generated_ident_54051 + _generated_ident_54122 } } @@ -645,7 +645,7 @@ fn grover_sample_full_pipeline_reachable_items() { Controlled CS([ctls[0]], (ctls[1], qubit)); } else { let aux : Qubit[] = AllocateQubitArray(Length(ctls) - 2); - let _generated_ident_54079 : Unit = { + let _generated_ident_54150 : Unit = { { CollectControls(ctls, aux, 1 - Length(ctls) % 2); } @@ -666,7 +666,7 @@ fn grover_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(aux); - _generated_ident_54079 + _generated_ident_54150 } } @@ -691,7 +691,7 @@ fn grover_sample_full_pipeline_reachable_items() { Controlled Adjoint CS([ctls[0]], (ctls[1], qubit)); } else { let aux : Qubit[] = AllocateQubitArray(Length(ctls) - 2); - let _generated_ident_54093 : Unit = { + let _generated_ident_54164 : Unit = { { CollectControls(ctls, aux, 1 - Length(ctls) % 2); } @@ -712,7 +712,7 @@ fn grover_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(aux); - _generated_ident_54093 + _generated_ident_54164 } } @@ -739,7 +739,7 @@ fn grover_sample_full_pipeline_reachable_items() { CT(ctls[0], qubit); } else { let aux : Qubit[] = AllocateQubitArray(Length(ctls) - 1); - let _generated_ident_54135 : Unit = { + let _generated_ident_54206 : Unit = { { CollectControls(ctls, aux, 0); AdjustForSingleControl(ctls, aux); @@ -756,7 +756,7 @@ fn grover_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(aux); - _generated_ident_54135 + _generated_ident_54206 } } @@ -773,7 +773,7 @@ fn grover_sample_full_pipeline_reachable_items() { Adjoint CT(ctls[0], qubit); } else { let aux : Qubit[] = AllocateQubitArray(Length(ctls) - 1); - let _generated_ident_54149 : Unit = { + let _generated_ident_54220 : Unit = { { CollectControls(ctls, aux, 0); AdjustForSingleControl(ctls, aux); @@ -790,7 +790,7 @@ fn grover_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(aux); - _generated_ident_54149 + _generated_ident_54220 } } @@ -821,7 +821,7 @@ fn grover_sample_full_pipeline_reachable_items() { __quantum__qis__ccx__body(ctls[0], ctls[1], qubit); } else { let aux : Qubit[] = AllocateQubitArray(Length(ctls) - 2); - let _generated_ident_54163 : Unit = { + let _generated_ident_54234 : Unit = { { CollectControls(ctls, aux, 1 - Length(ctls) % 2); } @@ -842,7 +842,7 @@ fn grover_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(aux); - _generated_ident_54163 + _generated_ident_54234 } } @@ -867,7 +867,7 @@ fn grover_sample_full_pipeline_reachable_items() { __quantum__qis__ccx__body(ctls[0], ctls[1], qubit); } else { let aux : Qubit[] = AllocateQubitArray(Length(ctls) - 2); - let _generated_ident_54177 : Unit = { + let _generated_ident_54248 : Unit = { { CollectControls(ctls, aux, 1 - Length(ctls) % 2); } @@ -888,7 +888,7 @@ fn grover_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(aux); - _generated_ident_54177 + _generated_ident_54248 } } @@ -921,7 +921,7 @@ fn grover_sample_full_pipeline_reachable_items() { CCZ(ctls[0], ctls[1], qubit); } else { let aux : Qubit[] = AllocateQubitArray(Length(ctls) - 2); - let _generated_ident_54219 : Unit = { + let _generated_ident_54290 : Unit = { { CollectControls(ctls, aux, 1 - Length(ctls) % 2); } @@ -942,7 +942,7 @@ fn grover_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(aux); - _generated_ident_54219 + _generated_ident_54290 } } @@ -967,7 +967,7 @@ fn grover_sample_full_pipeline_reachable_items() { CCZ(ctls[0], ctls[1], qubit); } else { let aux : Qubit[] = AllocateQubitArray(Length(ctls) - 2); - let _generated_ident_54233 : Unit = { + let _generated_ident_54304 : Unit = { { CollectControls(ctls, aux, 1 - Length(ctls) % 2); } @@ -988,7 +988,7 @@ fn grover_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(aux); - _generated_ident_54233 + _generated_ident_54304 } } @@ -1037,13 +1037,13 @@ fn grover_sample_full_pipeline_reachable_items() { operation MResetEachZ(register : Qubit[]) : Result[] { mutable results : Result[] = []; { - let _array_id_49646 : Qubit[] = register; - let _len_id_49650 : Int = Length(_array_id_49646); - mutable _index_id_49655 : Int = 0; - while _index_id_49655 < _len_id_49650 { - let qubit : Qubit = _array_id_49646[_index_id_49655]; + let _array_id_49717 : Qubit[] = register; + let _len_id_49721 : Int = Length(_array_id_49717); + mutable _index_id_49726 : Int = 0; + while _index_id_49726 < _len_id_49721 { + let qubit : Qubit = _array_id_49717[_index_id_49726]; results += [MResetZ(qubit)]; - _index_id_49655 += 1; + _index_id_49726 += 1; } } diff --git a/source/compiler/qsc_fir_transforms/src/sample_pipeline_tests/shor.rs b/source/compiler/qsc_fir_transforms/src/sample_pipeline_tests/shor.rs index 6eccd77e20a..c31675cc85f 100644 --- a/source/compiler/qsc_fir_transforms/src/sample_pipeline_tests/shor.rs +++ b/source/compiler/qsc_fir_transforms/src/sample_pipeline_tests/shor.rs @@ -148,17 +148,17 @@ fn shor_sample_full_pipeline_reachable_items() { Fact(value >= 0, $"`value` must be non-negative."); mutable runningValue : Int = value; { - let _array_id_47710 : Qubit[] = target; - let _len_id_47714 : Int = Length(_array_id_47710); - mutable _index_id_47719 : Int = 0; - while _index_id_47719 < _len_id_47714 { - let q : Qubit = _array_id_47710[_index_id_47719]; + let _array_id_47781 : Qubit[] = target; + let _len_id_47785 : Int = Length(_array_id_47781); + mutable _index_id_47790 : Int = 0; + while _index_id_47790 < _len_id_47785 { + let q : Qubit = _array_id_47781[_index_id_47790]; if runningValue &&& 1 != 0 { X(q); } runningValue >>>= 1; - _index_id_47719 += 1; + _index_id_47790 += 1; } } @@ -169,17 +169,17 @@ fn shor_sample_full_pipeline_reachable_items() { Fact(value >= 0, $"`value` must be non-negative."); mutable runningValue : Int = value; { - let _array_id_47738 : Qubit[] = target; - let _len_id_47742 : Int = Length(_array_id_47738); - mutable _index_id_47747 : Int = 0; - while _index_id_47747 < _len_id_47742 { - let q : Qubit = _array_id_47738[_index_id_47747]; + let _array_id_47809 : Qubit[] = target; + let _len_id_47813 : Int = Length(_array_id_47809); + mutable _index_id_47818 : Int = 0; + while _index_id_47818 < _len_id_47813 { + let q : Qubit = _array_id_47809[_index_id_47818]; if runningValue &&& 1 != 0 { X(q); } runningValue >>>= 1; - _index_id_47747 += 1; + _index_id_47818 += 1; } } @@ -190,17 +190,17 @@ fn shor_sample_full_pipeline_reachable_items() { Fact(value >= 0, $"`value` must be non-negative."); mutable runningValue : Int = value; { - let _array_id_47766 : Qubit[] = target; - let _len_id_47770 : Int = Length(_array_id_47766); - mutable _index_id_47775 : Int = 0; - while _index_id_47775 < _len_id_47770 { - let q : Qubit = _array_id_47766[_index_id_47775]; + let _array_id_47837 : Qubit[] = target; + let _len_id_47841 : Int = Length(_array_id_47837); + mutable _index_id_47846 : Int = 0; + while _index_id_47846 < _len_id_47841 { + let q : Qubit = _array_id_47837[_index_id_47846]; if runningValue &&& 1 != 0 { Controlled X(ctls, q); } runningValue >>>= 1; - _index_id_47775 += 1; + _index_id_47846 += 1; } } @@ -211,17 +211,17 @@ fn shor_sample_full_pipeline_reachable_items() { Fact(value >= 0, $"`value` must be non-negative."); mutable runningValue : Int = value; { - let _array_id_47794 : Qubit[] = target; - let _len_id_47798 : Int = Length(_array_id_47794); - mutable _index_id_47803 : Int = 0; - while _index_id_47803 < _len_id_47798 { - let q : Qubit = _array_id_47794[_index_id_47803]; + let _array_id_47865 : Qubit[] = target; + let _len_id_47869 : Int = Length(_array_id_47865); + mutable _index_id_47874 : Int = 0; + while _index_id_47874 < _len_id_47869 { + let q : Qubit = _array_id_47865[_index_id_47874]; if runningValue &&& 1 != 0 { Controlled X(ctls, q); } runningValue >>>= 1; - _index_id_47803 += 1; + _index_id_47874 += 1; } } @@ -430,27 +430,27 @@ fn shor_sample_full_pipeline_reachable_items() { operation CollectControls(ctls : Qubit[], aux : Qubit[], adjustment : Int) : Unit is Adj { body ... { { - let _range_id_48878 : Range = 0..2..Length(ctls) - 2; - mutable _index_id_48881 : Int = _range_id_48878::Start; - let _step_id_48886 : Int = _range_id_48878::Step; - let _end_id_48891 : Int = _range_id_48878::End; - while _step_id_48886 > 0 and _index_id_48881 <= _end_id_48891 or _step_id_48886 < 0 and _index_id_48881 >= _end_id_48891 { - let i : Int = _index_id_48881; + let _range_id_48949 : Range = 0..2..Length(ctls) - 2; + mutable _index_id_48952 : Int = _range_id_48949::Start; + let _step_id_48957 : Int = _range_id_48949::Step; + let _end_id_48962 : Int = _range_id_48949::End; + while _step_id_48957 > 0 and _index_id_48952 <= _end_id_48962 or _step_id_48957 < 0 and _index_id_48952 >= _end_id_48962 { + let i : Int = _index_id_48952; CCNOT(ctls[i], ctls[i + 1], aux[i / 2]); - _index_id_48881 += _step_id_48886; + _index_id_48952 += _step_id_48957; } } { - let _range_id_48921 : Range = 0..Length(ctls) / 2 - 2 - adjustment; - mutable _index_id_48924 : Int = _range_id_48921::Start; - let _step_id_48929 : Int = _range_id_48921::Step; - let _end_id_48934 : Int = _range_id_48921::End; - while _step_id_48929 > 0 and _index_id_48924 <= _end_id_48934 or _step_id_48929 < 0 and _index_id_48924 >= _end_id_48934 { - let i : Int = _index_id_48924; + let _range_id_48992 : Range = 0..Length(ctls) / 2 - 2 - adjustment; + mutable _index_id_48995 : Int = _range_id_48992::Start; + let _step_id_49000 : Int = _range_id_48992::Step; + let _end_id_49005 : Int = _range_id_48992::End; + while _step_id_49000 > 0 and _index_id_48995 <= _end_id_49005 or _step_id_49000 < 0 and _index_id_48995 >= _end_id_49005 { + let i : Int = _index_id_48995; CCNOT(aux[i * 2], aux[i * 2 + 1], aux[i + Length(ctls) / 2]); - _index_id_48924 += _step_id_48929; + _index_id_48995 += _step_id_49000; } } @@ -460,14 +460,14 @@ fn shor_sample_full_pipeline_reachable_items() { { let _range : Range = 0..Length(ctls) / 2 - 2 - adjustment; { - let _range_id_48964 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; - mutable _index_id_48967 : Int = _range_id_48964::Start; - let _step_id_48972 : Int = _range_id_48964::Step; - let _end_id_48977 : Int = _range_id_48964::End; - while _step_id_48972 > 0 and _index_id_48967 <= _end_id_48977 or _step_id_48972 < 0 and _index_id_48967 >= _end_id_48977 { - let i : Int = _index_id_48967; + let _range_id_49035 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; + mutable _index_id_49038 : Int = _range_id_49035::Start; + let _step_id_49043 : Int = _range_id_49035::Step; + let _end_id_49048 : Int = _range_id_49035::End; + while _step_id_49043 > 0 and _index_id_49038 <= _end_id_49048 or _step_id_49043 < 0 and _index_id_49038 >= _end_id_49048 { + let i : Int = _index_id_49038; Adjoint CCNOT(aux[i * 2], aux[i * 2 + 1], aux[i + Length(ctls) / 2]); - _index_id_48967 += _step_id_48972; + _index_id_49038 += _step_id_49043; } } @@ -477,14 +477,14 @@ fn shor_sample_full_pipeline_reachable_items() { { let _range : Range = 0..2..Length(ctls) - 2; { - let _range_id_49007 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; - mutable _index_id_49010 : Int = _range_id_49007::Start; - let _step_id_49015 : Int = _range_id_49007::Step; - let _end_id_49020 : Int = _range_id_49007::End; - while _step_id_49015 > 0 and _index_id_49010 <= _end_id_49020 or _step_id_49015 < 0 and _index_id_49010 >= _end_id_49020 { - let i : Int = _index_id_49010; + let _range_id_49078 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; + mutable _index_id_49081 : Int = _range_id_49078::Start; + let _step_id_49086 : Int = _range_id_49078::Step; + let _end_id_49091 : Int = _range_id_49078::End; + while _step_id_49086 > 0 and _index_id_49081 <= _end_id_49091 or _step_id_49086 < 0 and _index_id_49081 >= _end_id_49091 { + let i : Int = _index_id_49081; Adjoint CCNOT(ctls[i], ctls[i + 1], aux[i / 2]); - _index_id_49010 += _step_id_49015; + _index_id_49081 += _step_id_49086; } } @@ -597,7 +597,7 @@ fn shor_sample_full_pipeline_reachable_items() { CCH(ctls[0], ctls[1], qubit); } else { let aux : Qubit[] = AllocateQubitArray(Length(ctls) - 1 - Length(ctls) % 2); - let _generated_ident_53981 : Unit = { + let _generated_ident_54052 : Unit = { { CollectControls(ctls, aux, 0); } @@ -618,7 +618,7 @@ fn shor_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(aux); - _generated_ident_53981 + _generated_ident_54052 } } @@ -643,7 +643,7 @@ fn shor_sample_full_pipeline_reachable_items() { CCH(ctls[0], ctls[1], qubit); } else { let aux : Qubit[] = AllocateQubitArray(Length(ctls) - 1 - Length(ctls) % 2); - let _generated_ident_53995 : Unit = { + let _generated_ident_54066 : Unit = { { CollectControls(ctls, aux, 0); } @@ -664,7 +664,7 @@ fn shor_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(aux); - _generated_ident_53995 + _generated_ident_54066 } } @@ -749,13 +749,13 @@ fn shor_sample_full_pipeline_reachable_items() { } operation ResetAll(qubits : Qubit[]) : Unit { { - let _array_id_49379 : Qubit[] = qubits; - let _len_id_49383 : Int = Length(_array_id_49379); - mutable _index_id_49388 : Int = 0; - while _index_id_49388 < _len_id_49383 { - let q : Qubit = _array_id_49379[_index_id_49388]; + let _array_id_49450 : Qubit[] = qubits; + let _len_id_49454 : Int = Length(_array_id_49450); + mutable _index_id_49459 : Int = 0; + while _index_id_49459 < _len_id_49454 { + let q : Qubit = _array_id_49450[_index_id_49459]; Reset(q); - _index_id_49388 += 1; + _index_id_49459 += 1; } } @@ -865,7 +865,7 @@ fn shor_sample_full_pipeline_reachable_items() { CRz(ctls[0], theta, qubit); } else { let aux : Qubit[] = AllocateQubitArray(Length(ctls) - 1); - let _generated_ident_54051 : Unit = { + let _generated_ident_54122 : Unit = { { CollectControls(ctls, aux, 0); AdjustForSingleControl(ctls, aux); @@ -882,7 +882,7 @@ fn shor_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(aux); - _generated_ident_54051 + _generated_ident_54122 } } @@ -916,7 +916,7 @@ fn shor_sample_full_pipeline_reachable_items() { Controlled CS([ctls[0]], (ctls[1], qubit)); } else { let aux : Qubit[] = AllocateQubitArray(Length(ctls) - 2); - let _generated_ident_54079 : Unit = { + let _generated_ident_54150 : Unit = { { CollectControls(ctls, aux, 1 - Length(ctls) % 2); } @@ -937,7 +937,7 @@ fn shor_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(aux); - _generated_ident_54079 + _generated_ident_54150 } } @@ -962,7 +962,7 @@ fn shor_sample_full_pipeline_reachable_items() { Controlled Adjoint CS([ctls[0]], (ctls[1], qubit)); } else { let aux : Qubit[] = AllocateQubitArray(Length(ctls) - 2); - let _generated_ident_54093 : Unit = { + let _generated_ident_54164 : Unit = { { CollectControls(ctls, aux, 1 - Length(ctls) % 2); } @@ -983,7 +983,7 @@ fn shor_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(aux); - _generated_ident_54093 + _generated_ident_54164 } } @@ -1064,7 +1064,7 @@ fn shor_sample_full_pipeline_reachable_items() { CT(ctls[0], qubit); } else { let aux : Qubit[] = AllocateQubitArray(Length(ctls) - 1); - let _generated_ident_54135 : Unit = { + let _generated_ident_54206 : Unit = { { CollectControls(ctls, aux, 0); AdjustForSingleControl(ctls, aux); @@ -1081,7 +1081,7 @@ fn shor_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(aux); - _generated_ident_54135 + _generated_ident_54206 } } @@ -1098,7 +1098,7 @@ fn shor_sample_full_pipeline_reachable_items() { Adjoint CT(ctls[0], qubit); } else { let aux : Qubit[] = AllocateQubitArray(Length(ctls) - 1); - let _generated_ident_54149 : Unit = { + let _generated_ident_54220 : Unit = { { CollectControls(ctls, aux, 0); AdjustForSingleControl(ctls, aux); @@ -1115,7 +1115,7 @@ fn shor_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(aux); - _generated_ident_54149 + _generated_ident_54220 } } @@ -1146,7 +1146,7 @@ fn shor_sample_full_pipeline_reachable_items() { __quantum__qis__ccx__body(ctls[0], ctls[1], qubit); } else { let aux : Qubit[] = AllocateQubitArray(Length(ctls) - 2); - let _generated_ident_54163 : Unit = { + let _generated_ident_54234 : Unit = { { CollectControls(ctls, aux, 1 - Length(ctls) % 2); } @@ -1167,7 +1167,7 @@ fn shor_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(aux); - _generated_ident_54163 + _generated_ident_54234 } } @@ -1192,7 +1192,7 @@ fn shor_sample_full_pipeline_reachable_items() { __quantum__qis__ccx__body(ctls[0], ctls[1], qubit); } else { let aux : Qubit[] = AllocateQubitArray(Length(ctls) - 2); - let _generated_ident_54177 : Unit = { + let _generated_ident_54248 : Unit = { { CollectControls(ctls, aux, 1 - Length(ctls) % 2); } @@ -1213,7 +1213,7 @@ fn shor_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(aux); - _generated_ident_54177 + _generated_ident_54248 } } @@ -1722,27 +1722,27 @@ fn shor_sample_full_pipeline_reachable_items() { body ... { Fact(Length(xs) <= Length(ys), $"Input register ys must be at least as long as xs."); { - let _range_id_51179 : Range = 1..Length(xs) - 1; - mutable _index_id_51182 : Int = _range_id_51179::Start; - let _step_id_51187 : Int = _range_id_51179::Step; - let _end_id_51192 : Int = _range_id_51179::End; - while _step_id_51187 > 0 and _index_id_51182 <= _end_id_51192 or _step_id_51187 < 0 and _index_id_51182 >= _end_id_51192 { - let i : Int = _index_id_51182; + let _range_id_51250 : Range = 1..Length(xs) - 1; + mutable _index_id_51253 : Int = _range_id_51250::Start; + let _step_id_51258 : Int = _range_id_51250::Step; + let _end_id_51263 : Int = _range_id_51250::End; + while _step_id_51258 > 0 and _index_id_51253 <= _end_id_51263 or _step_id_51258 < 0 and _index_id_51253 >= _end_id_51263 { + let i : Int = _index_id_51253; CNOT(xs[i], ys[i]); - _index_id_51182 += _step_id_51187; + _index_id_51253 += _step_id_51258; } } { - let _range_id_51222 : Range = Length(xs) - 2..-1..1; - mutable _index_id_51225 : Int = _range_id_51222::Start; - let _step_id_51230 : Int = _range_id_51222::Step; - let _end_id_51235 : Int = _range_id_51222::End; - while _step_id_51230 > 0 and _index_id_51225 <= _end_id_51235 or _step_id_51230 < 0 and _index_id_51225 >= _end_id_51235 { - let i : Int = _index_id_51225; + let _range_id_51293 : Range = Length(xs) - 2..-1..1; + mutable _index_id_51296 : Int = _range_id_51293::Start; + let _step_id_51301 : Int = _range_id_51293::Step; + let _end_id_51306 : Int = _range_id_51293::End; + while _step_id_51301 > 0 and _index_id_51296 <= _end_id_51306 or _step_id_51301 < 0 and _index_id_51296 >= _end_id_51306 { + let i : Int = _index_id_51296; CNOT(xs[i], xs[i + 1]); - _index_id_51225 += _step_id_51230; + _index_id_51296 += _step_id_51301; } } @@ -1753,14 +1753,14 @@ fn shor_sample_full_pipeline_reachable_items() { { let _range : Range = Length(xs) - 2..-1..1; { - let _range_id_51265 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; - mutable _index_id_51268 : Int = _range_id_51265::Start; - let _step_id_51273 : Int = _range_id_51265::Step; - let _end_id_51278 : Int = _range_id_51265::End; - while _step_id_51273 > 0 and _index_id_51268 <= _end_id_51278 or _step_id_51273 < 0 and _index_id_51268 >= _end_id_51278 { - let i : Int = _index_id_51268; + let _range_id_51336 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; + mutable _index_id_51339 : Int = _range_id_51336::Start; + let _step_id_51344 : Int = _range_id_51336::Step; + let _end_id_51349 : Int = _range_id_51336::End; + while _step_id_51344 > 0 and _index_id_51339 <= _end_id_51349 or _step_id_51344 < 0 and _index_id_51339 >= _end_id_51349 { + let i : Int = _index_id_51339; Adjoint CNOT(xs[i], xs[i + 1]); - _index_id_51268 += _step_id_51273; + _index_id_51339 += _step_id_51344; } } @@ -1770,14 +1770,14 @@ fn shor_sample_full_pipeline_reachable_items() { { let _range : Range = 1..Length(xs) - 1; { - let _range_id_51308 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; - mutable _index_id_51311 : Int = _range_id_51308::Start; - let _step_id_51316 : Int = _range_id_51308::Step; - let _end_id_51321 : Int = _range_id_51308::End; - while _step_id_51316 > 0 and _index_id_51311 <= _end_id_51321 or _step_id_51316 < 0 and _index_id_51311 >= _end_id_51321 { - let i : Int = _index_id_51311; + let _range_id_51379 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; + mutable _index_id_51382 : Int = _range_id_51379::Start; + let _step_id_51387 : Int = _range_id_51379::Step; + let _end_id_51392 : Int = _range_id_51379::End; + while _step_id_51387 > 0 and _index_id_51382 <= _end_id_51392 or _step_id_51387 < 0 and _index_id_51382 >= _end_id_51392 { + let i : Int = _index_id_51382; Adjoint CNOT(xs[i], ys[i]); - _index_id_51311 += _step_id_51316; + _index_id_51382 += _step_id_51387; } } @@ -1788,27 +1788,27 @@ fn shor_sample_full_pipeline_reachable_items() { controlled (ctls, ...) { Fact(Length(xs) <= Length(ys), $"Input register ys must be at least as long as xs."); { - let _range_id_51351 : Range = 1..Length(xs) - 1; - mutable _index_id_51354 : Int = _range_id_51351::Start; - let _step_id_51359 : Int = _range_id_51351::Step; - let _end_id_51364 : Int = _range_id_51351::End; - while _step_id_51359 > 0 and _index_id_51354 <= _end_id_51364 or _step_id_51359 < 0 and _index_id_51354 >= _end_id_51364 { - let i : Int = _index_id_51354; + let _range_id_51422 : Range = 1..Length(xs) - 1; + mutable _index_id_51425 : Int = _range_id_51422::Start; + let _step_id_51430 : Int = _range_id_51422::Step; + let _end_id_51435 : Int = _range_id_51422::End; + while _step_id_51430 > 0 and _index_id_51425 <= _end_id_51435 or _step_id_51430 < 0 and _index_id_51425 >= _end_id_51435 { + let i : Int = _index_id_51425; Controlled CNOT(ctls, (xs[i], ys[i])); - _index_id_51354 += _step_id_51359; + _index_id_51425 += _step_id_51430; } } { - let _range_id_51394 : Range = Length(xs) - 2..-1..1; - mutable _index_id_51397 : Int = _range_id_51394::Start; - let _step_id_51402 : Int = _range_id_51394::Step; - let _end_id_51407 : Int = _range_id_51394::End; - while _step_id_51402 > 0 and _index_id_51397 <= _end_id_51407 or _step_id_51402 < 0 and _index_id_51397 >= _end_id_51407 { - let i : Int = _index_id_51397; + let _range_id_51465 : Range = Length(xs) - 2..-1..1; + mutable _index_id_51468 : Int = _range_id_51465::Start; + let _step_id_51473 : Int = _range_id_51465::Step; + let _end_id_51478 : Int = _range_id_51465::End; + while _step_id_51473 > 0 and _index_id_51468 <= _end_id_51478 or _step_id_51473 < 0 and _index_id_51468 >= _end_id_51478 { + let i : Int = _index_id_51468; Controlled CNOT(ctls, (xs[i], xs[i + 1])); - _index_id_51397 += _step_id_51402; + _index_id_51468 += _step_id_51473; } } @@ -1819,14 +1819,14 @@ fn shor_sample_full_pipeline_reachable_items() { { let _range : Range = Length(xs) - 2..-1..1; { - let _range_id_51437 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; - mutable _index_id_51440 : Int = _range_id_51437::Start; - let _step_id_51445 : Int = _range_id_51437::Step; - let _end_id_51450 : Int = _range_id_51437::End; - while _step_id_51445 > 0 and _index_id_51440 <= _end_id_51450 or _step_id_51445 < 0 and _index_id_51440 >= _end_id_51450 { - let i : Int = _index_id_51440; + let _range_id_51508 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; + mutable _index_id_51511 : Int = _range_id_51508::Start; + let _step_id_51516 : Int = _range_id_51508::Step; + let _end_id_51521 : Int = _range_id_51508::End; + while _step_id_51516 > 0 and _index_id_51511 <= _end_id_51521 or _step_id_51516 < 0 and _index_id_51511 >= _end_id_51521 { + let i : Int = _index_id_51511; Controlled Adjoint CNOT(ctls, (xs[i], xs[i + 1])); - _index_id_51440 += _step_id_51445; + _index_id_51511 += _step_id_51516; } } @@ -1836,14 +1836,14 @@ fn shor_sample_full_pipeline_reachable_items() { { let _range : Range = 1..Length(xs) - 1; { - let _range_id_51480 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; - mutable _index_id_51483 : Int = _range_id_51480::Start; - let _step_id_51488 : Int = _range_id_51480::Step; - let _end_id_51493 : Int = _range_id_51480::End; - while _step_id_51488 > 0 and _index_id_51483 <= _end_id_51493 or _step_id_51488 < 0 and _index_id_51483 >= _end_id_51493 { - let i : Int = _index_id_51483; + let _range_id_51551 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; + mutable _index_id_51554 : Int = _range_id_51551::Start; + let _step_id_51559 : Int = _range_id_51551::Step; + let _end_id_51564 : Int = _range_id_51551::End; + while _step_id_51559 > 0 and _index_id_51554 <= _end_id_51564 or _step_id_51559 < 0 and _index_id_51554 >= _end_id_51564 { + let i : Int = _index_id_51554; Controlled Adjoint CNOT(ctls, (xs[i], ys[i])); - _index_id_51483 += _step_id_51488; + _index_id_51554 += _step_id_51559; } } @@ -1862,28 +1862,28 @@ fn shor_sample_full_pipeline_reachable_items() { controlled (controls, ...) { Fact(Length(xs) == Length(ys), $"Input registers must have the same number of qubits."); { - let _range_id_51523 : Range = 0..Length(xs) - 2; - mutable _index_id_51526 : Int = _range_id_51523::Start; - let _step_id_51531 : Int = _range_id_51523::Step; - let _end_id_51536 : Int = _range_id_51523::End; - while _step_id_51531 > 0 and _index_id_51526 <= _end_id_51536 or _step_id_51531 < 0 and _index_id_51526 >= _end_id_51536 { - let idx : Int = _index_id_51526; + let _range_id_51594 : Range = 0..Length(xs) - 2; + mutable _index_id_51597 : Int = _range_id_51594::Start; + let _step_id_51602 : Int = _range_id_51594::Step; + let _end_id_51607 : Int = _range_id_51594::End; + while _step_id_51602 > 0 and _index_id_51597 <= _end_id_51607 or _step_id_51602 < 0 and _index_id_51597 >= _end_id_51607 { + let idx : Int = _index_id_51597; CCNOT(xs[idx], ys[idx], xs[idx + 1]); - _index_id_51526 += _step_id_51531; + _index_id_51597 += _step_id_51602; } } { - let _range_id_51566 : Range = Length(xs) - 1..-1..1; - mutable _index_id_51569 : Int = _range_id_51566::Start; - let _step_id_51574 : Int = _range_id_51566::Step; - let _end_id_51579 : Int = _range_id_51566::End; - while _step_id_51574 > 0 and _index_id_51569 <= _end_id_51579 or _step_id_51574 < 0 and _index_id_51569 >= _end_id_51579 { - let idx : Int = _index_id_51569; + let _range_id_51637 : Range = Length(xs) - 1..-1..1; + mutable _index_id_51640 : Int = _range_id_51637::Start; + let _step_id_51645 : Int = _range_id_51637::Step; + let _end_id_51650 : Int = _range_id_51637::End; + while _step_id_51645 > 0 and _index_id_51640 <= _end_id_51650 or _step_id_51645 < 0 and _index_id_51640 >= _end_id_51650 { + let idx : Int = _index_id_51640; Controlled CNOT(controls, (xs[idx], ys[idx])); CCNOT(xs[idx - 1], ys[idx - 1], xs[idx]); - _index_id_51569 += _step_id_51574; + _index_id_51640 += _step_id_51645; } } @@ -1894,15 +1894,15 @@ fn shor_sample_full_pipeline_reachable_items() { { let _range : Range = Length(xs) - 1..-1..1; { - let _range_id_51609 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; - mutable _index_id_51612 : Int = _range_id_51609::Start; - let _step_id_51617 : Int = _range_id_51609::Step; - let _end_id_51622 : Int = _range_id_51609::End; - while _step_id_51617 > 0 and _index_id_51612 <= _end_id_51622 or _step_id_51617 < 0 and _index_id_51612 >= _end_id_51622 { - let idx : Int = _index_id_51612; + let _range_id_51680 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; + mutable _index_id_51683 : Int = _range_id_51680::Start; + let _step_id_51688 : Int = _range_id_51680::Step; + let _end_id_51693 : Int = _range_id_51680::End; + while _step_id_51688 > 0 and _index_id_51683 <= _end_id_51693 or _step_id_51688 < 0 and _index_id_51683 >= _end_id_51693 { + let idx : Int = _index_id_51683; Adjoint CCNOT(xs[idx - 1], ys[idx - 1], xs[idx]); Adjoint Controlled CNOT(controls, (xs[idx], ys[idx])); - _index_id_51612 += _step_id_51617; + _index_id_51683 += _step_id_51688; } } @@ -1912,14 +1912,14 @@ fn shor_sample_full_pipeline_reachable_items() { { let _range : Range = 0..Length(xs) - 2; { - let _range_id_51652 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; - mutable _index_id_51655 : Int = _range_id_51652::Start; - let _step_id_51660 : Int = _range_id_51652::Step; - let _end_id_51665 : Int = _range_id_51652::End; - while _step_id_51660 > 0 and _index_id_51655 <= _end_id_51665 or _step_id_51660 < 0 and _index_id_51655 >= _end_id_51665 { - let idx : Int = _index_id_51655; + let _range_id_51723 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; + mutable _index_id_51726 : Int = _range_id_51723::Start; + let _step_id_51731 : Int = _range_id_51723::Step; + let _end_id_51736 : Int = _range_id_51723::End; + while _step_id_51731 > 0 and _index_id_51726 <= _end_id_51736 or _step_id_51731 < 0 and _index_id_51726 >= _end_id_51736 { + let idx : Int = _index_id_51726; Adjoint CCNOT(xs[idx], ys[idx], xs[idx + 1]); - _index_id_51655 += _step_id_51660; + _index_id_51726 += _step_id_51731; } } @@ -1940,29 +1940,29 @@ fn shor_sample_full_pipeline_reachable_items() { Fact(Length(xs) > 0, $"Array should not be empty."); let nQubits : Int = Length(xs); { - let _range_id_51695 : Range = 0..nQubits - 2; - mutable _index_id_51698 : Int = _range_id_51695::Start; - let _step_id_51703 : Int = _range_id_51695::Step; - let _end_id_51708 : Int = _range_id_51695::End; - while _step_id_51703 > 0 and _index_id_51698 <= _end_id_51708 or _step_id_51703 < 0 and _index_id_51698 >= _end_id_51708 { - let idx : Int = _index_id_51698; + let _range_id_51766 : Range = 0..nQubits - 2; + mutable _index_id_51769 : Int = _range_id_51766::Start; + let _step_id_51774 : Int = _range_id_51766::Step; + let _end_id_51779 : Int = _range_id_51766::End; + while _step_id_51774 > 0 and _index_id_51769 <= _end_id_51779 or _step_id_51774 < 0 and _index_id_51769 >= _end_id_51779 { + let idx : Int = _index_id_51769; CCNOT(xs[idx], ys[idx], xs[idx + 1]); - _index_id_51698 += _step_id_51703; + _index_id_51769 += _step_id_51774; } } Controlled CCNOT(controls, (xs[nQubits - 1], ys[nQubits - 1], ys[nQubits])); { - let _range_id_51738 : Range = nQubits - 1..-1..1; - mutable _index_id_51741 : Int = _range_id_51738::Start; - let _step_id_51746 : Int = _range_id_51738::Step; - let _end_id_51751 : Int = _range_id_51738::End; - while _step_id_51746 > 0 and _index_id_51741 <= _end_id_51751 or _step_id_51746 < 0 and _index_id_51741 >= _end_id_51751 { - let idx : Int = _index_id_51741; + let _range_id_51809 : Range = nQubits - 1..-1..1; + mutable _index_id_51812 : Int = _range_id_51809::Start; + let _step_id_51817 : Int = _range_id_51809::Step; + let _end_id_51822 : Int = _range_id_51809::End; + while _step_id_51817 > 0 and _index_id_51812 <= _end_id_51822 or _step_id_51817 < 0 and _index_id_51812 >= _end_id_51822 { + let idx : Int = _index_id_51812; Controlled CNOT(controls, (xs[idx], ys[idx])); CCNOT(xs[idx - 1], ys[idx - 1], xs[idx]); - _index_id_51741 += _step_id_51746; + _index_id_51812 += _step_id_51817; } } @@ -1975,15 +1975,15 @@ fn shor_sample_full_pipeline_reachable_items() { { let _range : Range = nQubits - 1..-1..1; { - let _range_id_51781 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; - mutable _index_id_51784 : Int = _range_id_51781::Start; - let _step_id_51789 : Int = _range_id_51781::Step; - let _end_id_51794 : Int = _range_id_51781::End; - while _step_id_51789 > 0 and _index_id_51784 <= _end_id_51794 or _step_id_51789 < 0 and _index_id_51784 >= _end_id_51794 { - let idx : Int = _index_id_51784; + let _range_id_51852 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; + mutable _index_id_51855 : Int = _range_id_51852::Start; + let _step_id_51860 : Int = _range_id_51852::Step; + let _end_id_51865 : Int = _range_id_51852::End; + while _step_id_51860 > 0 and _index_id_51855 <= _end_id_51865 or _step_id_51860 < 0 and _index_id_51855 >= _end_id_51865 { + let idx : Int = _index_id_51855; Adjoint CCNOT(xs[idx - 1], ys[idx - 1], xs[idx]); Adjoint Controlled CNOT(controls, (xs[idx], ys[idx])); - _index_id_51784 += _step_id_51789; + _index_id_51855 += _step_id_51860; } } @@ -1994,14 +1994,14 @@ fn shor_sample_full_pipeline_reachable_items() { { let _range : Range = 0..nQubits - 2; { - let _range_id_51824 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; - mutable _index_id_51827 : Int = _range_id_51824::Start; - let _step_id_51832 : Int = _range_id_51824::Step; - let _end_id_51837 : Int = _range_id_51824::End; - while _step_id_51832 > 0 and _index_id_51827 <= _end_id_51837 or _step_id_51832 < 0 and _index_id_51827 >= _end_id_51837 { - let idx : Int = _index_id_51827; + let _range_id_51895 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; + mutable _index_id_51898 : Int = _range_id_51895::Start; + let _step_id_51903 : Int = _range_id_51895::Step; + let _end_id_51908 : Int = _range_id_51895::End; + while _step_id_51903 > 0 and _index_id_51898 <= _end_id_51908 or _step_id_51903 < 0 and _index_id_51898 >= _end_id_51908 { + let idx : Int = _index_id_51898; Adjoint CCNOT(xs[idx], ys[idx], xs[idx + 1]); - _index_id_51827 += _step_id_51832; + _index_id_51898 += _step_id_51903; } } @@ -2079,7 +2079,7 @@ fn shor_sample_full_pipeline_reachable_items() { if c != 0 { let j : Int = TrailingZeroCountI(c); let x : Qubit[] = AllocateQubitArray(ysLen - j); - let _generated_ident_54421 : Unit = { + let _generated_ident_54492 : Unit = { { ApplyXorInPlace(c >>> j, x); } @@ -2094,7 +2094,7 @@ fn shor_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(x); - _generated_ident_54421 + _generated_ident_54492 } } @@ -2106,7 +2106,7 @@ fn shor_sample_full_pipeline_reachable_items() { if c != 0 { let j : Int = TrailingZeroCountI(c); let x : Qubit[] = AllocateQubitArray(ysLen - j); - let _generated_ident_54435 : Unit = { + let _generated_ident_54506 : Unit = { { ApplyXorInPlace(c >>> j, x); } @@ -2121,7 +2121,7 @@ fn shor_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(x); - _generated_ident_54435 + _generated_ident_54506 } } @@ -2133,7 +2133,7 @@ fn shor_sample_full_pipeline_reachable_items() { if c != 0 { let j : Int = TrailingZeroCountI(c); let x : Qubit[] = AllocateQubitArray(ysLen - j); - let _generated_ident_54449 : Unit = { + let _generated_ident_54520 : Unit = { { ApplyXorInPlace(c >>> j, x); } @@ -2148,7 +2148,7 @@ fn shor_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(x); - _generated_ident_54449 + _generated_ident_54520 } } @@ -2160,7 +2160,7 @@ fn shor_sample_full_pipeline_reachable_items() { if c != 0 { let j : Int = TrailingZeroCountI(c); let x : Qubit[] = AllocateQubitArray(ysLen - j); - let _generated_ident_54463 : Unit = { + let _generated_ident_54534 : Unit = { { ApplyXorInPlace(c >>> j, x); } @@ -2175,7 +2175,7 @@ fn shor_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(x); - _generated_ident_54463 + _generated_ident_54534 } } @@ -2707,21 +2707,21 @@ fn shor_sample_full_pipeline_reachable_items() { [Head_Qubit_(xNormalized)] + Most_Qubit_(qs) }; Fact(Length(cs1) == Length(qs), $"Arrays should be of the same length."); - let _generated_ident_54592 : Unit = { + let _generated_ident_54663 : Unit = { { { - let _range_id_52540 : Range = 0..Length(cs1) - 1; - mutable _index_id_52543 : Int = _range_id_52540::Start; - let _step_id_52548 : Int = _range_id_52540::Step; - let _end_id_52553 : Int = _range_id_52540::End; - while _step_id_52548 > 0 and _index_id_52543 <= _end_id_52553 or _step_id_52548 < 0 and _index_id_52543 >= _end_id_52553 { - let i : Int = _index_id_52543; + let _range_id_52611 : Range = 0..Length(cs1) - 1; + mutable _index_id_52614 : Int = _range_id_52611::Start; + let _step_id_52619 : Int = _range_id_52611::Step; + let _end_id_52624 : Int = _range_id_52611::End; + while _step_id_52619 > 0 and _index_id_52614 <= _end_id_52624 or _step_id_52619 < 0 and _index_id_52614 >= _end_id_52624 { + let i : Int = _index_id_52614; if cNormalized &&& 1L <<< i + 1 != 0L { AND(cs1[i], xNormalized[i + 1], qs[i]) } else { ApplyOrAssuming0Target(cs1[i], xNormalized[i + 1], qs[i]) }; - _index_id_52543 += _step_id_52548; + _index_id_52614 += _step_id_52619; } } @@ -2760,18 +2760,18 @@ fn shor_sample_full_pipeline_reachable_items() { { let _range : Range = 0..Length(cs1) - 1; { - let _range_id_52583 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; - mutable _index_id_52586 : Int = _range_id_52583::Start; - let _step_id_52591 : Int = _range_id_52583::Step; - let _end_id_52596 : Int = _range_id_52583::End; - while _step_id_52591 > 0 and _index_id_52586 <= _end_id_52596 or _step_id_52591 < 0 and _index_id_52586 >= _end_id_52596 { - let i : Int = _index_id_52586; + let _range_id_52654 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; + mutable _index_id_52657 : Int = _range_id_52654::Start; + let _step_id_52662 : Int = _range_id_52654::Step; + let _end_id_52667 : Int = _range_id_52654::End; + while _step_id_52662 > 0 and _index_id_52657 <= _end_id_52667 or _step_id_52662 < 0 and _index_id_52657 >= _end_id_52667 { + let i : Int = _index_id_52657; if cNormalized &&& 1L <<< i + 1 != 0L { Adjoint AND(cs1[i], xNormalized[i + 1], qs[i]) } else { Adjoint ApplyOrAssuming0Target(cs1[i], xNormalized[i + 1], qs[i]) }; - _index_id_52586 += _step_id_52591; + _index_id_52657 += _step_id_52662; } } @@ -2783,7 +2783,7 @@ fn shor_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(qs); - _generated_ident_54592 + _generated_ident_54663 } } @@ -2816,21 +2816,21 @@ fn shor_sample_full_pipeline_reachable_items() { [Head_Qubit_(xNormalized)] + Most_Qubit_(qs) }; Fact(Length(cs1) == Length(qs), $"Arrays should be of the same length."); - let _generated_ident_54606 : Unit = { + let _generated_ident_54677 : Unit = { { { - let _range_id_52626 : Range = 0..Length(cs1) - 1; - mutable _index_id_52629 : Int = _range_id_52626::Start; - let _step_id_52634 : Int = _range_id_52626::Step; - let _end_id_52639 : Int = _range_id_52626::End; - while _step_id_52634 > 0 and _index_id_52629 <= _end_id_52639 or _step_id_52634 < 0 and _index_id_52629 >= _end_id_52639 { - let i : Int = _index_id_52629; + let _range_id_52697 : Range = 0..Length(cs1) - 1; + mutable _index_id_52700 : Int = _range_id_52697::Start; + let _step_id_52705 : Int = _range_id_52697::Step; + let _end_id_52710 : Int = _range_id_52697::End; + while _step_id_52705 > 0 and _index_id_52700 <= _end_id_52710 or _step_id_52705 < 0 and _index_id_52700 >= _end_id_52710 { + let i : Int = _index_id_52700; if cNormalized &&& 1L <<< i + 1 != 0L { AND(cs1[i], xNormalized[i + 1], qs[i]) } else { ApplyOrAssuming0Target(cs1[i], xNormalized[i + 1], qs[i]) }; - _index_id_52629 += _step_id_52634; + _index_id_52700 += _step_id_52705; } } @@ -2869,18 +2869,18 @@ fn shor_sample_full_pipeline_reachable_items() { { let _range : Range = 0..Length(cs1) - 1; { - let _range_id_52669 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; - mutable _index_id_52672 : Int = _range_id_52669::Start; - let _step_id_52677 : Int = _range_id_52669::Step; - let _end_id_52682 : Int = _range_id_52669::End; - while _step_id_52677 > 0 and _index_id_52672 <= _end_id_52682 or _step_id_52677 < 0 and _index_id_52672 >= _end_id_52682 { - let i : Int = _index_id_52672; + let _range_id_52740 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; + mutable _index_id_52743 : Int = _range_id_52740::Start; + let _step_id_52748 : Int = _range_id_52740::Step; + let _end_id_52753 : Int = _range_id_52740::End; + while _step_id_52748 > 0 and _index_id_52743 <= _end_id_52753 or _step_id_52748 < 0 and _index_id_52743 >= _end_id_52753 { + let i : Int = _index_id_52743; if cNormalized &&& 1L <<< i + 1 != 0L { Adjoint AND(cs1[i], xNormalized[i + 1], qs[i]) } else { Adjoint ApplyOrAssuming0Target(cs1[i], xNormalized[i + 1], qs[i]) }; - _index_id_52672 += _step_id_52677; + _index_id_52743 += _step_id_52748; } } @@ -2892,7 +2892,7 @@ fn shor_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(qs); - _generated_ident_54606 + _generated_ident_54677 } } @@ -2925,21 +2925,21 @@ fn shor_sample_full_pipeline_reachable_items() { [Head_Qubit_(xNormalized)] + Most_Qubit_(qs) }; Fact(Length(cs1) == Length(qs), $"Arrays should be of the same length."); - let _generated_ident_54620 : Unit = { + let _generated_ident_54691 : Unit = { { { - let _range_id_52712 : Range = 0..Length(cs1) - 1; - mutable _index_id_52715 : Int = _range_id_52712::Start; - let _step_id_52720 : Int = _range_id_52712::Step; - let _end_id_52725 : Int = _range_id_52712::End; - while _step_id_52720 > 0 and _index_id_52715 <= _end_id_52725 or _step_id_52720 < 0 and _index_id_52715 >= _end_id_52725 { - let i : Int = _index_id_52715; + let _range_id_52783 : Range = 0..Length(cs1) - 1; + mutable _index_id_52786 : Int = _range_id_52783::Start; + let _step_id_52791 : Int = _range_id_52783::Step; + let _end_id_52796 : Int = _range_id_52783::End; + while _step_id_52791 > 0 and _index_id_52786 <= _end_id_52796 or _step_id_52791 < 0 and _index_id_52786 >= _end_id_52796 { + let i : Int = _index_id_52786; if cNormalized &&& 1L <<< i + 1 != 0L { AND(cs1[i], xNormalized[i + 1], qs[i]) } else { ApplyOrAssuming0Target(cs1[i], xNormalized[i + 1], qs[i]) }; - _index_id_52715 += _step_id_52720; + _index_id_52786 += _step_id_52791; } } @@ -2978,18 +2978,18 @@ fn shor_sample_full_pipeline_reachable_items() { { let _range : Range = 0..Length(cs1) - 1; { - let _range_id_52755 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; - mutable _index_id_52758 : Int = _range_id_52755::Start; - let _step_id_52763 : Int = _range_id_52755::Step; - let _end_id_52768 : Int = _range_id_52755::End; - while _step_id_52763 > 0 and _index_id_52758 <= _end_id_52768 or _step_id_52763 < 0 and _index_id_52758 >= _end_id_52768 { - let i : Int = _index_id_52758; + let _range_id_52826 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; + mutable _index_id_52829 : Int = _range_id_52826::Start; + let _step_id_52834 : Int = _range_id_52826::Step; + let _end_id_52839 : Int = _range_id_52826::End; + while _step_id_52834 > 0 and _index_id_52829 <= _end_id_52839 or _step_id_52834 < 0 and _index_id_52829 >= _end_id_52839 { + let i : Int = _index_id_52829; if cNormalized &&& 1L <<< i + 1 != 0L { Adjoint AND(cs1[i], xNormalized[i + 1], qs[i]) } else { Adjoint ApplyOrAssuming0Target(cs1[i], xNormalized[i + 1], qs[i]) }; - _index_id_52758 += _step_id_52763; + _index_id_52829 += _step_id_52834; } } @@ -3001,7 +3001,7 @@ fn shor_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(qs); - _generated_ident_54620 + _generated_ident_54691 } } @@ -3034,21 +3034,21 @@ fn shor_sample_full_pipeline_reachable_items() { [Head_Qubit_(xNormalized)] + Most_Qubit_(qs) }; Fact(Length(cs1) == Length(qs), $"Arrays should be of the same length."); - let _generated_ident_54634 : Unit = { + let _generated_ident_54705 : Unit = { { { - let _range_id_52798 : Range = 0..Length(cs1) - 1; - mutable _index_id_52801 : Int = _range_id_52798::Start; - let _step_id_52806 : Int = _range_id_52798::Step; - let _end_id_52811 : Int = _range_id_52798::End; - while _step_id_52806 > 0 and _index_id_52801 <= _end_id_52811 or _step_id_52806 < 0 and _index_id_52801 >= _end_id_52811 { - let i : Int = _index_id_52801; + let _range_id_52869 : Range = 0..Length(cs1) - 1; + mutable _index_id_52872 : Int = _range_id_52869::Start; + let _step_id_52877 : Int = _range_id_52869::Step; + let _end_id_52882 : Int = _range_id_52869::End; + while _step_id_52877 > 0 and _index_id_52872 <= _end_id_52882 or _step_id_52877 < 0 and _index_id_52872 >= _end_id_52882 { + let i : Int = _index_id_52872; if cNormalized &&& 1L <<< i + 1 != 0L { AND(cs1[i], xNormalized[i + 1], qs[i]) } else { ApplyOrAssuming0Target(cs1[i], xNormalized[i + 1], qs[i]) }; - _index_id_52801 += _step_id_52806; + _index_id_52872 += _step_id_52877; } } @@ -3087,18 +3087,18 @@ fn shor_sample_full_pipeline_reachable_items() { { let _range : Range = 0..Length(cs1) - 1; { - let _range_id_52841 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; - mutable _index_id_52844 : Int = _range_id_52841::Start; - let _step_id_52849 : Int = _range_id_52841::Step; - let _end_id_52854 : Int = _range_id_52841::End; - while _step_id_52849 > 0 and _index_id_52844 <= _end_id_52854 or _step_id_52849 < 0 and _index_id_52844 >= _end_id_52854 { - let i : Int = _index_id_52844; + let _range_id_52912 : Range = _range::Start + _range::End - _range::Start / _range::Step * _range::Step..-_range::Step.._range::Start; + mutable _index_id_52915 : Int = _range_id_52912::Start; + let _step_id_52920 : Int = _range_id_52912::Step; + let _end_id_52925 : Int = _range_id_52912::End; + while _step_id_52920 > 0 and _index_id_52915 <= _end_id_52925 or _step_id_52920 < 0 and _index_id_52915 >= _end_id_52925 { + let i : Int = _index_id_52915; if cNormalized &&& 1L <<< i + 1 != 0L { Adjoint AND(cs1[i], xNormalized[i + 1], qs[i]) } else { Adjoint ApplyOrAssuming0Target(cs1[i], xNormalized[i + 1], qs[i]) }; - _index_id_52844 += _step_id_52849; + _index_id_52915 += _step_id_52920; } } @@ -3110,7 +3110,7 @@ fn shor_sample_full_pipeline_reachable_items() { _apply_res }; ReleaseQubitArray(qs); - _generated_ident_54634 + _generated_ident_54705 } } diff --git a/source/compiler/qsc_fir_transforms/src/tuple_decompose.rs b/source/compiler/qsc_fir_transforms/src/tuple_decompose.rs index db9cebbfb96..b95d0598f2f 100644 --- a/source/compiler/qsc_fir_transforms/src/tuple_decompose.rs +++ b/source/compiler/qsc_fir_transforms/src/tuple_decompose.rs @@ -42,7 +42,8 @@ mod tests; mod semantic_equivalence_tests; use crate::fir_builder::{ - alloc_local_var_expr, decompose_binding, functored_specs, reachable_local_callables, + alloc_assign_expr, alloc_field_path_expr, alloc_local_var_expr, alloc_semi_stmt, + decompose_binding, functored_specs, reachable_local_callables, }; use crate::package_assigners::PackageAssigners; use crate::reachability::{ @@ -53,7 +54,7 @@ use crate::walk_utils::{UseClass, classify_block_use, collect_expr_ids_in_local_ use qsc_data_structures::span::Span; use qsc_fir::assigner::Assigner; use qsc_fir::fir::{ - Block, BlockId, CallableDecl, CallableImpl, Expr, ExprId, ExprKind, Field, FieldPath, ItemKind, + Block, BlockId, CallableDecl, CallableImpl, Expr, ExprId, ExprKind, Field, ItemKind, LocalItemId, LocalVarId, Package, PackageId, PackageLookup, PackageStore, Pat, PatId, PatKind, Res, SpecDecl, SpecImpl, Stmt, StmtId, StmtKind, StoreItemId, }; @@ -62,8 +63,6 @@ use qsc_fir::visit::{self, Visitor}; use rustc_hash::{FxHashMap, FxHashSet}; use std::rc::Rc; -use crate::EMPTY_EXEC_RANGE; - /// Runs the tuple-decompose pass across the entry-reachable package closure. /// /// For each local binding whose type resolves to a multi-field tuple, @@ -426,7 +425,6 @@ fn rewrite_field_accesses( /// and a fresh `Field(.., Path([j, ...]))` wrapper. Redirecting references /// instead of mutating the original projection keeps shared expression nodes /// stable for sibling projections created by earlier passes. -#[allow(clippy::too_many_lines)] fn rewrite_single_expr( package: &mut Package, assigner: &mut Assigner, @@ -465,19 +463,13 @@ fn rewrite_single_expr( let ty = elem_types[idx].clone(); alloc_local_var_expr(package, assigner, new_local, ty, span) }; - let replacement_id = assigner.next_expr(); - package.exprs.insert( - replacement_id, - Expr { - id: replacement_id, - span, - ty: expr_ty, - kind: ExprKind::Field( - new_inner_id, - Field::Path(FieldPath { indices: remaining }), - ), - exec_graph_range: EMPTY_EXEC_RANGE, - }, + let replacement_id = alloc_field_path_expr( + package, + assigner, + new_inner_id, + remaining, + expr_ty, + span, ); replace_expr_references(package, owner_item, expr_id, replacement_id); } @@ -744,15 +736,13 @@ fn rewrite_assign_tuples( // Rewrite the original Assign in-place to target the first element. { // Create a new Var expr for the first element's LHS. - let new_lhs_id = assigner.next_expr(); - let new_lhs = Expr { - id: new_lhs_id, - span: Span::default(), - ty: elem_types[0].clone(), - kind: ExprKind::Var(Res::Local(new_locals[0]), vec![]), - exec_graph_range: EMPTY_EXEC_RANGE, - }; - package.exprs.insert(new_lhs_id, new_lhs); + let new_lhs_id = alloc_local_var_expr( + package, + assigner, + new_locals[0], + elem_types[0].clone(), + Span::default(), + ); let assign = package .exprs @@ -765,34 +755,16 @@ fn rewrite_assign_tuples( // For elements 1..n, create new Assign exprs and Semi stmts. let mut new_stmt_ids: Vec = Vec::with_capacity(n - 1); for i in 1..n { - let lhs_id = assigner.next_expr(); - let lhs_expr = Expr { - id: lhs_id, - span: Span::default(), - ty: elem_types[i].clone(), - kind: ExprKind::Var(Res::Local(new_locals[i]), vec![]), - exec_graph_range: EMPTY_EXEC_RANGE, - }; - package.exprs.insert(lhs_id, lhs_expr); - - let assign_id = assigner.next_expr(); - let assign_expr = Expr { - id: assign_id, - span: Span::default(), - ty: Ty::UNIT, - kind: ExprKind::Assign(lhs_id, elements[i]), - exec_graph_range: EMPTY_EXEC_RANGE, - }; - package.exprs.insert(assign_id, assign_expr); - - let new_stmt_id = assigner.next_stmt(); - let new_stmt = Stmt { - id: new_stmt_id, - span: Span::default(), - kind: StmtKind::Semi(assign_id), - exec_graph_range: EMPTY_EXEC_RANGE, - }; - package.stmts.insert(new_stmt_id, new_stmt); + let lhs_id = alloc_local_var_expr( + package, + assigner, + new_locals[i], + elem_types[i].clone(), + Span::default(), + ); + let assign_id = + alloc_assign_expr(package, assigner, lhs_id, elements[i], Span::default()); + let new_stmt_id = alloc_semi_stmt(package, assigner, assign_id, Span::default()); new_stmt_ids.push(new_stmt_id); } diff --git a/source/compiler/qsc_fir_transforms/src/tuple_destructuring.rs b/source/compiler/qsc_fir_transforms/src/tuple_destructuring.rs index e1e75b31b78..b9378485a9b 100644 --- a/source/compiler/qsc_fir_transforms/src/tuple_destructuring.rs +++ b/source/compiler/qsc_fir_transforms/src/tuple_destructuring.rs @@ -20,16 +20,16 @@ //! Synthesized expressions use [`crate::EMPTY_EXEC_RANGE`]; //! [`crate::exec_graph_rebuild`] rebuilds exec graphs later. -use crate::EMPTY_EXEC_RANGE; +use crate::fir_builder::alloc_field_path_expr; +use crate::fir_builder::alloc_local_stmt; use crate::fir_builder::alloc_local_var_expr; use crate::fir_builder::reachable_local_callables; use crate::tuple_decompose::collect_all_block_ids_in_callable; use qsc_data_structures::span::Span; use qsc_fir::assigner::Assigner; use qsc_fir::fir::{ - BlockId, Expr, ExprId, ExprKind, Field, FieldPath, ItemKind, LocalItemId, LocalVarId, - Mutability, Package, PackageId, PackageLookup, PackageStore, PatId, PatKind, Res, Stmt, StmtId, - StmtKind, StoreItemId, + BlockId, ExprId, ExprKind, ItemKind, LocalItemId, LocalVarId, Mutability, Package, PackageId, + PackageLookup, PackageStore, PatId, PatKind, Res, StmtId, StmtKind, StoreItemId, }; use qsc_fir::ty::Ty; use rustc_hash::FxHashSet; @@ -357,15 +357,13 @@ fn apply_destructure_rewrite( // Allocate fresh statements for the remaining projections. let mut new_stmt_ids: Vec = Vec::with_capacity(descriptors.len() - 1); for &(mutability, pat_id, rhs_id) in &descriptors[1..] { - let stmt_id = assigner.next_stmt(); - package.stmts.insert( - stmt_id, - Stmt { - id: stmt_id, - span: Span::default(), - kind: StmtKind::Local(mutability, pat_id, rhs_id), - exec_graph_range: EMPTY_EXEC_RANGE, - }, + let stmt_id = alloc_local_stmt( + package, + assigner, + mutability, + pat_id, + rhs_id, + Span::default(), ); new_stmt_ids.push(stmt_id); } @@ -437,21 +435,12 @@ fn create_local_projection_path( tuple_ty.clone(), Span::default(), ); - let field_expr_id = assigner.next_expr(); - package.exprs.insert( - field_expr_id, - Expr { - id: field_expr_id, - span: Span::default(), - ty: leaf_ty.clone(), - kind: ExprKind::Field( - base_id, - Field::Path(FieldPath { - indices: indices.to_vec(), - }), - ), - exec_graph_range: EMPTY_EXEC_RANGE, - }, - ); - field_expr_id + alloc_field_path_expr( + package, + assigner, + base_id, + indices.to_vec(), + leaf_ty.clone(), + Span::default(), + ) } diff --git a/source/compiler/qsc_openqasm_compiler/src/tests/statement/gate_call.rs b/source/compiler/qsc_openqasm_compiler/src/tests/statement/gate_call.rs index d88278d8ca0..9e23aba942d 100644 --- a/source/compiler/qsc_openqasm_compiler/src/tests/statement/gate_call.rs +++ b/source/compiler/qsc_openqasm_compiler/src/tests/statement/gate_call.rs @@ -234,7 +234,7 @@ fn barrier_generates_qir() -> miette::Result<(), Vec> { c[0] = measure q[0]; "#; - let qsharp = compile_qasm_to_qir(source)?; + let qir = compile_qasm_to_qir(source)?; expect![[r#" %Result = type opaque %Qubit = type opaque @@ -278,7 +278,7 @@ fn barrier_generates_qir() -> miette::Result<(), Vec> { !3 = !{i32 1, !"dynamic_result_management", i1 false} !4 = !{i32 5, !"int_computations", !{!"i64"}} "#]] - .assert_eq(&qsharp); + .assert_eq(&qir); Ok(()) } @@ -445,6 +445,228 @@ fn implicit_cast_to_angle_works() -> miette::Result<(), Vec> { Ok(()) } +#[test] +#[allow(clippy::too_many_lines)] +fn custom_gate_with_angle_parameter_generates_qir_adaptive_rif() -> miette::Result<(), Vec> +{ + let source = r#" + OPENQASM 3.0; + include "stdgates.inc"; + #pragma qdk.qir.profile Adaptive_RIF + gate phase_by(theta) q { + rz(theta) q; + } + qubit q; + output bit c; + phase_by(1.0) q; + c = measure q; + "#; + + let qir = compile_qasm_to_qir(source)?; + expect![[r#" + %Result = type opaque + %Qubit = type opaque + + @0 = internal constant [4 x i8] c"0_t\00" + + define i64 @ENTRYPOINT__main() #0 { + block_0: + call void @__quantum__rt__initialize(i8* null) + call void @__quantum__qis__rz__body(double 1.0000000000000002, %Qubit* inttoptr (i64 0 to %Qubit*)) + call void @__quantum__qis__m__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 0 to %Result*)) + call void @__quantum__rt__tuple_record_output(i64 0, i8* getelementptr inbounds ([4 x i8], [4 x i8]* @0, i64 0, i64 0)) + ret i64 0 + } + + declare void @__quantum__rt__initialize(i8*) + + declare void @__quantum__qis__rz__body(double, %Qubit*) + + declare void @__quantum__qis__m__body(%Qubit*, %Result*) #1 + + declare void @__quantum__rt__tuple_record_output(i64, i8*) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="1" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5} + + !0 = !{i32 1, !"qir_major_version", i32 1} + !1 = !{i32 7, !"qir_minor_version", i32 0} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + "#]] + .assert_eq(&qir); + Ok(()) +} + +#[test] +#[allow(clippy::too_many_lines)] +fn custom_gate_with_angle_parameter_generates_qir_adaptive() -> miette::Result<(), Vec> { + let source = r#" + OPENQASM 3.0; + include "stdgates.inc"; + #pragma qdk.qir.profile Adaptive + gate phase_by(theta) q { + rz(theta) q; + } + qubit q; + output bit c; + phase_by(1.0) q; + c = measure q; + "#; + + let qir = compile_qasm_to_qir(source)?; + expect![[r#" + @0 = internal constant [4 x i8] c"0_t\00" + + define i64 @ENTRYPOINT__main() #0 { + block_0: + call void @__quantum__rt__initialize(ptr null) + call void @phase_by(i64 1433540284805665, i64 53, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__m__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__tuple_record_output(i64 0, ptr @0) + ret i64 0 + } + + declare void @__quantum__rt__initialize(ptr) + + define void @phase_by(i64 %var_0, i64 %var_1, ptr %var_2) { + block_1: + call void @rz(i64 %var_0, i64 %var_1, ptr %var_2) + ret void + } + + define void @rz(i64 %var_3, i64 %var_4, ptr %var_5) { + block_2: + %var_42 = call double @AngleAsDouble(i64 %var_3, i64 %var_4) + call void @Rz(double %var_42, ptr %var_5) + ret void + } + + define double @AngleAsDouble(i64 %var_6, i64 %var_7) { + block_3: + %var_9 = alloca i64 + %var_14 = alloca i64 + %var_19 = alloca i64 + %var_21 = alloca i64 + %var_23 = alloca i1 + %var_25 = alloca i1 + %var_28 = alloca i64 + %var_30 = alloca i64 + %var_32 = alloca i64 + %var_8 = icmp sgt i64 %var_7, 53 + br i1 %var_8, label %block_4, label %block_5 + block_4: + %var_10 = sub i64 %var_7, 53 + %var_12 = sub i64 %var_10, 1 + %var_13 = shl i64 1, %var_12 + store i64 %var_13, ptr %var_14 + %var_15 = shl i64 1, %var_10 + %var_16 = sub i64 %var_15, 1 + %var_18 = and i64 %var_6, %var_16 + store i64 %var_18, ptr %var_19 + %var_20 = ashr i64 %var_6, %var_10 + store i64 %var_20, ptr %var_21 + %var_53 = load i64, ptr %var_19 + %var_54 = load i64, ptr %var_14 + %var_22 = icmp sgt i64 %var_53, %var_54 + store i1 true, ptr %var_23 + br i1 %var_22, label %block_9, label %block_6 + block_5: + store i64 %var_6, ptr %var_9 + br label %block_13 + block_6: + %var_56 = load i64, ptr %var_19 + %var_57 = load i64, ptr %var_14 + %var_24 = icmp eq i64 %var_56, %var_57 + store i1 false, ptr %var_25 + br i1 %var_24, label %block_7, label %block_8 + block_7: + %var_68 = load i64, ptr %var_21 + %var_26 = and i64 %var_68, 1 + %var_27 = icmp eq i64 %var_26, 1 + store i1 %var_27, ptr %var_25 + br label %block_8 + block_8: + %var_59 = load i1, ptr %var_25 + store i1 %var_59, ptr %var_23 + br label %block_9 + block_9: + %var_61 = load i1, ptr %var_23 + br i1 %var_61, label %block_10, label %block_11 + block_10: + %var_66 = load i64, ptr %var_21 + %var_29 = add i64 %var_66, 1 + store i64 %var_29, ptr %var_28 + br label %block_12 + block_11: + %var_62 = load i64, ptr %var_21 + store i64 %var_62, ptr %var_28 + br label %block_12 + block_12: + %var_64 = load i64, ptr %var_28 + store i64 %var_64, ptr %var_9 + br label %block_13 + block_13: + %var_44 = load i64, ptr %var_9 + store i64 %var_44, ptr %var_30 + %var_31 = icmp sgt i64 %var_7, 53 + br i1 %var_31, label %block_14, label %block_15 + block_14: + store i64 53, ptr %var_32 + br label %block_16 + block_15: + store i64 %var_7, ptr %var_32 + br label %block_16 + block_16: + %var_47 = load i64, ptr %var_32 + %var_34 = shl i64 1, %var_47 + %var_35 = sitofp i64 %var_34 to double + %var_48 = load i64, ptr %var_30 + %var_37 = sitofp i64 %var_48 to double + %var_39 = fdiv double 6.283185307179586, %var_35 + %var_41 = fmul double %var_37, %var_39 + ret double %var_41 + } + + define void @Rz(double %var_44, ptr %var_45) { + block_17: + call void @__quantum__qis__rz__body(double %var_44, ptr %var_45) + ret void + } + + declare void @__quantum__qis__rz__body(double, ptr) + + declare void @__quantum__qis__m__body(ptr, ptr) #1 + + declare void @__quantum__rt__tuple_record_output(i64, ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="1" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7, !8} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + !8 = !{i32 1, !"ir_functions", i1 true} + "#]] + .assert_eq(&qir); + Ok(()) +} + #[test] fn custom_gate_can_be_called() -> miette::Result<(), Vec> { let source = r#" @@ -600,7 +822,7 @@ fn simulatable_intrinsic_on_gate_stmt_generates_correct_qir() -> miette::Result< bit result = measure q; "#; - let qsharp = compile_qasm_to_qir(source)?; + let qir = compile_qasm_to_qir(source)?; expect![[r#" %Result = type opaque %Qubit = type opaque @@ -636,7 +858,7 @@ fn simulatable_intrinsic_on_gate_stmt_generates_correct_qir() -> miette::Result< !2 = !{i32 1, !"dynamic_qubit_management", i1 false} !3 = !{i32 1, !"dynamic_result_management", i1 false} !4 = !{i32 5, !"int_computations", !{!"i64"}} - "#]].assert_eq(&qsharp); + "#]].assert_eq(&qir); Ok(()) } @@ -656,7 +878,7 @@ fn qdk_qir_intrinsic_on_gate_stmt_generates_correct_qir() -> miette::Result<(), bit result = measure q; "#; - let qsharp = compile_qasm_to_qir(source)?; + let qir = compile_qasm_to_qir(source)?; expect![[r#" %Result = type opaque %Qubit = type opaque @@ -692,7 +914,7 @@ fn qdk_qir_intrinsic_on_gate_stmt_generates_correct_qir() -> miette::Result<(), !2 = !{i32 1, !"dynamic_qubit_management", i1 false} !3 = !{i32 1, !"dynamic_result_management", i1 false} !4 = !{i32 5, !"int_computations", !{!"i64"}} - "#]].assert_eq(&qsharp); + "#]].assert_eq(&qir); Ok(()) } @@ -1565,7 +1787,7 @@ fn qasm2_barrier_generates_qir() -> miette::Result<(), Vec> { c[0] = measure q[0]; "#; - let qsharp = compile_qasm_to_qir(source)?; + let qir = compile_qasm_to_qir(source)?; expect![[r#" %Result = type opaque %Qubit = type opaque @@ -1609,7 +1831,7 @@ fn qasm2_barrier_generates_qir() -> miette::Result<(), Vec> { !3 = !{i32 1, !"dynamic_result_management", i1 false} !4 = !{i32 5, !"int_computations", !{!"i64"}} "#]] - .assert_eq(&qsharp); + .assert_eq(&qir); Ok(()) } @@ -1831,7 +2053,7 @@ fn qasm2_simulatable_intrinsic_on_gate_stmt_generates_correct_qir() result = measure q; "#; - let qsharp = compile_qasm_to_qir(source)?; + let qir = compile_qasm_to_qir(source)?; expect![[r#" %Result = type opaque %Qubit = type opaque @@ -1871,7 +2093,7 @@ fn qasm2_simulatable_intrinsic_on_gate_stmt_generates_correct_qir() !2 = !{i32 1, !"dynamic_qubit_management", i1 false} !3 = !{i32 1, !"dynamic_result_management", i1 false} !4 = !{i32 5, !"int_computations", !{!"i64"}} - "#]].assert_eq(&qsharp); + "#]].assert_eq(&qir); Ok(()) } diff --git a/source/qdk_package/tests/test_callable_passing.py b/source/qdk_package/tests/test_callable_passing.py index 037edd11efa..8dbdfbc767b 100644 --- a/source/qdk_package/tests/test_callable_passing.py +++ b/source/qdk_package/tests/test_callable_passing.py @@ -263,6 +263,117 @@ def test_qir_from_qsharp_closure_passed_to_python_callable() -> None: ) +def test_same_target_multi_closure_args_generate_qir() -> None: + # Python mirror of the Rust + # `same_target_multi_closure_args_route_to_synthetic_entry_and_generate_qir` + # test in source/compiler/qsc/src/codegen/tests.rs. + qsharp.init(target_profile=qsharp.TargetProfile.Base) + qsharp.eval(""" + operation InvokeThree( + first : Qubit => Unit, + second : Qubit => Unit, + third : Qubit => Unit + ) : Unit { + use q = Qubit(); + first(q); + second(q); + third(q); + } + + function MakeRz(theta : Double) : Qubit => Unit { + Rz(theta, _) + } + """) + from qdk.code import InvokeThree + + first = qsharp.eval("MakeRz(1.0)") + second = qsharp.eval("MakeRz(2.0)") + third = qsharp.eval("MakeRz(3.0)") + + qir = str(qsharp.compile(InvokeThree, first, second, third)) + expected_calls = [ + "call void @__quantum__qis__rz__body(double 1.0,", + "call void @__quantum__qis__rz__body(double 2.0,", + "call void @__quantum__qis__rz__body(double 3.0,", + ] + assert [qir.count(call) for call in expected_calls] == [1, 1, 1] + positions = [qir.index(call) for call in expected_calls] + assert positions == sorted(positions) + + +def test_nested_closure_arg_generates_inner_effect() -> None: + # Python mirror of the Rust + # `nested_closure_arg_routes_to_synthetic_entry_and_generates_inner_effect` + # test in source/compiler/qsc/src/codegen/tests.rs. + qsharp.init(target_profile=qsharp.TargetProfile.Base) + qsharp.eval(""" + operation InvokeOne(op : Qubit => Unit) : Unit { + use q = Qubit(); + op(q); + } + + function MakeRz(theta : Double) : Qubit => Unit { + Rz(theta, _) + } + + function MakeOuter(inner : Qubit => Unit) : Qubit => Unit { + inner(_) + } + """) + from qdk.code import InvokeOne + + outer = qsharp.eval("let inner = MakeRz(4.0); MakeOuter(inner)") + + qir = str(qsharp.compile(InvokeOne, outer)) + assert qir.count("call void @__quantum__qis__rz__body(double 4.0,") == 1 + + +def test_qir_from_returned_for_each_callable_capturing_length_alias() -> None: + qsharp.init(target_profile=qsharp.TargetProfile.Adaptive_RI) + qsharp.eval(""" + import Std.Arrays.ForEach; + function Make() : Int[][] => Int[] { + let f = Length; + ForEach(arr => f(arr), _) + } + operation Apply(op : Int[][] => Int[], arrs : Int[][]) : Int[] { + return op(arrs); + } + """) + + from qdk import code + + op = code.Make() + qir = str(qsharp.compile(code.Apply, op, [[1, 2], [1]])) + assert "i64 2" in qir + assert "i64 1" in qir + + +def test_qir_from_returned_for_each_callable_calling_length_directly() -> None: + qsharp.init(target_profile=qsharp.TargetProfile.Adaptive_RI) + qsharp.eval(""" + import Std.Arrays.ForEach; + function Make() : Int[][] => Int[] { + ForEach(arr => Length(arr), _) + } + operation ToLengths(arrs : Int[][]) : Int[] { + return Make()(arrs); + } + operation Apply(op : Int[][] => Int[], arrs : Int[][]) : Int[] { + return op(arrs); + } + """) + + from qdk import code + + assert qsharp.eval("ToLengths([[1, 2], [1]])") == [2, 1] + op = code.Make() + qir = str(qsharp.compile(code.Apply, op, [[1, 2], [1]])) + assert "__quantum__rt__array_record_output(i64 2" in qir + assert "__quantum__rt__int_record_output(i64 2" in qir + assert "__quantum__rt__int_record_output(i64 1" in qir + + def test_circuit_from_python_callable_passed_to_python_callable() -> None: qsharp.init(target_profile=qsharp.TargetProfile.Base) qsharp.eval(""" @@ -352,6 +463,342 @@ def test_qir_from_callable_returning_closure_passed_to_qsharp_callable() -> None assert "__quantum__qis__h__body" in str(qir) +def test_chemistry_like_controlled_factory_generates_qir() -> None: + # Python mirror of the Rust `chemistry_like_controlled_factory_generates_qir` + # test in source/compiler/qsc/src/codegen/tests.rs. + qsharp.init(target_profile=qsharp.TargetProfile.Base) + qsharp.eval(""" + operation PrepareIdentity(qs : Qubit[]) : Unit is Adj + Ctl {} + + operation SelectIdentity(systems : Qubit[], ancilla : Qubit[]) : Unit is Adj + Ctl {} + + function MakeControlledPrepSelPrepOp( + prepareOp : Qubit[] => Unit is Adj + Ctl, + selectOp : (Qubit[], Qubit[]) => Unit is Adj + Ctl, + numSystemQubits : Int, + numAncillaQubits : Int, + power : Int + ) : (Qubit, Qubit[]) => Unit { + (control, allQubits) => { + let systems = allQubits[0..numSystemQubits - 1]; + let ancilla = allQubits[numSystemQubits...]; + for _ in 0..power - 1 { + Controlled prepareOp([control], systems); + Controlled selectOp([control], (systems, ancilla)); + } + } + } + + operation MakeControlledPrepSelPrepCircuit( + prepareOp : Qubit[] => Unit is Adj + Ctl, + selectOp : (Qubit[], Qubit[]) => Unit is Adj + Ctl, + numSystemQubits : Int, + numAncillaQubits : Int, + power : Int + ) : Unit { + use control = Qubit(); + use systems = Qubit[numSystemQubits + numAncillaQubits]; + let op = MakeControlledPrepSelPrepOp( + prepareOp, + selectOp, + numSystemQubits, + numAncillaQubits, + power + ); + op(control, systems); + } + """) + from qdk.code import ( + MakeControlledPrepSelPrepCircuit, + PrepareIdentity, + SelectIdentity, + ) + + qir = str( + qsharp.compile( + MakeControlledPrepSelPrepCircuit, + PrepareIdentity, + SelectIdentity, + 1, + 1, + 1, + ) + ) + assert "define i64 @ENTRYPOINT__main()" in qir + + +def test_chemistry_like_state_preparation_closure_with_empty_expansion_ops_generates_qir() -> ( + None +): + # Python mirror of the Rust + # `chemistry_like_state_preparation_closure_with_empty_expansion_ops_generates_qir` + # test in source/compiler/qsc/src/codegen/tests.rs. + qsharp.init(target_profile=qsharp.TargetProfile.Base) + qsharp.eval(""" + struct StatePreparationParams { + rowMap : Int[], + stateVector : Double[], + expansionOps : Int[][], + numQubits : Int + } + + operation ApplyStatePreparation(params : StatePreparationParams, qs : Qubit[]) : Unit is Adj + Ctl { + if Length(params.expansionOps) != 0 { + X(qs[0]); + } + } + + operation MakeStatePreparationCircuit( + rowMap : Int[], + stateVector : Double[], + expansionOps : Int[][], + numQubits : Int + ) : Unit { + use qs = Qubit[numQubits]; + ApplyStatePreparation( + new StatePreparationParams { + rowMap = rowMap, + stateVector = stateVector, + expansionOps = expansionOps, + numQubits = numQubits + }, + qs + ); + } + + function MakeStatePreparationOp( + rowMap : Int[], + stateVector : Double[], + expansionOps : Int[][], + numQubits : Int + ) : Qubit[] => Unit is Adj + Ctl { + ApplyStatePreparation( + new StatePreparationParams { + rowMap = rowMap, + stateVector = stateVector, + expansionOps = expansionOps, + numQubits = numQubits + }, + _ + ) + } + + operation SelectIdentity(systems : Qubit[], ancilla : Qubit[]) : Unit is Adj + Ctl {} + + function MakeControlledPrepSelPrepOp( + prepareOp : Qubit[] => Unit is Adj + Ctl, + selectOp : (Qubit[], Qubit[]) => Unit is Adj + Ctl, + numSystemQubits : Int, + numAncillaQubits : Int, + power : Int + ) : (Qubit, Qubit[]) => Unit { + (control, allQubits) => { + let systems = allQubits[0..numSystemQubits - 1]; + let ancilla = allQubits[numSystemQubits...]; + for _ in 0..power - 1 { + Controlled prepareOp([control], systems); + Controlled selectOp([control], (systems, ancilla)); + } + } + } + + operation MakeControlledPrepSelPrepCircuit( + prepareOp : Qubit[] => Unit is Adj + Ctl, + selectOp : (Qubit[], Qubit[]) => Unit is Adj + Ctl, + numSystemQubits : Int, + numAncillaQubits : Int, + power : Int + ) : Unit { + use control = Qubit(); + use systems = Qubit[numSystemQubits + numAncillaQubits]; + let op = MakeControlledPrepSelPrepOp( + prepareOp, + selectOp, + numSystemQubits, + numAncillaQubits, + power + ); + op(control, systems); + } + """) + from qdk.code import ( + MakeControlledPrepSelPrepCircuit, + MakeStatePreparationCircuit, + SelectIdentity, + ) + + state_prep_args = ([0], [1.0, 0.0], [], 1) + + direct_qir = str(qsharp.compile(MakeStatePreparationCircuit, *state_prep_args)) + assert "define i64 @ENTRYPOINT__main()" in direct_qir + + prepare_op = qsharp.eval("MakeStatePreparationOp([0], [1.0, 0.0], [], 1)") + nested_qir = str( + qsharp.compile( + MakeControlledPrepSelPrepCircuit, + prepare_op, + SelectIdentity, + 1, + 1, + 1, + ) + ) + assert "define i64 @ENTRYPOINT__main()" in nested_qir + + +def test_chemistry_like_iqpe_params_struct_generates_qir() -> None: + # Python mirror of the Rust `chemistry_like_iqpe_params_struct_generates_qir` + # test in source/compiler/qsc/src/codegen/tests.rs. + qsharp.init(target_profile=qsharp.TargetProfile.Base) + qsharp.eval(""" + import Std.Arrays.Subarray; + + struct IterativePhaseEstimationParams { + statePrep : Qubit[] => Unit, + repControlledUnitary : (Qubit, Qubit[]) => Unit, + accumulatePhase : Double, + phaseQubit : Int, + systems : Int[], + numAncillaQubits : Int + } + + operation PrepareSystems(systems : Qubit[]) : Unit {} + + operation RepControlledUnitary(control : Qubit, targets : Qubit[]) : Unit {} + + operation RunIQPE(params : IterativePhaseEstimationParams) : Result[] { + use qs = Qubit[Length(params.systems) + 1 + params.numAncillaQubits]; + let phaseQubit = qs[params.phaseQubit]; + let systems = Subarray(params.systems, qs); + let ancillas = if params.numAncillaQubits == 0 { + [] + } else { + qs[1 + Length(params.systems)..Length(qs) - 1] + }; + let allTargets = systems + ancillas; + + params.statePrep(systems); + + within { + H(phaseQubit); + } apply { + Rz(params.accumulatePhase, phaseQubit); + params.repControlledUnitary(phaseQubit, allTargets); + } + ResetAll(allTargets); + return [MResetZ(phaseQubit)]; + } + + operation MakeIQPECircuit( + statePrep : Qubit[] => Unit, + repControlledUnitary : (Qubit, Qubit[]) => Unit, + accumulatePhase : Double, + phaseQubit : Int, + systems : Int[], + numAncillaQubits : Int + ) : Result[] { + return RunIQPE(new IterativePhaseEstimationParams { + statePrep = statePrep, + repControlledUnitary = repControlledUnitary, + accumulatePhase = accumulatePhase, + phaseQubit = phaseQubit, + systems = systems, + numAncillaQubits = numAncillaQubits + }); + } + """) + from qdk.code import MakeIQPECircuit, PrepareSystems, RepControlledUnitary + + qir = str( + qsharp.compile( + MakeIQPECircuit, + PrepareSystems, + RepControlledUnitary, + 0.25, + 0, + [1], + 1, + ) + ) + assert "define i64 @ENTRYPOINT__main()" in qir + + +def test_chemistry_like_sequential_partial_application_generates_qir() -> None: + # Python mirror of the Rust + # `chemistry_like_sequential_partial_application_generates_qir` + # test in source/compiler/qsc/src/codegen/tests.rs. + qsharp.init(target_profile=qsharp.TargetProfile.Base) + qsharp.eval(""" + import Std.Arrays.Subarray; + + operation ApplyFirstStep(systems : Qubit[]) : Unit { + for q in systems { + H(q); + } + } + + operation ApplySecondStep(systems : Qubit[]) : Unit { + for q in systems { + X(q); + } + } + + operation ApplyThirdStep(systems : Qubit[]) : Unit { + for q in systems { + Z(q); + } + } + + operation ApplySequential( + first : Qubit[] => Unit, + second : Qubit[] => Unit, + systems : Qubit[] + ) : Unit { + first(systems); + second(systems); + } + + function MakeSequentialOp( + first : Qubit[] => Unit, + second : Qubit[] => Unit + ) : Qubit[] => Unit { + ApplySequential(first, second, _) + } + + function MaxInt(values : Int[]) : Int { + mutable max = values[0]; + for idx in 1 .. Length(values) - 1 { + let value = values[idx]; + if value > max { + set max = value; + } + } + return max; + } + + operation MakeSequentialCircuit( + first : Qubit[] => Unit, + second : Qubit[] => Unit, + targets : Int[] + ) : Unit { + if Length(targets) == 0 { + return (); + } else { + let maxTarget = MaxInt(targets); + use qs = Qubit[1 + maxTarget]; + ApplySequential(first, second, Subarray(targets, qs)); + } + } + """) + from qdk.code import ApplyThirdStep, MakeSequentialCircuit + + sequential = qsharp.eval("MakeSequentialOp(ApplyFirstStep, ApplySecondStep)") + qir = str(qsharp.compile(MakeSequentialCircuit, sequential, ApplyThirdStep, [0, 1])) + assert "__quantum__qis__h__body" in qir + assert "__quantum__qis__x__body" in qir + assert "__quantum__qis__z__body" in qir + + @pytest.mark.xfail( raises=QSharpError, strict=True,