diff --git a/source/compiler/qsc/src/codegen/tests.rs b/source/compiler/qsc/src/codegen/tests.rs index 1e460ede1ea..2dfe94f55a8 100644 --- a/source/compiler/qsc/src/codegen/tests.rs +++ b/source/compiler/qsc/src/codegen/tests.rs @@ -86,6 +86,71 @@ fn compile_source_to_qir_result( ) } +#[test] +fn dump_operation_is_codegen_noop_across_restricted_profiles() { + let source = r#" + namespace Test { + @EntryPoint() + operation Main() : Unit { + Std.Diagnostics.DumpOperation(1, qs => H(qs[0])); + } + } + "#; + + for profile in [Profile::Base, Profile::AdaptiveRI, Profile::AdaptiveRIF] { + let qir = compile_source_to_qir(source, profile.into()); + assert!( + !qir.contains("DumpOperation"), + "expected DumpOperation to emit no call, declaration, or symbol for {profile:?}:\n{qir}" + ); + } +} + +#[test] +fn codegen_noop_intrinsic_preserves_effectful_arguments() { + let source = r#" + namespace Test { + operation CountAfterMeasurement(q : Qubit) : Int { + M(q); + 1 + } + + @EntryPoint() + operation Main() : Unit { + use q = Qubit(); + Std.Diagnostics.DumpOperation( + CountAfterMeasurement(q), + qs => H(qs[0]) + ); + } + } + "#; + + let qir = compile_source_to_qir(source, Profile::AdaptiveRI.into()); + assert!( + qir.contains("__quantum__qis__m__body"), + "expected the effectful DumpOperation argument to remain in generated QIR:\n{qir}" + ); +} + +#[test] +fn fact_is_codegen_noop_after_simulatable_intrinsic_collapse() { + let source = r#" + namespace Test { + @EntryPoint() + operation Main() : Unit { + Std.Diagnostics.Fact(false, "message"); + } + } + "#; + + let qir = compile_source_to_qir(source, Profile::Base.into()); + assert!( + !qir.contains("Fact"), + "expected Fact to emit no QIR symbol:\n{qir}" + ); +} + /// Compiles `lib_source` as a separate library package, then generates QIR for /// `user_source` with that library as a dependency. The library's namespaces are /// visible to the user program without an alias, so user code can reference them diff --git a/source/compiler/qsc_data_structures/src/intrinsic_names.rs b/source/compiler/qsc_data_structures/src/intrinsic_names.rs new file mode 100644 index 00000000000..d726389fee9 --- /dev/null +++ b/source/compiler/qsc_data_structures/src/intrinsic_names.rs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/// Intrinsic names that partial evaluation handles as code generation no-ops. +pub const CODEGEN_NOOP_INTRINSIC_NAMES: &[&str] = &[ + "DumpRegister", + "DumpOperation", + "AccountForEstimatesInternal", + "BeginRepeatEstimatesInternal", + "EndRepeatEstimatesInternal", + "EnableMemoryComputeArchitecture", + "Load", + "Store", + "ApplyIdleNoise", + "GlobalPhase", + "Message", + "PostSelectZ", + "Fact", +]; + +/// Returns whether the intrinsic is handled as a code generation no-op. +#[must_use] +pub fn is_codegen_noop_intrinsic(name: &str) -> bool { + CODEGEN_NOOP_INTRINSIC_NAMES.contains(&name) +} + +/// Returns whether downstream FIR consumers require the intrinsic's literal name. +#[must_use] +pub fn must_preserve_intrinsic_name(name: &str) -> bool { + name == "Length" || is_codegen_noop_intrinsic(name) +} diff --git a/source/compiler/qsc_data_structures/src/lib.rs b/source/compiler/qsc_data_structures/src/lib.rs index 033ee408c72..12adb25b588 100644 --- a/source/compiler/qsc_data_structures/src/lib.rs +++ b/source/compiler/qsc_data_structures/src/lib.rs @@ -5,6 +5,7 @@ pub mod attrs; pub mod display; pub mod error; pub mod functors; +pub mod intrinsic_names; pub mod language_features; pub mod line_column; pub mod namespaces; diff --git a/source/compiler/qsc_eval/src/intrinsic.rs b/source/compiler/qsc_eval/src/intrinsic.rs index 792549da2c3..151b3d2f2e8 100644 --- a/source/compiler/qsc_eval/src/intrinsic.rs +++ b/source/compiler/qsc_eval/src/intrinsic.rs @@ -119,6 +119,10 @@ pub(crate) fn call( Ok(()) => Ok(Value::unit()), Err(_) => Err(Error::OutputFail(name_span)), }, + // Codegen transforms collapse `Fact`'s simulatable body, so evaluating + // transformed FIR reaches intrinsic dispatch. Untransformed simulation + // still executes the Q# body and preserves its assertion behavior. + "Fact" => Ok(Value::unit()), "CheckZero" => Ok(Value::Bool( sim.qubit_is_zero( arg.unwrap_qubit() diff --git a/source/compiler/qsc_fir_transforms/src/arg_promote.rs b/source/compiler/qsc_fir_transforms/src/arg_promote.rs index e973aebc44a..8ec9c70e8ef 100644 --- a/source/compiler/qsc_fir_transforms/src/arg_promote.rs +++ b/source/compiler/qsc_fir_transforms/src/arg_promote.rs @@ -456,10 +456,9 @@ fn find_param_binds_in_pat( /// Classifies every use of `local_id` across all specialization bodies of the /// callable, returning the flat list of [`ParamUse`] classifications. /// -/// Only `CallableImpl::Spec` callables ever reach this function: the intrinsic -/// gate in `find_promotion_candidates` skips both `Intrinsic` and -/// `SimulatableIntrinsic` callables before any candidate is constructed, so the -/// non-`Spec` arms are unreachable. +/// Only `CallableImpl::Spec` callables normally reach this function: the +/// intrinsic gate in `find_promotion_candidates` skips bodyless callables +/// before any candidate is constructed. fn classify_param_uses( package: &Package, decl: &CallableDecl, @@ -467,18 +466,7 @@ fn classify_param_uses( ) -> Vec { match &decl.implementation { CallableImpl::Spec(spec_impl) => classify_uses_in_spec_impl(package, spec_impl, local_id), - // Dead arm: gated by the intrinsic skip in `find_promotion_candidates` - CallableImpl::Intrinsic => unreachable!( - "intrinsic callables are skipped by the intrinsic gate in \ - find_promotion_candidates before any candidate reaches \ - classify_param_uses" - ), - // Dead arm: same intrinsic gate as the `Intrinsic` arm above. - CallableImpl::SimulatableIntrinsic(_) => unreachable!( - "simulatable-intrinsic callables are skipped by the intrinsic gate in \ - find_promotion_candidates before any candidate reaches \ - classify_param_uses" - ), + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => Vec::new(), } } @@ -996,8 +984,7 @@ fn refresh_spec_input_types(package: &mut Package, item_id: LocalItemId) { .filter_map(|spec| spec.input) .chain(spec_impl.body.input) .collect(), - CallableImpl::SimulatableIntrinsic(spec) => spec.input.into_iter().collect(), - CallableImpl::Intrinsic => Vec::new(), + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => Vec::new(), } }; for pat_id in spec_input_pats { diff --git a/source/compiler/qsc_fir_transforms/src/arg_promote/tests.rs b/source/compiler/qsc_fir_transforms/src/arg_promote/tests.rs index c2e4e447c41..d9e93634b61 100644 --- a/source/compiler/qsc_fir_transforms/src/arg_promote/tests.rs +++ b/source/compiler/qsc_fir_transforms/src/arg_promote/tests.rs @@ -2241,43 +2241,6 @@ fn aggregate_argument_expression_is_bound_once_before_field_projection() { assert_call_shape_count(&store, pkg_id, "Main", "BuildPair(", 1); } -#[test] -fn simulatable_intrinsic_tuple_parameter_is_not_promoted() { - // A `@SimulatableIntrinsic` callable with a UDT parameter is skipped by - // arg_promote and keeps its signature. Like a regular `body intrinsic`, - // it has no FIR-usable body (it is codegen-only), so the full pipeline - // rejects such a signature in the intrinsic precheck before arg_promote - // runs. This drives arg_promote directly on the FIR to prove the pass - // itself leaves the signature untouched (and never hits the intrinsic - // gate's `unreachable!()` arms). - let source = "struct Pair { X : Int, Y : Int } - @SimulatableIntrinsic() - operation MeasurePair(p : Pair) : Int { - p.X + p.Y - } - @EntryPoint() - operation Main() : Int { - let pair = new Pair { X = 1, Y = 2 }; - MeasurePair(pair) - }"; - - let (mut store, pkg_id) = compile_to_fir(source); - - let mut assigners = PackageAssigners::new(&store, pkg_id); - arg_promote(&mut store, pkg_id, &mut assigners); - - let package = store.get(pkg_id); - // Signature unchanged: parameter stays a single whole binding. - assert_eq!( - callable_input_binding_names(package, "MeasurePair"), - vec!["p"] - ); - - // Call site keeps the whole argument (not flattened into projections). - let call_shapes = extract_call_shapes(&store, pkg_id, "Main"); - expect!["MeasurePair(pair)"].assert_eq(&call_shapes); -} - #[test] fn regular_intrinsic_tuple_parameter_is_not_promoted() { // A regular `body intrinsic` callable with a tuple parameter is skipped by @@ -2301,17 +2264,13 @@ fn regular_intrinsic_tuple_parameter_is_not_promoted() { #[test] fn intrinsic_nested_tuple_parameter_is_not_promoted() { // An intrinsic callable with a *nested* (depth >= 2) tuple parameter is - // skipped by arg_promote regardless of intrinsic flavor: both a - // `@SimulatableIntrinsic` and a regular `body intrinsic` keep their - // tuple-shaped signature, and their call sites keep the whole nested-tuple - // argument (never decomposed into multi-index leaf projections). This also - // guards the `unreachable!()` arms behind the intrinsic gate, proving the - // gate still excludes intrinsics upstream (no panic). - // - // Like a regular `body intrinsic`, a simulatable intrinsic has no - // FIR-usable body (codegen-only), so the full pipeline rejects these - // signatures in the intrinsic precheck before arg_promote runs. This drives - // arg_promote directly on the FIR to exercise the pass in isolation. + // skipped by arg_promote: a regular `body intrinsic` keeps its tuple-shaped + // signature, and its call site keeps the whole nested-tuple argument (never + // decomposed into multi-index leaf projections). This also guards the + // `unreachable!()` arms behind the intrinsic gate, proving the gate still + // excludes intrinsics upstream (no panic). The full pipeline rejects this + // signature in the intrinsic precheck before arg_promote runs, so this + // drives arg_promote directly on the FIR to exercise the pass in isolation. fn assert_nested_tuple_param_untouched(source: &str, callable: &str, expected_call: &str) { let (mut store, pkg_id) = compile_to_fir(source); @@ -2336,24 +2295,6 @@ fn intrinsic_nested_tuple_parameter_is_not_promoted() { ); } - // `@SimulatableIntrinsic` flavor: signature and call site both untouched. - assert_nested_tuple_param_untouched( - "@SimulatableIntrinsic() - operation MeasureNested(p : (Int, (Int, Int))) : Int { - let (a, (b, c)) = p; - a + b + c - } - @EntryPoint() - operation Main() : Int { - let nested = (1, (2, 3)); - MeasureNested(nested) - }", - "MeasureNested", - "MeasureNested(nested)", - ); - - // Regular `body intrinsic` flavor: same skip behavior on a literal nested - // tuple argument. assert_nested_tuple_param_untouched( "operation Foo(p : (Int, (Int, Int))) : Unit { body intrinsic; } @EntryPoint() diff --git a/source/compiler/qsc_fir_transforms/src/cloner.rs b/source/compiler/qsc_fir_transforms/src/cloner.rs index e64bc08066d..967979339b2 100644 --- a/source/compiler/qsc_fir_transforms/src/cloner.rs +++ b/source/compiler/qsc_fir_transforms/src/cloner.rs @@ -145,13 +145,12 @@ impl FirCloner { target: &mut Package, ) -> CallableImpl { match callable_impl { - CallableImpl::Intrinsic => CallableImpl::Intrinsic, + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => { + CallableImpl::Intrinsic + } CallableImpl::Spec(spec_impl) => { CallableImpl::Spec(self.clone_spec_impl(source, spec_impl, target)) } - CallableImpl::SimulatableIntrinsic(spec_decl) => { - CallableImpl::SimulatableIntrinsic(self.clone_spec_decl(source, spec_decl, target)) - } } } diff --git a/source/compiler/qsc_fir_transforms/src/cond_normalize.rs b/source/compiler/qsc_fir_transforms/src/cond_normalize.rs index 778a4b0a1e9..3ca75db6965 100644 --- a/source/compiler/qsc_fir_transforms/src/cond_normalize.rs +++ b/source/compiler/qsc_fir_transforms/src/cond_normalize.rs @@ -223,19 +223,10 @@ fn collect_targets_in_callable_impl( targets: &mut Vec, ) { match callable_impl { - CallableImpl::Intrinsic => {} + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => {} CallableImpl::Spec(spec_impl) => { collect_targets_in_spec_impl(package, package_id, spec_impl, targets); } - CallableImpl::SimulatableIntrinsic(spec_decl) => { - collect_targets_in_block( - package, - package_id, - spec_decl.block, - spec_decl.block, - targets, - ); - } } } diff --git a/source/compiler/qsc_fir_transforms/src/defunctionalize/analysis.rs b/source/compiler/qsc_fir_transforms/src/defunctionalize/analysis.rs index 2800cb763b8..dea6f0f0ca9 100644 --- a/source/compiler/qsc_fir_transforms/src/defunctionalize/analysis.rs +++ b/source/compiler/qsc_fir_transforms/src/defunctionalize/analysis.rs @@ -1513,10 +1513,9 @@ fn resolve_callable_return( spec_impl.body.block, spec_impl.body.input.unwrap_or(decl.input), ), - CallableImpl::SimulatableIntrinsic(spec_decl) => { - (spec_decl.block, spec_decl.input.unwrap_or(decl.input)) + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => { + return CalleeLattice::Dynamic; } - CallableImpl::Intrinsic => return CalleeLattice::Dynamic, }; let mut state = LocalState { @@ -2786,7 +2785,7 @@ fn collect_callable_param_types( ) -> FxHashMap { let mut map = FxHashMap::default(); match callable_impl { - CallableImpl::Intrinsic => { + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => { collect_binding_types_from_pat_into(pkg, fallback_input, &mut map); } CallableImpl::Spec(spec_impl) => { @@ -2803,13 +2802,6 @@ fn collect_callable_param_types( ); } } - CallableImpl::SimulatableIntrinsic(spec_decl) => { - collect_binding_types_from_pat_into( - pkg, - spec_decl.input.unwrap_or(fallback_input), - &mut map, - ); - } } map } @@ -2886,20 +2878,10 @@ fn build_callable_flow_state( closure_capturable_var_types: collect_callable_param_types(pkg, callable_impl, input_pat), }; match callable_impl { - CallableImpl::Intrinsic => {} + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => {} CallableImpl::Spec(spec_impl) => { analyze_spec_flow(pkg, store, spec_impl, &mut state, package_id, recorder); } - CallableImpl::SimulatableIntrinsic(spec_decl) => { - analyze_block_flow( - pkg, - store, - spec_decl.block, - &mut state, - package_id, - recorder, - ); - } } state } diff --git a/source/compiler/qsc_fir_transforms/src/defunctionalize/rewrite.rs b/source/compiler/qsc_fir_transforms/src/defunctionalize/rewrite.rs index 605b119c256..3070aa29b32 100644 --- a/source/compiler/qsc_fir_transforms/src/defunctionalize/rewrite.rs +++ b/source/compiler/qsc_fir_transforms/src/defunctionalize/rewrite.rs @@ -1281,8 +1281,7 @@ fn resolve_concrete_closure_captures( /// 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::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => {} CallableImpl::Spec(spec_impl) => { vis.visit_block(spec_impl.body.block); for spec in [ @@ -1561,16 +1560,13 @@ pub(crate) fn build_expr_block_lookup(package: &Package) -> FxHashMap {} + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => {} CallableImpl::Spec(spec_impl) => { record_block_context(package, spec_impl.body.block, &mut lookup); for spec in crate::fir_builder::functored_specs(spec_impl) { record_block_context(package, spec.block, &mut lookup); } } - CallableImpl::SimulatableIntrinsic(spec_decl) => { - record_block_context(package, spec_decl.block, &mut lookup); - } } } } @@ -1820,10 +1816,8 @@ fn remove_write_only_callable_local_from_callable( 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::Intrinsic + | qsc_fir::fir::CallableImpl::SimulatableIntrinsic(_) => {} 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] @@ -2037,10 +2031,8 @@ fn remove_dead_callable_local_from_callable( let implementation = decl.implementation.clone(); match implementation { - qsc_fir::fir::CallableImpl::Intrinsic => {} - qsc_fir::fir::CallableImpl::SimulatableIntrinsic(spec_decl) => { - remove_dead_callable_local_from_block(package, spec_decl.block, local_var); - } + qsc_fir::fir::CallableImpl::Intrinsic + | qsc_fir::fir::CallableImpl::SimulatableIntrinsic(_) => {} qsc_fir::fir::CallableImpl::Spec(spec_impl) => { remove_dead_callable_local_from_block(package, spec_impl.body.block, local_var); for spec in [spec_impl.adj, spec_impl.ctl, spec_impl.ctl_adj] @@ -2101,10 +2093,8 @@ fn prune_dead_top_level_callable_locals(package: &mut Package, package_id: Packa for (_item_id, implementation) in callable_items { match implementation { - qsc_fir::fir::CallableImpl::Intrinsic => {} - qsc_fir::fir::CallableImpl::SimulatableIntrinsic(spec_decl) => { - prune_dead_callable_locals_in_block(package, package_id, spec_decl.block); - } + qsc_fir::fir::CallableImpl::Intrinsic + | qsc_fir::fir::CallableImpl::SimulatableIntrinsic(_) => {} qsc_fir::fir::CallableImpl::Spec(spec_impl) => { prune_dead_callable_locals_in_block(package, package_id, spec_impl.body.block); for spec in [spec_impl.adj, spec_impl.ctl, spec_impl.ctl_adj] diff --git a/source/compiler/qsc_fir_transforms/src/defunctionalize/specialize.rs b/source/compiler/qsc_fir_transforms/src/defunctionalize/specialize.rs index 74046350ae1..a2ce4351138 100644 --- a/source/compiler/qsc_fir_transforms/src/defunctionalize/specialize.rs +++ b/source/compiler/qsc_fir_transforms/src/defunctionalize/specialize.rs @@ -522,16 +522,13 @@ fn report_excessive_specializations( /// callable implementation. fn refresh_rewritten_value_types(package: &mut Package, callable_impl: &CallableImpl) { match callable_impl { - CallableImpl::Intrinsic => {} + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => {} CallableImpl::Spec(spec_impl) => { refresh_block_types(package, spec_impl.body.block); for spec in functored_specs(spec_impl) { refresh_block_types(package, spec.block); } } - CallableImpl::SimulatableIntrinsic(spec) => { - refresh_block_types(package, spec.block); - } } } @@ -1953,7 +1950,7 @@ fn transform_callable_body( ) { let mut alias_set = AliasSet::default(); match callable_impl { - CallableImpl::Intrinsic => {} + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => {} CallableImpl::Spec(spec_impl) => { transform_block( package, @@ -2006,19 +2003,6 @@ fn transform_callable_body( ); } } - CallableImpl::SimulatableIntrinsic(spec_decl) => { - transform_block( - package, - package_id, - spec_decl.block, - param, - concrete, - concrete_group, - &mut alias_set, - specialized_capture_targets, - assigner, - ); - } } } @@ -4125,7 +4109,7 @@ fn collect_calls_to_closure_target( calls: FxHashSet::default(), }; match callable_impl { - CallableImpl::Intrinsic => {} + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => {} CallableImpl::Spec(spec_impl) => { collector.visit_block(spec_impl.body.block); if let Some(adj) = &spec_impl.adj { @@ -4138,9 +4122,6 @@ fn collect_calls_to_closure_target( collector.visit_block(ctl_adj.block); } } - CallableImpl::SimulatableIntrinsic(spec_decl) => { - collector.visit_block(spec_decl.block); - } } collector.calls } @@ -4210,7 +4191,7 @@ fn rewrite_closure_target_call_args( assigner: &mut Assigner, ) { match callable_impl { - CallableImpl::Intrinsic => {} + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => {} CallableImpl::Spec(spec_impl) => { rewrite_closure_target_call_args_in_block( package, @@ -4251,16 +4232,6 @@ fn rewrite_closure_target_call_args( ); } } - CallableImpl::SimulatableIntrinsic(spec_decl) => { - rewrite_closure_target_call_args_in_block( - package, - spec_decl.block, - package_id, - closure_target, - capture_bindings, - assigner, - ); - } } } @@ -4660,7 +4631,7 @@ fn prepend_capture_args_to_call( fn spec_block_ids(callable_impl: &CallableImpl) -> Vec { let mut ids = Vec::new(); match callable_impl { - CallableImpl::Intrinsic => {} + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => {} CallableImpl::Spec(spec_impl) => { ids.push(spec_impl.body.block); if let Some(ref adj) = spec_impl.adj { @@ -4673,7 +4644,6 @@ fn spec_block_ids(callable_impl: &CallableImpl) -> Vec { ids.push(ctl_adj.block); } } - CallableImpl::SimulatableIntrinsic(spec_decl) => ids.push(spec_decl.block), } ids } @@ -5040,13 +5010,6 @@ fn remove_nested_callable_param( inner_path, ); } - } else if let CallableImpl::SimulatableIntrinsic(spec_decl) = &decl.implementation { - rewrite_destructuring_pat_in_block( - package, - spec_decl.block, - param.param_var, - inner_path, - ); } } } @@ -5378,16 +5341,13 @@ fn extract_callable_body(source_pkg: &Package, decl: &CallableDecl) -> Package { extract_pat(source_pkg, decl.input, &mut body_pkg); match &decl.implementation { - CallableImpl::Intrinsic => {} + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => {} CallableImpl::Spec(spec_impl) => { extract_spec_decl_body(source_pkg, &spec_impl.body, &mut body_pkg); for spec in functored_specs(spec_impl) { extract_spec_decl_body(source_pkg, spec, &mut body_pkg); } } - CallableImpl::SimulatableIntrinsic(spec) => { - extract_spec_decl_body(source_pkg, spec, &mut body_pkg); - } } body_pkg @@ -5526,20 +5486,23 @@ fn extract_item(source: &Package, item_id: LocalItemId, target: &mut Package) { return; } let item = source.get_item(item_id); - target.items.insert(item_id, item.clone()); + let mut extracted_item = item.clone(); + if let ItemKind::Callable(decl) = &mut extracted_item.kind + && matches!(decl.implementation, CallableImpl::SimulatableIntrinsic(_)) + { + decl.implementation = CallableImpl::Intrinsic; + } + target.items.insert(item_id, extracted_item); if let ItemKind::Callable(decl) = &item.kind { extract_pat(source, decl.input, target); match &decl.implementation { - CallableImpl::Intrinsic => {} + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => {} CallableImpl::Spec(spec_impl) => { extract_spec_decl_body(source, &spec_impl.body, target); for spec in functored_specs(spec_impl) { extract_spec_decl_body(source, spec, target); } } - CallableImpl::SimulatableIntrinsic(spec) => { - extract_spec_decl_body(source, spec, target); - } } } } diff --git a/source/compiler/qsc_fir_transforms/src/exec_graph_rebuild.rs b/source/compiler/qsc_fir_transforms/src/exec_graph_rebuild.rs index 773f3f017d9..ba0a7f56589 100644 --- a/source/compiler/qsc_fir_transforms/src/exec_graph_rebuild.rs +++ b/source/compiler/qsc_fir_transforms/src/exec_graph_rebuild.rs @@ -158,7 +158,7 @@ fn collect_callable_specs( fn collect_specs_from_impl(implementation: &CallableImpl) -> Vec { let mut specs = Vec::new(); match implementation { - CallableImpl::Intrinsic => {} + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => {} CallableImpl::Spec(spec_impl) => { specs.push(SpecInfo { block: spec_impl.body.block, @@ -183,12 +183,6 @@ fn collect_specs_from_impl(implementation: &CallableImpl) -> Vec { }); } } - CallableImpl::SimulatableIntrinsic(spec) => { - specs.push(SpecInfo { - block: spec.block, - kind: CallableSpecKind::SimulatableIntrinsic, - }); - } } specs } @@ -250,10 +244,6 @@ fn get_spec_decl_mut( CallableImpl::Spec(si) => si.ctl_adj.as_mut().expect("ctl_adj must exist"), _ => unreachable!("already verified Spec"), }, - CallableSpecKind::SimulatableIntrinsic => match &mut decl.implementation { - CallableImpl::SimulatableIntrinsic(spec) => spec, - _ => unreachable!("already verified SimulatableIntrinsic"), - }, } } diff --git a/source/compiler/qsc_fir_transforms/src/exec_graph_rebuild/tests.rs b/source/compiler/qsc_fir_transforms/src/exec_graph_rebuild/tests.rs index de76ee7383a..69ca864fc5c 100644 --- a/source/compiler/qsc_fir_transforms/src/exec_graph_rebuild/tests.rs +++ b/source/compiler/qsc_fir_transforms/src/exec_graph_rebuild/tests.rs @@ -24,7 +24,6 @@ enum CallableSpecKind { Adj, Ctl, CtlAdj, - SimulatableIntrinsic, } /// Formats the body spec exec graph of the entry callable as a string for @@ -107,12 +106,7 @@ fn callable_local_names( } } } - CallableImpl::SimulatableIntrinsic(spec) => { - if let Some(input_pat) = spec.input { - collect_pat_names(package, input_pat, &mut names); - } - } - CallableImpl::Intrinsic => {} + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => {} } names @@ -190,7 +184,6 @@ fn format_callable_spec_exec_graph( .ctl_adj .as_ref() .expect("controlled adjoint spec should exist"), - (CallableSpecKind::SimulatableIntrinsic, CallableImpl::SimulatableIntrinsic(spec)) => spec, _ => panic!("requested spec kind is not present on '{callable_name}'"), }; @@ -244,8 +237,9 @@ fn format_store_callable_exec_graph( let local_names = callable_local_names(package, decl); let spec = match &decl.implementation { CallableImpl::Spec(spec_impl) => &spec_impl.body, - CallableImpl::SimulatableIntrinsic(spec) => spec, - CallableImpl::Intrinsic => panic!("callable '{}' should have a body", decl.name.name), + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => { + panic!("callable '{}' should have a body", decl.name.name) + } }; format_exec_graph_nodes( @@ -271,8 +265,9 @@ fn clear_store_callable_exec_graph( match &mut decl.implementation { CallableImpl::Spec(spec_impl) => spec_impl.body.exec_graph = Default::default(), - CallableImpl::SimulatableIntrinsic(spec) => spec.exec_graph = Default::default(), - CallableImpl::Intrinsic => panic!("callable '{}' should have a body", decl.name.name), + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => { + panic!("callable '{}' should have a body", decl.name.name) + } } } @@ -292,10 +287,9 @@ fn callable_body_exec_graph_len( .exec_graph .select_ref(ExecGraphConfig::NoDebug) .len(), - CallableImpl::SimulatableIntrinsic(spec) => { - spec.exec_graph.select_ref(ExecGraphConfig::NoDebug).len() + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => { + panic!("callable '{}' should have a body", decl.name.name) } - CallableImpl::Intrinsic => panic!("callable '{}' should have a body", decl.name.name), } } @@ -855,34 +849,6 @@ fn controlled_adjoint_spec_exec_graph_rebuilds_semantic_order() { ); } -#[test] -fn simulatable_intrinsic_spec_exec_graph_rebuilds_semantic_order() { - check_callable_spec_exec_graph( - "@SimulatableIntrinsic() - operation MyMeasurement(q : Qubit) : Result { - H(q); - M(q) - } - @EntryPoint() - operation Main() : Result { - use q = Qubit(); - MyMeasurement(q) - }", - "MyMeasurement", - CallableSpecKind::SimulatableIntrinsic, - &expect![[r#" - 0: H - 1: Store - 2: Var(q) - 3: Call - 4: M - 5: Store - 6: Var(q) - 7: Call - 8: Ret"#]], - ); -} - #[test] fn exec_graph_entry_expression_rebuilt_correctly() { check_exec_graph( diff --git a/source/compiler/qsc_fir_transforms/src/gc_unreachable.rs b/source/compiler/qsc_fir_transforms/src/gc_unreachable.rs index b2ca2269345..4139d156e76 100644 --- a/source/compiler/qsc_fir_transforms/src/gc_unreachable.rs +++ b/source/compiler/qsc_fir_transforms/src/gc_unreachable.rs @@ -1,7 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -//! FIR arena garbage collection — runs immediately after item-level DCE, as +//! FIR arena garbage collection. +//! +//! The transform pipeline runs this after simulatable-intrinsic collapse to +//! discard their orphaned override bodies, and again after item-level DCE as //! the last cleanup before exec graph rebuild. //! //! Tombstones blocks, stmts, exprs, and pats in a package's `IndexMap` arenas diff --git a/source/compiler/qsc_fir_transforms/src/intrinsic_precheck.rs b/source/compiler/qsc_fir_transforms/src/intrinsic_precheck.rs index ad6ab4dde62..a47837e020a 100644 --- a/source/compiler/qsc_fir_transforms/src/intrinsic_precheck.rs +++ b/source/compiler/qsc_fir_transforms/src/intrinsic_precheck.rs @@ -6,9 +6,7 @@ //! Rejects reachable intrinsic callables whose parameter or return types //! contain non-empty tuples or user-defined types, which cannot survive UDT //! erasure and tuple-decompose (an intrinsic has no body to rewrite). A failure -//! is fatal and short-circuits the pipeline with -//! [`Error::UnsupportedParamType`] / [`Error::UnsupportedReturnType`] before any -//! other pass runs. +//! is fatal and short-circuits the pipeline before any other pass runs. #[cfg(test)] mod tests; diff --git a/source/compiler/qsc_fir_transforms/src/invariants.rs b/source/compiler/qsc_fir_transforms/src/invariants.rs index 274881c6617..a3f9b5c2168 100644 --- a/source/compiler/qsc_fir_transforms/src/invariants.rs +++ b/source/compiler/qsc_fir_transforms/src/invariants.rs @@ -333,11 +333,7 @@ fn check_reachable_spec_exec_graphs(store: &PackageStore, reachable: &FxHashSet< } } } - CallableImpl::SimulatableIntrinsic(spec) => { - check_spec_exec_graph(package, spec, &format!("{name}/sim_intrinsic")); - check_spec_exec_graph_ranges(package, spec, &format!("{name}/sim_intrinsic")); - } - CallableImpl::Intrinsic => {} + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => {} } } } @@ -605,14 +601,7 @@ fn check_callable_non_unit_block_tails(package: &Package, decl: &CallableDecl) { } } } - CallableImpl::SimulatableIntrinsic(spec) => { - check_spec_block_tail( - package, - spec, - &format!("callable '{callable_name}' simulatable intrinsic"), - ); - } - CallableImpl::Intrinsic => {} + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => {} } crate::walk_utils::for_each_expr_in_callable_impl( @@ -1024,10 +1013,7 @@ fn check_reachable_invariants( check_spec_decl_types(store, item_pkg, spec, level); } } - CallableImpl::SimulatableIntrinsic(spec) => { - check_spec_decl_types(store, item_pkg, spec, level); - } - CallableImpl::Intrinsic => {} + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => {} } if enforces_stage(level, StageCheck::Mono) { @@ -1191,12 +1177,10 @@ fn collect_return_flag_locals(package: &Package) -> FxHashSet { .collect() } -/// Returns the root blocks of a callable's specializations (body plus any -/// functor specializations or simulatable-intrinsic body). +/// Returns the root blocks of a callable's body and functor specializations. fn callable_root_blocks(decl: &CallableDecl) -> Vec { match &decl.implementation { - CallableImpl::Intrinsic => Vec::new(), - CallableImpl::SimulatableIntrinsic(spec) => vec![spec.block], + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => Vec::new(), CallableImpl::Spec(spec_impl) => { let mut blocks = vec![spec_impl.body.block]; for spec in [&spec_impl.adj, &spec_impl.ctl, &spec_impl.ctl_adj] @@ -1495,16 +1479,7 @@ fn check_callable_input_pattern_shapes(package: &Package, decl: &CallableDecl) { } } } - CallableImpl::SimulatableIntrinsic(spec) => { - if let Some(pat_id) = spec.input { - check_tuple_pat_shape_matches_type( - package, - pat_id, - &format!("callable '{callable_name}' simulatable intrinsic input"), - ); - } - } - CallableImpl::Intrinsic => {} + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => {} } } @@ -2017,16 +1992,7 @@ fn check_local_var_consistency(package: &Package, decl: &CallableDecl) { } } } - CallableImpl::SimulatableIntrinsic(spec) => { - check_spec_local_var_consistency( - package, - decl, - "simulatable intrinsic", - spec, - &callable_scope, - ); - } - CallableImpl::Intrinsic => {} + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => {} } } @@ -2327,10 +2293,7 @@ fn check_expr_id_ownership( } v } - CallableImpl::SimulatableIntrinsic(spec) => { - vec![(spec, "sim")] - } - CallableImpl::Intrinsic => continue, + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => continue, }; let seen = seen_by_package.entry(item_id.package).or_default(); diff --git a/source/compiler/qsc_fir_transforms/src/invariants/test_utils.rs b/source/compiler/qsc_fir_transforms/src/invariants/test_utils.rs index dbf4403601b..d8d9b5570c9 100644 --- a/source/compiler/qsc_fir_transforms/src/invariants/test_utils.rs +++ b/source/compiler/qsc_fir_transforms/src/invariants/test_utils.rs @@ -343,24 +343,6 @@ pub(super) fn inject_stale_local_var( panic!("no Res::Local expression found to mutate"); } -pub(super) fn inject_stale_local_var_in_callable( - store: &mut PackageStore, - pkg_id: qsc_fir::fir::PackageId, - callable_name: &str, - bad_id: LocalVarId, -) { - let target_id = { - let pkg = store.get(pkg_id); - find_expr_in_named_callable(pkg, callable_name, |_, _, expr| { - matches!(expr.kind, ExprKind::Var(Res::Local(_), _)) - }) - }; - - let pkg = store.get_mut(pkg_id); - let expr = pkg.exprs.get_mut(target_id).expect("expr not found"); - expr.kind = ExprKind::Var(Res::Local(bad_id), vec![]); -} - pub(super) fn inject_udt_expr_type_in_callable( store: &mut PackageStore, pkg_id: qsc_fir::fir::PackageId, diff --git a/source/compiler/qsc_fir_transforms/src/invariants/tests.rs b/source/compiler/qsc_fir_transforms/src/invariants/tests.rs index 087d54d54de..f03d8836041 100644 --- a/source/compiler/qsc_fir_transforms/src/invariants/tests.rs +++ b/source/compiler/qsc_fir_transforms/src/invariants/tests.rs @@ -14,8 +14,8 @@ use crate::invariants::test_utils::{ inject_nested_tuple_bound_arrow_local, inject_nested_tuple_eq_in_if_branch, inject_non_copy_struct, inject_non_tuple_field_path_target, inject_non_unit_assignment_expression_type, inject_stale_local_var, - inject_stale_local_var_in_callable, inject_tuple_arity_mismatch, inject_ty_param, - inject_udt_callable_output, inject_udt_expr_type, inject_udt_expr_type_in_callable, + inject_tuple_arity_mismatch, inject_ty_param, inject_udt_callable_output, inject_udt_expr_type, + inject_udt_expr_type_in_callable, }; use crate::test_utils::{ PipelineStage, assert_panics_with, compile_and_run_pipeline_to, @@ -138,22 +138,6 @@ const NESTED_TUPLE_LITERAL_INSIDE_IF: &str = r#" } "#; -const SIMULATABLE_INTRINSIC_BODY: &str = r#" - namespace Test { - @SimulatableIntrinsic() - operation MyMeasurement(q : Qubit) : Result { - let r = M(q); - r - } - - @EntryPoint() - operation Main() : Result { - use q = Qubit(); - MyMeasurement(q) - } - } -"#; - #[test] fn invariant_passes_with_valid_local_var() { let (store, pkg_id) = compile_and_run_pipeline_to(SIMPLE_LOCAL_VAR, PipelineStage::Mono); @@ -747,31 +731,6 @@ fn post_arg_promote_catches_functor_wrapper_stale_item_signature() { }); } -#[test] -fn post_mono_catches_stale_local_in_simulatable_intrinsic_body() { - let (mut store, pkg_id) = - compile_and_run_pipeline_to(SIMULATABLE_INTRINSIC_BODY, PipelineStage::Mono); - inject_stale_local_var_in_callable( - &mut store, - pkg_id, - "MyMeasurement", - LocalVarId::from(9999u32), - ); - assert_panics_with("LocalVarId consistency", || { - check(&store, pkg_id, InvariantLevel::PostMono); - }); -} - -#[test] -fn post_all_catches_simulatable_intrinsic_body_type_violation() { - let (mut store, pkg_id) = - compile_and_run_pipeline_to(SIMULATABLE_INTRINSIC_BODY, PipelineStage::Full); - inject_udt_expr_type_in_callable(&mut store, pkg_id, "MyMeasurement"); - assert_panics_with("contains Ty::Udt after UDT erasure", || { - check(&store, pkg_id, InvariantLevel::PostAll); - }); -} - #[test] fn post_all_field_path_on_non_tuple_panics() { let (mut store, pkg_id) = compile_and_run_pipeline_to(STRUCT_FIELD_ACCESS, PipelineStage::Full); diff --git a/source/compiler/qsc_fir_transforms/src/item_dce/tests.rs b/source/compiler/qsc_fir_transforms/src/item_dce/tests.rs index 746cfae954d..776bda74228 100644 --- a/source/compiler/qsc_fir_transforms/src/item_dce/tests.rs +++ b/source/compiler/qsc_fir_transforms/src/item_dce/tests.rs @@ -557,8 +557,8 @@ mod item_dce_contracts { if let ItemKind::Callable(callable) = &package.get_item(local_item_id).kind { let spec = match &callable.implementation { qsc_fir::fir::CallableImpl::Spec(spec_impl) => &spec_impl.body, - qsc_fir::fir::CallableImpl::SimulatableIntrinsic(spec) => spec, - qsc_fir::fir::CallableImpl::Intrinsic => continue, + qsc_fir::fir::CallableImpl::Intrinsic + | qsc_fir::fir::CallableImpl::SimulatableIntrinsic(_) => continue, }; // Collect all statements in the callable body block diff --git a/source/compiler/qsc_fir_transforms/src/lib.rs b/source/compiler/qsc_fir_transforms/src/lib.rs index 2a89b512fc2..85ba9b7a56f 100644 --- a/source/compiler/qsc_fir_transforms/src/lib.rs +++ b/source/compiler/qsc_fir_transforms/src/lib.rs @@ -92,11 +92,18 @@ pub mod test_utils; pub(crate) mod walk_utils; use miette::Diagnostic; -use qsc_fir::fir::{ExecGraphIdx, ItemKind, PackageId, PackageStore, StoreItemId}; +use qsc_fir::{ + fir::{ + CallableImpl, ExecGraphIdx, ExprKind, Global, ItemKind, PackageId, PackageLookup, + PackageStore, PackageStoreLookup, Res, StoreItemId, + }, + ty::Ty, +}; use rustc_hash::FxHashSet; use thiserror::Error; use crate::package_assigners::PackageAssigners; +pub use qsc_data_structures::intrinsic_names::is_codegen_noop_intrinsic; /// Kinds of callable specializations that carry execution graphs. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -109,8 +116,6 @@ pub(crate) enum CallableSpecKind { Ctl, /// The controlled-adjoint specialization. CtlAdj, - /// A simulatable intrinsic with an explicit body block. - SimulatableIntrinsic, } /// An empty execution graph range for synthesized FIR nodes that do not @@ -288,9 +293,16 @@ fn run_pipeline_to_impl( return result; } - let mut result = PipelineResult::default(); - let mut assigners = PackageAssigners::new(store, package_id); + assigners.seed_all(store); + collapse_simulatable_intrinsics(store); + let package_ids = store.iter().map(|(id, _)| id).collect::>(); + for package_id in package_ids { + let _ = gc_unreachable::gc_unreachable(store.get_mut(package_id)); + } + assert_no_simulatable_intrinsics(store); + + let mut result = PipelineResult::default(); monomorphize::monomorphize(store, package_id, &mut assigners); invariants::check(store, package_id, invariants::InvariantLevel::PostMono); @@ -298,6 +310,8 @@ fn run_pipeline_to_impl( return result; } + lower_codegen_noop_intrinsic_calls(store, &mut assigners); + let (ru_errors, skipped) = return_unify::unify_returns(store, package_id, &mut assigners); let (ru_warnings, ru_fatal): (Vec<_>, Vec<_>) = ru_errors .into_iter() @@ -485,6 +499,149 @@ fn run_arg_promote_stages( matches!(stage, PipelineStage::TupleDecompose2) } +fn collapse_simulatable_intrinsics(store: &mut PackageStore) { + let package_ids = store + .iter() + .map(|(package_id, _)| package_id) + .collect::>(); + for package_id in package_ids { + for item in store.get_mut(package_id).items.values_mut() { + let ItemKind::Callable(decl) = &mut item.kind else { + continue; + }; + if matches!(decl.implementation, CallableImpl::SimulatableIntrinsic(_)) { + decl.implementation = CallableImpl::Intrinsic; + } + } + } +} + +/// Removes callable-valued arguments from code generation no-op intrinsic calls +/// before defunctionalization. +/// +/// Partial evaluation normally recognizes these intrinsics by literal name and +/// emits no call for them. Defunctionalization runs earlier, however, and would +/// reject a callable argument such as the operation passed to `DumpOperation` +/// before partial evaluation can apply that policy. This rewrite replaces only +/// direct calls to known no-op intrinsics that contain an arrow-typed argument. +/// Calls without callable values remain intact for partial evaluation. +/// +/// Removing the no-op call must not remove observable argument evaluation. +/// Arguments proven total and side-effect-free are discarded; all others are +/// evaluated once, in source order, in a synthesized block that returns unit. +/// If a retained argument still has arrow type, the call is left unchanged +/// rather than unsafely discarding or evaluating a callable value. +fn lower_codegen_noop_intrinsic_calls(store: &mut PackageStore, assigners: &mut PackageAssigners) { + // Discover rewrites under an immutable store borrow. Applying them in a + // second phase permits fresh FIR allocation from each owning package's + // assigner without invalidating the arena iteration. + let mut calls = Vec::new(); + for (package_id, package) in store.iter() { + for (expr_id, expr) in &package.exprs { + let ExprKind::Call(callee_id, args_id) = expr.kind else { + continue; + }; + // Restrict the rewrite to direct item calls so intrinsic identity + // is statically known. Dynamic or locally-bound callees are left to + // the normal defunctionalization diagnostics. + let ExprKind::Var(Res::Item(item_id), _) = package.get_expr(callee_id).kind else { + continue; + }; + let Some(Global::Callable(decl)) = store.get_global(StoreItemId { + package: item_id.package, + item: item_id.item, + }) else { + continue; + }; + if matches!(decl.implementation, CallableImpl::Intrinsic) + && is_codegen_noop_intrinsic(&decl.name.name) + { + // FIR represents a multi-parameter argument as a tuple and a + // single parameter directly. Normalize both shapes to source- + // ordered top-level arguments for filtering and sequencing. + let args = match &package.get_expr(args_id).kind { + ExprKind::Tuple(args) => args.clone(), + _ => vec![args_id], + }; + // Ordinary no-op calls need no early rewrite. Intervene only + // when an argument contains callable type residue that would + // otherwise prevent defunctionalization from converging. + if !args.iter().any(|arg_id| { + crate::defunctionalize::ty_contains_arrow(&package.get_expr(*arg_id).ty) + }) { + continue; + } + // Safe arguments, including closure construction with no + // observable evaluation, can disappear with the no-op call. + // Retain every fallible or effectful argument for sequencing. + let retained_args = args + .into_iter() + .filter(|arg_id| { + !crate::walk_utils::expr_is_safe_to_discard(package, package_id, *arg_id) + }) + .collect::>(); + // Fail closed if purity analysis could not discard every + // callable value. Synthesizing a statement that evaluates an + // arrow value would preserve the residue this pass must remove. + if retained_args + .iter() + .any(|arg_id| matches!(package.get_expr(*arg_id).ty, Ty::Arrow(_))) + { + continue; + } + calls.push((package_id, expr_id, expr.span, retained_args)); + } + } + } + + for (package_id, expr_id, span, retained_args) in calls { + let assigner = assigners.get_mut(store, package_id); + let package = store.get_mut(package_id); + let kind = if retained_args.is_empty() { + ExprKind::Tuple(Vec::new()) + } else { + // A block of semicolon statements preserves left-to-right argument + // evaluation while discarding each value. The trailing unit gives + // the replacement the original no-op call's result type. + let mut statements = retained_args + .into_iter() + .map(|arg_id| { + let arg_span = package.get_expr(arg_id).span; + crate::fir_builder::alloc_semi_stmt(package, assigner, arg_id, arg_span) + }) + .collect::>(); + let unit = crate::fir_builder::alloc_unit_expr(package, assigner, span); + statements.push(crate::fir_builder::alloc_expr_stmt( + package, assigner, unit, span, + )); + let block = + crate::fir_builder::alloc_block(package, assigner, statements, Ty::UNIT, span); + ExprKind::Block(block) + }; + let expr = package + .exprs + .get_mut(expr_id) + .expect("codegen no-op call should exist"); + expr.kind = kind; + expr.ty = Ty::UNIT; + } +} + +fn assert_no_simulatable_intrinsics(store: &PackageStore) { + assert!( + store + .iter() + .all(|(_, package)| package.items.values().all(|item| { + !matches!( + &item.kind, + ItemKind::Callable(decl) + if matches!(decl.implementation, CallableImpl::SimulatableIntrinsic(_)) + ) + })), + "FIR transform pipeline requires simulatable intrinsics to be collapsed" + ); +} + /// Runs the backend stages after all structural transforms: pinned-item /// validation, item dead-code elimination, execution-graph rebuild, and the /// final `PostAll` invariant walk. @@ -824,12 +981,15 @@ pub fn run_pipeline_with_diagnostics( /// # Panics /// /// Panics if the package has no entry expression (the codegen path guarantees -/// one exists after the main pipeline runs). +/// one exists after the main pipeline runs), or if a simulatable intrinsic has +/// not been collapsed by the main pipeline. pub fn run_signature_preserving_subpipeline( store: &mut PackageStore, package_id: PackageId, seeds: &[StoreItemId], ) -> PipelineResult { + assert_no_simulatable_intrinsics(store); + let mut result = PipelineResult::default(); let mut assigners = PackageAssigners::new(store, package_id); diff --git a/source/compiler/qsc_fir_transforms/src/monomorphize.rs b/source/compiler/qsc_fir_transforms/src/monomorphize.rs index 4ea7829fe57..ea505ee05ce 100644 --- a/source/compiler/qsc_fir_transforms/src/monomorphize.rs +++ b/source/compiler/qsc_fir_transforms/src/monomorphize.rs @@ -48,6 +48,8 @@ use qsc_fir::fir::{ LocalItemId, LocalVarId, Package, PackageId, PackageLookup, PackageStore, PatId, PatKind, Res, StmtId, StmtKind, StoreItemId, Visibility, }; + +pub use qsc_data_structures::intrinsic_names::must_preserve_intrinsic_name; use qsc_fir::ty::{Arrow, FunctorSet, GenericArg, ParamId, Ty, TypeParameter}; use rustc_hash::{FxHashMap, FxHashSet}; use std::collections::VecDeque; @@ -385,11 +387,13 @@ fn mono_key(source: StoreItemId, args: &[GenericArg]) -> String { /// concrete generic arguments to the base name using `` notation. /// /// Functor set arguments use compact identifiers (`Empty`, `Adj`, `Ctl`, -/// `AdjCtl`) instead of the user-facing display forms. The intrinsic `Length` -/// is exempt because downstream passes match on that name literally. +/// `AdjCtl`) instead of the user-facing display forms. A bounded set of +/// intrinsic names is exempt because downstream FIR consumers recognize them +/// by literal name. fn mono_name(decl: &CallableDecl, args: &[GenericArg]) -> Rc { use std::fmt::Write; - if matches!(decl.implementation, CallableImpl::Intrinsic) && decl.name.name.as_ref() == "Length" + if matches!(decl.implementation, CallableImpl::Intrinsic) + && must_preserve_intrinsic_name(&decl.name.name) { return Rc::clone(&decl.name.name); } @@ -1065,16 +1069,13 @@ fn extract_callable_body(source_pkg: &Package, decl: &CallableDecl) -> Package { extract_pat(source_pkg, decl.input, &mut body_pkg); match &decl.implementation { - CallableImpl::Intrinsic => {} + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => {} CallableImpl::Spec(spec_impl) => { extract_spec_decl_body(source_pkg, &spec_impl.body, &mut body_pkg); for spec in functored_specs(spec_impl) { extract_spec_decl_body(source_pkg, spec, &mut body_pkg); } } - CallableImpl::SimulatableIntrinsic(spec) => { - extract_spec_decl_body(source_pkg, spec, &mut body_pkg); - } } body_pkg @@ -1208,22 +1209,25 @@ fn extract_item(source: &Package, item_id: LocalItemId, target: &mut Package) { return; } let item = source.get_item(item_id); - target.items.insert(item_id, item.clone()); + let mut extracted_item = item.clone(); + if let ItemKind::Callable(decl) = &mut extracted_item.kind + && matches!(decl.implementation, CallableImpl::SimulatableIntrinsic(_)) + { + decl.implementation = CallableImpl::Intrinsic; + } + target.items.insert(item_id, extracted_item); if let ItemKind::Callable(decl) = &item.kind { // Extract all nodes transitively referenced by this callable into // the target body package. extract_pat(source, decl.input, target); match &decl.implementation { - CallableImpl::Intrinsic => {} + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => {} CallableImpl::Spec(spec_impl) => { extract_spec_decl_body(source, &spec_impl.body, target); for spec in functored_specs(spec_impl) { extract_spec_decl_body(source, spec, target); } } - CallableImpl::SimulatableIntrinsic(spec) => { - extract_spec_decl_body(source, spec, target); - } } } } diff --git a/source/compiler/qsc_fir_transforms/src/monomorphize/tests.rs b/source/compiler/qsc_fir_transforms/src/monomorphize/tests.rs index 2baac066430..47b5640dd60 100644 --- a/source/compiler/qsc_fir_transforms/src/monomorphize/tests.rs +++ b/source/compiler/qsc_fir_transforms/src/monomorphize/tests.rs @@ -60,6 +60,32 @@ fn entry_callee_name_and_generic_arg_count(package: &qsc_fir::fir::Package) -> ( (decl.name.name.to_string(), generic_args.len()) } +fn reachable_callables_with_prefix( + store: &qsc_fir::fir::PackageStore, + pkg_id: qsc_fir::fir::PackageId, + prefix: &str, +) -> Vec<(String, bool)> { + let reachable = crate::reachability::collect_reachable_from_entry(store, pkg_id); + let mut callables = reachable + .iter() + .filter_map(|store_item_id| { + let package = store.get(store_item_id.package); + let item = package.items.get(store_item_id.item)?; + let ItemKind::Callable(decl) = &item.kind else { + return None; + }; + decl.name.name.starts_with(prefix).then(|| { + ( + decl.name.name.to_string(), + matches!(decl.implementation, CallableImpl::Intrinsic), + ) + }) + }) + .collect::>(); + callables.sort(); + callables +} + /// Compiles Q# source, runs monomorphization, and asserts no /// `ExprKind::Var` in the user package still carries generic args. fn assert_no_generic_args(source: &str) { @@ -1056,6 +1082,43 @@ fn mono_cross_package_length() { ); } +#[test] +fn mono_generic_dump_operation_preserves_literal_name_after_pipeline_collapse() { + let source = indoc! {r#" + operation Main() : Unit { + use qs = Qubit[1]; + Std.Diagnostics.DumpOperation(1, qs => H(qs[0])); + } + "#}; + let (store, pkg_id) = crate::test_utils::compile_and_run_pipeline_to( + source, + crate::test_utils::PipelineStage::Mono, + ); + + assert_eq!( + reachable_callables_with_prefix(&store, pkg_id, "DumpOperation"), + vec![("DumpOperation".to_string(), true)] + ); +} + +// This is a marker in case we ever need to add tests for intrinsic generics that should not be mangled. +#[test] +fn mono_unrelated_generic_intrinsic_still_mangles_name() { + let source = indoc! {r#" + operation UnrelatedIntrinsic<'T>(value : 'T) : Unit { body intrinsic; } + operation Main() : Unit { UnrelatedIntrinsic(42); } + "#}; + let (store, pkg_id) = crate::test_utils::compile_and_run_pipeline_to( + source, + crate::test_utils::PipelineStage::Mono, + ); + + assert_eq!( + reachable_callables_with_prefix(&store, pkg_id, "UnrelatedIntrinsic"), + vec![("UnrelatedIntrinsic".to_string(), true)] + ); +} + #[test] fn mono_cross_package_reversed() { // Reversed is a cross-package generic callable. @@ -1337,9 +1400,8 @@ fn mono_recursive_generic() { } #[test] -fn mono_generic_with_simulatable_intrinsic() { - // A generic function used via a simulatable intrinsic path. - // Length is a cross-package intrinsic: verify it's specialized. +fn mono_generic_with_cross_package_intrinsic() { + // A generic function that calls the cross-package Length intrinsic. let source = indoc! {r#" operation Wrap<'T>(arr : 'T[]) : Int { Length(arr) } operation Main() : Int { @@ -2033,41 +2095,6 @@ fn monomorphize_no_entry_panics() { }); } -#[test] -fn mono_preserves_simulatable_intrinsic_impl() { - // A generic @SimulatableIntrinsic callable should, after monomorphization, - // produce a specialization that retains the SimulatableIntrinsic variant. - let (mut store, pkg_id) = crate::test_utils::compile_to_fir(indoc! {r#" - @SimulatableIntrinsic() - operation MySimIntrinsic<'T>(x : 'T) : 'T { x } - operation Main() : Int { MySimIntrinsic(42) } - "#}); - let mut assigners = PackageAssigners::new(&store, pkg_id); - monomorphize(&mut store, pkg_id, &mut assigners); - - let package = store.get(pkg_id); - let mut found_specialized = false; - for (_, item) in &package.items { - if let ItemKind::Callable(decl) = &item.kind - && decl.name.name.as_ref() == "MySimIntrinsic" - { - assert!( - matches!(decl.implementation, CallableImpl::SimulatableIntrinsic(_)), - "specialized callable should preserve SimulatableIntrinsic variant" - ); - assert!( - decl.generics.is_empty(), - "specialized callable should have no generic params" - ); - found_specialized = true; - } - } - assert!( - found_specialized, - "should find a specialized MySimIntrinsic callable" - ); -} - #[test] fn mono_generic_with_type_class_constraint() { // A generic with a class constraint (`'T: Add`) must specialize correctly: diff --git a/source/compiler/qsc_fir_transforms/src/package_assigners.rs b/source/compiler/qsc_fir_transforms/src/package_assigners.rs index 276a10b619a..027d2495f6d 100644 --- a/source/compiler/qsc_fir_transforms/src/package_assigners.rs +++ b/source/compiler/qsc_fir_transforms/src/package_assigners.rs @@ -68,6 +68,15 @@ impl PackageAssigners { Self { map } } + /// Seeds assigners for every package from its current ID watermarks. + pub(crate) fn seed_all(&mut self, store: &PackageStore) { + for (package_id, package) in store { + self.map + .entry(package_id) + .or_insert_with(|| Assigner::from_package(package)); + } + } + /// Returns a mutable reference to the assigner for `package_id`, lazily /// seeding it from the package's current id watermark when absent. /// diff --git a/source/compiler/qsc_fir_transforms/src/package_assigners/tests.rs b/source/compiler/qsc_fir_transforms/src/package_assigners/tests.rs index 4e717ea1c0a..0d391caf7e8 100644 --- a/source/compiler/qsc_fir_transforms/src/package_assigners/tests.rs +++ b/source/compiler/qsc_fir_transforms/src/package_assigners/tests.rs @@ -10,9 +10,12 @@ //! that package's own watermark. use super::PackageAssigners; -use crate::test_utils::compile_to_fir; +use crate::{ + collapse_simulatable_intrinsics, gc_unreachable, + test_utils::{compile_to_fir, compile_to_fir_with_library}, +}; use qsc_fir::assigner::Assigner; -use qsc_fir::fir::{PackageId, PackageStore}; +use qsc_fir::fir::{CallableImpl, ItemKind, PackageId, PackageStore}; const SOURCE: &str = " operation Helper(x : Int) : Int { x + 1 } @@ -110,3 +113,74 @@ fn entry_seeded_assigner_minting_into_foreign_package_collides() { demonstrating the overwrite hazard the per-package pool prevents" ); } + +#[test] +fn eager_seed_preserves_all_foreign_package_watermarks_after_gc() { + let library_source = r#" + namespace TestLib { + @SimulatableIntrinsic() + operation ForeignSimulatableIntrinsic(value : (Int, Int)) : Unit { + let (left, right) = value; + mutable total = left + right; + set total += 1; + } + } + "#; + let user_source = r#" + namespace Test { + @EntryPoint() + operation Main() : Unit {} + } + "#; + let (mut store, entry_package_id) = compile_to_fir_with_library(library_source, user_source); + let foreign_package_id = store + .iter() + .find_map(|(package_id, package)| { + package + .items + .values() + .any(|item| { + matches!( + &item.kind, + ItemKind::Callable(decl) + if decl.name.name.as_ref() == "ForeignSimulatableIntrinsic" + && matches!( + decl.implementation, + CallableImpl::SimulatableIntrinsic(_) + ) + ) + }) + .then_some(package_id) + }) + .expect("foreign simulatable intrinsic should exist"); + + let mut expected = Assigner::from_package(store.get(foreign_package_id)); + let expected_block = expected.next_block(); + let expected_expr = expected.next_expr(); + let expected_pat = expected.next_pat(); + let expected_stmt = expected.next_stmt(); + let expected_local = expected.next_local(); + let expected_item = expected.next_item(); + + let mut pool = PackageAssigners::new(&store, entry_package_id); + pool.seed_all(&store); + collapse_simulatable_intrinsics(&mut store); + let removed = gc_unreachable::gc_unreachable(store.get_mut(foreign_package_id)); + assert!(removed > 0, "simulation body nodes should be collected"); + + let mut post_gc = Assigner::from_package(store.get(foreign_package_id)); + assert_ne!(post_gc.next_block(), expected_block); + assert_ne!(post_gc.next_expr(), expected_expr); + assert_ne!(post_gc.next_pat(), expected_pat); + assert_ne!(post_gc.next_stmt(), expected_stmt); + assert_ne!(post_gc.next_local(), expected_local); + assert_eq!(post_gc.next_item(), expected_item); + + let seeded = pool.get_mut(&store, foreign_package_id); + assert_eq!(seeded.next_block(), expected_block); + assert_eq!(seeded.next_expr(), expected_expr); + assert_eq!(seeded.next_pat(), expected_pat); + assert_eq!(seeded.next_stmt(), expected_stmt); + assert_eq!(seeded.next_local(), expected_local); + assert_eq!(seeded.next_item(), expected_item); +} diff --git a/source/compiler/qsc_fir_transforms/src/reachability.rs b/source/compiler/qsc_fir_transforms/src/reachability.rs index 20966b8968c..e7214ff35c8 100644 --- a/source/compiler/qsc_fir_transforms/src/reachability.rs +++ b/source/compiler/qsc_fir_transforms/src/reachability.rs @@ -164,9 +164,8 @@ fn walk_callable_impl( // **not** descend its simulation body. As a result, items referenced *only* // from a simulation body are not kept reachable and are pruned by item DCE, // rather than being monomorphized, type-erased, and processed as if they were - // part of the generated program. The simulation body itself is left intact in - // the FIR; partial evaluation still classically evaluates it through the - // separate `qsc_eval` interpreter path when a call is purely classical. + // part of the generated program. Reachability treats the simulation body as + // opaque. return; } let pkg = store.get(pkg_id); diff --git a/source/compiler/qsc_fir_transforms/src/return_unify.rs b/source/compiler/qsc_fir_transforms/src/return_unify.rs index 9833fe48ee6..344b29d5541 100644 --- a/source/compiler/qsc_fir_transforms/src/return_unify.rs +++ b/source/compiler/qsc_fir_transforms/src/return_unify.rs @@ -647,7 +647,7 @@ fn process_callable_returns( /// Intrinsics have no explicit body block, so the result is empty. fn get_callable_body_blocks(callable: &CallableDecl) -> Vec { match &callable.implementation { - CallableImpl::Intrinsic => Vec::new(), + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => Vec::new(), CallableImpl::Spec(spec_impl) => { let mut blocks = vec![spec_impl.body.block]; for spec in functored_specs(spec_impl) { @@ -655,7 +655,6 @@ fn get_callable_body_blocks(callable: &CallableDecl) -> Vec { } blocks } - CallableImpl::SimulatableIntrinsic(spec) => vec![spec.block], } } diff --git a/source/compiler/qsc_fir_transforms/src/return_unify/tests.rs b/source/compiler/qsc_fir_transforms/src/return_unify/tests.rs index 34dd1757750..aa1deae04d6 100644 --- a/source/compiler/qsc_fir_transforms/src/return_unify/tests.rs +++ b/source/compiler/qsc_fir_transforms/src/return_unify/tests.rs @@ -374,7 +374,9 @@ fn summarize_callable(package: &Package, callable_name: &str) -> String { )]; match &decl.implementation { - CallableImpl::Intrinsic => lines.push(" intrinsic".to_string()), + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => { + lines.push(" intrinsic".to_string()); + } CallableImpl::Spec(spec_impl) => { push_spec_summary(package, "body", &spec_impl.body, &mut lines); for (label, spec) in [ @@ -387,9 +389,6 @@ fn summarize_callable(package: &Package, callable_name: &str) -> String { } } } - CallableImpl::SimulatableIntrinsic(spec) => { - push_spec_summary(package, "simulatable", spec, &mut lines); - } } lines.join("\n") diff --git a/source/compiler/qsc_fir_transforms/src/return_unify/tests/flag_strategy.rs b/source/compiler/qsc_fir_transforms/src/return_unify/tests/flag_strategy.rs index a15e8c0f1e8..4496ee01048 100644 --- a/source/compiler/qsc_fir_transforms/src/return_unify/tests/flag_strategy.rs +++ b/source/compiler/qsc_fir_transforms/src/return_unify/tests/flag_strategy.rs @@ -1111,10 +1111,7 @@ fn lowered_reachable_callables_do_not_emit_while_local_initializers() { block_ids.push(spec.block); } } - CallableImpl::SimulatableIntrinsic(spec) => { - block_ids.push(spec.block); - } - CallableImpl::Intrinsic => {} + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => {} } for_each_expr_in_callable_impl(package, &decl.implementation, &mut |_expr_id, expr| { diff --git a/source/compiler/qsc_fir_transforms/src/return_unify/tests/general.rs b/source/compiler/qsc_fir_transforms/src/return_unify/tests/general.rs index fe0e6e2e4c6..093ed42a472 100644 --- a/source/compiler/qsc_fir_transforms/src/return_unify/tests/general.rs +++ b/source/compiler/qsc_fir_transforms/src/return_unify/tests/general.rs @@ -367,44 +367,6 @@ callable Main: input_ty=Unit, output_ty=Unit ); } -#[test] -fn simulatable_intrinsic_body_is_return_unified() { - check_structure( - indoc! {r#" - namespace Test { - @SimulatableIntrinsic() - operation Foo() : Int { - mutable i = 0; - while i < 3 { - if i == 1 { - return i; - } - i += 1; - } - -1 - } - - @EntryPoint() - operation Main() : Int { - Foo() - } - } - "#}, - &["Foo", "Main"], - &expect![[r#" - callable Foo: input_ty=Unit, output_ty=Int - simulatable: block_ty=Int - [0] Local(Mutable, _.has_returned: Bool): Lit(Bool(false)) - [1] Local(Mutable, _.ret_val: Int): Lit(Int(0)) - [2] Local(Mutable, i: Int): Lit(Int(0)) - [3] Expr While[ty=Unit] - [4] Expr If(cond=Var[ty=Bool], then=Var[ty=Int], else=Block[ty=Int]) - callable Main: input_ty=Unit, output_ty=Int - body: block_ty=Int - [0] Expr Call[ty=Int]"#]], - ); -} - #[test] fn already_normalized_idempotency() { // Running on already-normalized code (no returns) produces no changes. diff --git a/source/compiler/qsc_fir_transforms/src/return_unify/tests/semantic.rs b/source/compiler/qsc_fir_transforms/src/return_unify/tests/semantic.rs index 8585ed66d95..f985ec238db 100644 --- a/source/compiler/qsc_fir_transforms/src/return_unify/tests/semantic.rs +++ b/source/compiler/qsc_fir_transforms/src/return_unify/tests/semantic.rs @@ -703,7 +703,6 @@ fn flag_lowering_guards_local_after_return_semantic() { // // Specialization tests (Adj/Ctl, no single entry point output): // - explicit_specialization_bodies_are_return_unified -// - simulatable_intrinsic_body_is_return_unified // - all_four_specializations_with_return_in_loop // // No-return or identity tests (no transform to validate): 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 8b30a18aaf9..73663613d2e 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 @@ -139,12 +139,7 @@ fn grover_sample_full_pipeline_reachable_items() { body intrinsic; } function Fact(actual : Bool, message : String) : Unit { - body ... { - if (not actual) { - fail message; - } - - } + body intrinsic; } operation CH(control : Qubit, target : Qubit) : Unit is Adj { body ... { 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 2fed62a2172..ac57555f5aa 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 @@ -236,12 +236,7 @@ fn shor_sample_full_pipeline_reachable_items() { body intrinsic; } function Fact(actual : Bool, message : String) : Unit { - body ... { - if (not actual) { - fail message; - } - - } + body intrinsic; } operation CH(control : Qubit, target : Qubit) : Unit is Adj { body ... { diff --git a/source/compiler/qsc_fir_transforms/src/signature_preserving_tests.rs b/source/compiler/qsc_fir_transforms/src/signature_preserving_tests.rs index 8cc4360ccb4..293051100bf 100644 --- a/source/compiler/qsc_fir_transforms/src/signature_preserving_tests.rs +++ b/source/compiler/qsc_fir_transforms/src/signature_preserving_tests.rs @@ -103,6 +103,27 @@ fn prepare_pinned(source: &str, target: &str) -> (PackageStore, PackageId, Store (store, pkg_id, pinned_store_id) } +#[test] +fn signature_preserving_subpipeline_rejects_uncollapsed_store() { + let (mut store, pkg_id) = compile_to_fir( + r#" + namespace Test { + @SimulatableIntrinsic() + operation Override() : Unit {} + @EntryPoint() + operation Main() : Unit {} + } + "#, + ); + + assert_panics_with( + "FIR transform pipeline requires simulatable intrinsics to be collapsed", + || { + let _ = run_signature_preserving_subpipeline(&mut store, pkg_id, &[]); + }, + ); +} + #[test] fn subpipeline_rewrites_pinned_early_dynamic_return() { let (mut store, pkg_id, pinned) = prepare_pinned(PINNED_ARROW_EARLY_RETURN, "Pinned"); diff --git a/source/compiler/qsc_fir_transforms/src/test_utils.rs b/source/compiler/qsc_fir_transforms/src/test_utils.rs index 16deaf5a716..a47ffc32444 100644 --- a/source/compiler/qsc_fir_transforms/src/test_utils.rs +++ b/source/compiler/qsc_fir_transforms/src/test_utils.rs @@ -32,7 +32,6 @@ use std::cell::RefCell; use qsc_lowerer::map_hir_package_to_fir; pub(crate) use crate::PipelineStage; -use crate::package_assigners::PackageAssigners; fn format_errors(errors: &[T]) -> String { errors @@ -433,29 +432,35 @@ pub(crate) fn compile_and_run_pipeline_to_with_two_libraries( (store, pkg_id) } -/// Compiles Q# source through core+std → HIR passes → FIR lowering → -/// monomorphization. +/// Compiles Q# source through core+std → HIR passes → FIR lowering, then runs +/// the canonical FIR pipeline through monomorphization. /// /// Returns a monomorphized FIR store ready for defunctionalization or later -/// pipeline stages. Uses default (empty) target capabilities. +/// pipeline stages. Uses default (empty) target capabilities and asserts that +/// the pipeline produced no errors. #[must_use] pub fn compile_to_monomorphized_fir(source: &str) -> (fir::PackageStore, fir::PackageId) { compile_to_monomorphized_fir_with_capabilities(source, TargetCapabilityFlags::empty()) } -/// Compiles Q# source through core+std → HIR passes → FIR lowering → -/// monomorphization using the given target capabilities. +/// Compiles Q# source through core+std → HIR passes → FIR lowering, then runs +/// the canonical FIR pipeline through monomorphization using the given target +/// capabilities. /// /// Returns a monomorphized FIR store ready for defunctionalization or later -/// pipeline stages. +/// pipeline stages and asserts that the pipeline produced no errors. #[must_use] pub fn compile_to_monomorphized_fir_with_capabilities( source: &str, capabilities: TargetCapabilityFlags, ) -> (fir::PackageStore, fir::PackageId) { let (mut store, pkg_id) = compile_to_fir_with_capabilities(source, capabilities); - let mut assigners = PackageAssigners::new(&store, pkg_id); - crate::monomorphize::monomorphize(&mut store, pkg_id, &mut assigners); + let result = + crate::run_pipeline_to_with_diagnostics(&mut store, pkg_id, PipelineStage::Mono, &[]); + assert_no_pipeline_errors( + "compile_to_monomorphized_fir_with_capabilities", + &result.errors, + ); (store, pkg_id) } @@ -469,17 +474,20 @@ pub fn compile_to_fir_with_entry(source: &str, entry: &str) -> (fir::PackageStor } /// Compiles Q# source with an explicit executable entry expression through -/// core+std → HIR passes → FIR lowering → monomorphization. +/// core+std → HIR passes → FIR lowering, then runs the canonical FIR pipeline +/// through monomorphization. /// -/// Returns a monomorphized FIR store ready for later pipeline stages. +/// Returns a monomorphized FIR store ready for later pipeline stages and +/// asserts that the pipeline produced no errors. #[cfg(test)] pub(crate) fn compile_to_monomorphized_fir_with_entry( source: &str, entry: &str, ) -> (fir::PackageStore, fir::PackageId) { let (mut store, pkg_id) = compile_to_fir_with_entry(source, entry); - let mut assigners = PackageAssigners::new(&store, pkg_id); - crate::monomorphize::monomorphize(&mut store, pkg_id, &mut assigners); + let result = + crate::run_pipeline_to_with_diagnostics(&mut store, pkg_id, PipelineStage::Mono, &[]); + assert_no_pipeline_errors("compile_to_monomorphized_fir_with_entry", &result.errors); (store, pkg_id) } @@ -665,9 +673,8 @@ pub(crate) fn extract_reachable_callable_details( )]; match &decl.implementation { - CallableImpl::Intrinsic => lines.push(" intrinsic".to_string()), - CallableImpl::SimulatableIntrinsic(spec) => { - push_spec_decl_summary(package, pkg_id, "simulatable", spec, &mut lines); + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => { + lines.push(" intrinsic".to_string()); } CallableImpl::Spec(spec_impl) => { push_spec_decl_summary(package, pkg_id, "body", &spec_impl.body, &mut lines); @@ -738,8 +745,9 @@ pub fn assert_callable_body_terminal_expr_matches_block_type( }; let spec = match &decl.implementation { CallableImpl::Spec(spec_impl) => &spec_impl.body, - CallableImpl::SimulatableIntrinsic(spec) => spec, - CallableImpl::Intrinsic => panic!("callable '{callable_name}' should have a body"), + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => { + panic!("callable '{callable_name}' should have a body") + } }; let block = package.get_block(spec.block); @@ -886,9 +894,9 @@ pub(crate) fn callable_id_by_name(package: &Package, callable_name: &str) -> Loc .unwrap_or_else(|| panic!("callable {callable_name} should exist")) } -/// Finds the body [`BlockId`] of a callable by name. Accepts `Spec` and -/// `SimulatableIntrinsic` implementations and skips `Intrinsic` ones (which -/// have no body block). Panics if no matching callable with a body is found. +/// Finds the body [`BlockId`] of a callable by name. Accepts `Spec` +/// implementations and skips bodyless intrinsic implementations. Panics if no +/// matching callable with a body is found. #[cfg(test)] pub(crate) fn find_callable_body_block(package: &Package, callable_name: &str) -> BlockId { for item in package.items.values() { @@ -897,8 +905,7 @@ pub(crate) fn find_callable_body_block(package: &Package, callable_name: &str) - { return match &decl.implementation { CallableImpl::Spec(spec_impl) => spec_impl.body.block, - CallableImpl::SimulatableIntrinsic(spec) => spec.block, - CallableImpl::Intrinsic => continue, + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => continue, }; } } @@ -909,8 +916,9 @@ pub(crate) fn find_callable_body_block(package: &Package, callable_name: &str) - fn callable_body_spec<'a>(decl: &'a CallableDecl, callable_name: &str) -> &'a SpecDecl { match &decl.implementation { CallableImpl::Spec(spec_impl) => &spec_impl.body, - CallableImpl::SimulatableIntrinsic(spec) => spec, - CallableImpl::Intrinsic => panic!("callable '{callable_name}' should have a body"), + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => { + panic!("callable '{callable_name}' should have a body") + } } } diff --git a/source/compiler/qsc_fir_transforms/src/tuple_decompose.rs b/source/compiler/qsc_fir_transforms/src/tuple_decompose.rs index b95d0598f2f..c7330e1a61c 100644 --- a/source/compiler/qsc_fir_transforms/src/tuple_decompose.rs +++ b/source/compiler/qsc_fir_transforms/src/tuple_decompose.rs @@ -638,10 +638,8 @@ fn build_stmt_block_map_for_callable( /// Collects block IDs reachable from a callable's implementation. /// /// For a `Spec` implementation this includes each specialization's root -/// block plus every block nested within expressions. `Intrinsic` and -/// `SimulatableIntrinsic` implementations contribute no spec-level root -/// block; any blocks nested within a `SimulatableIntrinsic` body are still -/// picked up by the expression walk. +/// block plus every block nested within expressions. Intrinsic implementations +/// contribute no spec-level root blocks. pub(crate) fn collect_all_block_ids_in_callable( package: &Package, item_id: LocalItemId, diff --git a/source/compiler/qsc_fir_transforms/src/udt_erase/tests.rs b/source/compiler/qsc_fir_transforms/src/udt_erase/tests.rs index b6c75bbaa11..298ca22f54d 100644 --- a/source/compiler/qsc_fir_transforms/src/udt_erase/tests.rs +++ b/source/compiler/qsc_fir_transforms/src/udt_erase/tests.rs @@ -2069,68 +2069,3 @@ fn cross_package_udt_copy_update_semantic_equivalence() { check_semantic_equivalence_with_library(lib_source, user_source); } - -/// Verifies that a `@SimulatableIntrinsic()` operation in a library package -/// whose signature takes and returns a struct defined in that library has its -/// UDT types correctly erased. The simulatable intrinsic body is preserved -/// for simulation and must be rewritten just like a normal spec body. -#[test] -fn cross_package_simulatable_intrinsic_with_struct_param_and_return() { - use crate::test_utils::compile_to_fir_with_library; - - let lib_source = indoc! {" - namespace TestLib { - struct Pair { Fst: Int, Snd: Int } - - @SimulatableIntrinsic() - operation TransformPair(p: Pair) : Pair { - new Pair { Fst = p.Snd, Snd = p.Fst } - } - - export Pair, TransformPair; - } - "}; - let user_source = indoc! {" - import TestLib.*; - - @EntryPoint() - operation Main() : (Int, Int) { - let p = new Pair { Fst = 1, Snd = 2 }; - let swapped = TransformPair(p); - (swapped.Fst, swapped.Snd) - } - "}; - - let (mut store, pkg_id) = compile_to_fir_with_library(lib_source, user_source); - let mut assigners = crate::package_assigners::PackageAssigners::new(&store, pkg_id); - erase_udts(&mut store, pkg_id, &mut assigners); - - // Run post-UDT-erase invariants to confirm no Ty::Udt survives in - // reachable packages and no Field::Path on non-tuple types remains. - crate::invariants::check( - &store, - pkg_id, - crate::invariants::InvariantLevel::PostUdtErase, - ); - - // Verify the library callable's SimulatableIntrinsic body is non-empty - // (erasure must rewrite the body, not discard it). - let reachable = crate::reachability::collect_reachable_from_entry(&store, pkg_id); - for store_id in &reachable { - if store_id.package == pkg_id { - continue; - } - let ext_package = store.get(store_id.package); - let item = ext_package.get_item(store_id.item); - if let ItemKind::Callable(decl) = &item.kind - && let CallableImpl::SimulatableIntrinsic(spec) = &decl.implementation - { - let block = ext_package.get_block(spec.block); - assert!( - !block.stmts.is_empty(), - "SimulatableIntrinsic callable '{}' body should have non-empty stmts after UDT erasure", - decl.name.name - ); - } - } -} diff --git a/source/compiler/qsc_fir_transforms/src/walk_utils.rs b/source/compiler/qsc_fir_transforms/src/walk_utils.rs index ffb0c36d496..c590888886f 100644 --- a/source/compiler/qsc_fir_transforms/src/walk_utils.rs +++ b/source/compiler/qsc_fir_transforms/src/walk_utils.rs @@ -113,13 +113,10 @@ where F: FnMut(ExprId, &Expr), { match callable_impl { - CallableImpl::Intrinsic => {} + CallableImpl::Intrinsic | CallableImpl::SimulatableIntrinsic(_) => {} CallableImpl::Spec(spec_impl) => { for_each_expr_in_spec_impl(pkg, spec_impl, visit); } - CallableImpl::SimulatableIntrinsic(spec_decl) => { - for_each_expr_in_spec_decl(pkg, spec_decl, visit); - } } } @@ -265,8 +262,7 @@ pub enum CallableNode { /// Coverage is complete for the callable's reachable tree: /// - **Patterns.** The callable input ([`CallableDecl::input`]), each present /// specialization input ([`SpecDecl::input`], including the control-register -/// inputs carried by the `ctl` / `ctl_adj` specs and the single -/// [`CallableImpl::SimulatableIntrinsic`] spec), and every +/// inputs carried by the `ctl` / `ctl_adj` specs), and every /// [`StmtKind::Local`] binding — each walked recursively through /// [`PatKind::Tuple`] elements. /// - **Blocks / statements / expressions.** Every specialization body block, diff --git a/source/compiler/qsc_fir_transforms/tests/pipeline_integration.rs b/source/compiler/qsc_fir_transforms/tests/pipeline_integration.rs index ca8e8c53eb5..9c96533d900 100644 --- a/source/compiler/qsc_fir_transforms/tests/pipeline_integration.rs +++ b/source/compiler/qsc_fir_transforms/tests/pipeline_integration.rs @@ -7,7 +7,7 @@ use qsc_eval::val::Value; use qsc_fir::{ - fir::{ExecGraphConfig, ExprKind, ItemKind, PackageLookup, StoreItemId}, + fir::{CallableImpl, ExecGraphConfig, ExprKind, ItemKind, PackageLookup, StoreItemId}, validate::validate, visit::Visitor, }; @@ -969,6 +969,73 @@ fn multiple_generic_instantiations_each_specialized() { invariants::check(&fir_store, fir_pkg_id, invariants::InvariantLevel::PostAll); } +#[test] +fn pipeline_collapses_simulatable_intrinsics_before_monomorphization() { + let lib_source = r#" + namespace TestLib { + @SimulatableIntrinsic() + operation ForeignSimulatableIntrinsic(value : Int) : Unit {} + } + "#; + let user_source = r#" + namespace Test { + @EntryPoint() + operation Main() : Unit {} + } + "#; + let (mut store, pkg_id) = compile_to_fir_with_library(lib_source, user_source); + let (foreign_pkg_id, simulation_body_block) = store + .iter() + .find_map(|(package_id, package)| { + package.items.values().find_map(|item| { + let ItemKind::Callable(decl) = &item.kind else { + return None; + }; + let CallableImpl::SimulatableIntrinsic(spec) = &decl.implementation else { + return None; + }; + (decl.name.name.as_ref() == "ForeignSimulatableIntrinsic") + .then_some((package_id, spec.block)) + }) + }) + .expect("foreign simulatable intrinsic should exist before the pipeline"); + assert_ne!(foreign_pkg_id, pkg_id); + + run_pipeline_to_successfully(&mut store, pkg_id, PipelineStage::Mono); + + for (package_id, package) in &store { + for item in package.items.values() { + let ItemKind::Callable(decl) = &item.kind else { + continue; + }; + assert!( + !matches!(decl.implementation, CallableImpl::SimulatableIntrinsic(_)), + "package {package_id} still contains simulatable intrinsic `{}` at Mono", + decl.name.name + ); + } + } + assert!( + store.get(foreign_pkg_id).items.values().any(|item| { + matches!( + &item.kind, + ItemKind::Callable(decl) + if decl.name.name.as_ref() == "ForeignSimulatableIntrinsic" + && matches!(decl.implementation, CallableImpl::Intrinsic) + ) + }), + "foreign simulatable intrinsic should be collapsed to Intrinsic" + ); + assert!( + store + .get(foreign_pkg_id) + .blocks + .get(simulation_body_block) + .is_none(), + "foreign simulation body block should be collected before Mono" + ); +} + #[test] fn cross_package_nested_generics_fully_resolved() { // Uses Std.Arrays.Mapped (generic) which internally calls other std diff --git a/source/compiler/qsc_frontend/src/lower.rs b/source/compiler/qsc_frontend/src/lower.rs index dcfd26636b8..897e4b1616e 100644 --- a/source/compiler/qsc_frontend/src/lower.rs +++ b/source/compiler/qsc_frontend/src/lower.rs @@ -17,6 +17,7 @@ use miette::Diagnostic; use qsc_ast::ast::{self, FieldAccess, Ident, Idents, PathKind}; use qsc_data_structures::{ index_map::IndexMap, + intrinsic_names::is_codegen_noop_intrinsic, span::Span, target::{Profile, TargetCapabilityFlags}, }; @@ -52,6 +53,22 @@ pub(super) enum Error { #[diagnostic(help("try declaring the callable as an operation"))] #[diagnostic(code("Qdk.Qsc.LowerAst.InvalidAttrOnFunction"))] InvalidAttrOnFunction(String, #[label] Span), + #[error( + "the SimulatableIntrinsic attribute cannot be applied to a callable with explicit generic type parameters" + )] + #[diagnostic(help( + "remove the SimulatableIntrinsic attribute or the explicit generic type parameters" + ))] + #[diagnostic(code("Qdk.Qsc.LowerAst.InvalidSimulatableIntrinsicOnGenericCallable"))] + InvalidSimulatableIntrinsicOnGenericCallable(#[label("explicit generic type parameter")] Span), + #[error( + "the SimulatableIntrinsic attribute cannot be applied to a callable with an arrow-typed input parameter" + )] + #[diagnostic(help( + "remove the SimulatableIntrinsic attribute or replace the arrow-typed input parameter" + ))] + #[diagnostic(code("Qdk.Qsc.LowerAst.InvalidSimulatableIntrinsicArrowParam"))] + InvalidSimulatableIntrinsicArrowParam(#[label("arrow-typed input parameter")] Span), #[error("missing callable body")] #[diagnostic(code("Qdk.Qsc.LowerAst.MissingBody"))] MissingBody(#[label] Span), @@ -506,6 +523,20 @@ impl With<'_> { let kind = self.lower_callable_kind(decl.kind, attrs, decl.name.span); let name = self.lower_ident(&decl.name); let mut input = self.lower_pat(&decl.input); + + if attrs.contains(&hir::Attr::SimulatableIntrinsic) { + if let Some(generic) = decl.generics.first() { + self.lowerer + .errors + .push(Error::InvalidSimulatableIntrinsicOnGenericCallable( + generic.span, + )); + } + if !is_codegen_noop_intrinsic(&name.name) { + Self::validate_simulatable_intrinsic_input(&input, &mut self.lowerer.errors); + } + } + let output = convert::ty_from_ast(self.names, &decl.output, &mut Default::default()).0; let (generics, errs) = self.synthesize_callable_generics(&decl.generics, &mut input); let functors = convert::ast_callable_functors(decl); @@ -555,6 +586,21 @@ impl With<'_> { ) } + fn validate_simulatable_intrinsic_input(pat: &hir::Pat, errors: &mut Vec) { + match &pat.kind { + hir::PatKind::Tuple(items) => { + for item in items { + Self::validate_simulatable_intrinsic_input(item, errors); + } + } + hir::PatKind::Bind(_) | hir::PatKind::Discard | hir::PatKind::Err => { + if matches!(pat.ty, Ty::Arrow(_)) { + errors.push(Error::InvalidSimulatableIntrinsicArrowParam(pat.span)); + } + } + } + } + fn check_invalid_attrs_on_function(&mut self, attrs: &[hir::Attr], span: Span) { const INVALID_ATTRS: [hir::Attr; 3] = [ hir::Attr::Measurement, diff --git a/source/compiler/qsc_frontend/src/lower/tests.rs b/source/compiler/qsc_frontend/src/lower/tests.rs index 1d31df6983b..acb9c5f9487 100644 --- a/source/compiler/qsc_frontend/src/lower/tests.rs +++ b/source/compiler/qsc_frontend/src/lower/tests.rs @@ -2368,6 +2368,109 @@ fn test_reset_attr_on_function_issues_error() { ); } +#[test] +fn test_simulatable_intrinsic_attr_on_generic_operation_issues_error() { + check_errors( + indoc! {r#" + namespace Test { + @SimulatableIntrinsic() + operation Foo<'T>(value : 'T) : Unit {} + } + "#}, + &expect![[r#" + [ + InvalidSimulatableIntrinsicOnGenericCallable( + Span { + lo: 63, + hi: 65, + }, + ), + ] + "#]], + ); +} + +#[test] +fn test_simulatable_intrinsic_attr_on_generic_function_issues_error() { + check_errors( + indoc! {r#" + namespace Test { + @SimulatableIntrinsic() + function Foo<'T>(value : 'T) : Unit {} + } + "#}, + &expect![[r#" + [ + InvalidSimulatableIntrinsicOnGenericCallable( + Span { + lo: 62, + hi: 64, + }, + ), + ] + "#]], + ); +} + +#[test] +fn test_simulatable_intrinsic_attr_on_arrow_param_issues_error() { + check_errors( + indoc! {r#" + namespace Test { + @SimulatableIntrinsic() + operation Foo(op : Qubit => Unit) : Unit {} + } + "#}, + &expect![[r#" + [ + InvalidSimulatableIntrinsicArrowParam( + Span { + lo: 63, + hi: 81, + }, + ), + ] + "#]], + ); +} + +#[test] +fn test_simulatable_intrinsic_attr_on_nested_arrow_param_issues_one_error() { + check_errors( + indoc! {r#" + namespace Test { + @SimulatableIntrinsic() + operation Foo(value : Int, (op : Qubit => Unit, flag : Bool)) : Unit {} + } + "#}, + &expect![[r#" + [ + InvalidSimulatableIntrinsicArrowParam( + Span { + lo: 77, + hi: 95, + }, + ), + ] + "#]], + ); +} + +#[test] +fn test_simulatable_intrinsic_attr_on_dump_operation_arrow_param_is_allowed() { + check_errors( + indoc! {r#" + namespace Test { + @SimulatableIntrinsic() + operation DumpOperation(count : Int, op : Qubit[] => Unit) : Unit {} + } + "#}, + &expect![[r#" + [] + "#]], + ); +} + #[test] fn item_docs() { check_hir( diff --git a/source/compiler/qsc_partial_eval/src/lib.rs b/source/compiler/qsc_partial_eval/src/lib.rs index d262441cb4f..e86099e71a5 100644 --- a/source/compiler/qsc_partial_eval/src/lib.rs +++ b/source/compiler/qsc_partial_eval/src/lib.rs @@ -39,6 +39,8 @@ use qsc_fir::{ }, ty::{FunctorSetValue, Prim, Ty}, }; + +pub use qsc_data_structures::intrinsic_names::is_codegen_noop_intrinsic; use qsc_lowerer::map_fir_package_to_hir; use qsc_rca::{ ComputeKind, ComputePropertiesLookup, ItemComputeProperties, PackageStoreComputeProperties, @@ -1914,19 +1916,7 @@ impl<'a> PartialEvaluator<'a> { "IsResourceEstimating" => Ok(Value::Bool(false)), // The following intrinsic operations and functions are no-ops. "BeginEstimateCaching" => Ok(Value::Bool(true)), - "DumpRegister" - | "DumpOperation" - | "AccountForEstimatesInternal" - | "BeginRepeatEstimatesInternal" - | "EndRepeatEstimatesInternal" - | "EnableMemoryComputeArchitecture" - | "Load" - | "Store" - | "ApplyIdleNoise" - | "GlobalPhase" - | "Message" - | "PostSelectZ" - | "Fact" => Ok(Value::unit()), + name if is_codegen_noop_intrinsic(name) => Ok(Value::unit()), "CheckZero" => Err(Error::UnsupportedSimulationIntrinsic( "CheckZero".to_string(), callee_expr_span,