Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions source/compiler/qsc/src/codegen/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 31 additions & 0 deletions source/compiler/qsc_data_structures/src/intrinsic_names.rs
Original file line number Diff line number Diff line change
@@ -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",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without really understanding what this list refers to, it surprises me that DumpRegister and DumpOperation would be present, but DumpMachine would be absent.

"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)
}
1 change: 1 addition & 0 deletions source/compiler/qsc_data_structures/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions source/compiler/qsc_eval/src/intrinsic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ pub(crate) fn call<B: Backend>(
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()
Expand Down
23 changes: 5 additions & 18 deletions source/compiler/qsc_fir_transforms/src/arg_promote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -456,29 +456,17 @@ 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,
local_id: LocalVarId,
) -> Vec<ParamUse> {
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(),
}
}

Expand Down Expand Up @@ -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 {
Expand Down
73 changes: 7 additions & 66 deletions source/compiler/qsc_fir_transforms/src/arg_promote/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);

Expand All @@ -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()
Expand Down
7 changes: 3 additions & 4 deletions source/compiler/qsc_fir_transforms/src/cloner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
}

Expand Down
11 changes: 1 addition & 10 deletions source/compiler/qsc_fir_transforms/src/cond_normalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,19 +223,10 @@ fn collect_targets_in_callable_impl(
targets: &mut Vec<ConditionTarget>,
) {
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,
);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -2786,7 +2785,7 @@ fn collect_callable_param_types(
) -> FxHashMap<LocalVarId, Ty> {
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) => {
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
Loading
Loading