Skip to content

Add initial support for simple IR functions in QIR#3344

Open
idavis wants to merge 6 commits into
mainfrom
iadavis/simple-ir-functions
Open

Add initial support for simple IR functions in QIR#3344
idavis wants to merge 6 commits into
mainfrom
iadavis/simple-ir-functions

Conversation

@idavis

@idavis idavis commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Add support for simple IR functions in QIR

What

Adds support for emitting eligible user-package Q# callables as IR functions instead of inlining them in QIR codegen. This lays the groundwork for extending IR function support to non-use packages in follow-up PRs.

Why

Currently, all Q# callables are inlined into the entry point. By emitting simple eligible callables as IR functions, we:

  • Reduce code duplication when callables are invoked multiple times
  • Establish infrastructure for more sophisticated IR function handling in future work
  • Enable better code generation and optimization in QIR targets

How

Partial Evaluator Changes (qsc_partial_eval)

  • Identifies eligible callables: non-composite specializations returning Unit or scalars (Int/Double/Bool)
  • Emits these as Regular RIR callables with bodies instead of inlining
  • Adds comprehensive tests in new ir_functions.rs test module

RIR Builder & Codegen (qsc_rir, qsc_codegen)

  • Extended qsc_rir::builder to support RIR function definitions with proper block management
  • Added qir::name module for correct LLVM global symbol name formatting (handling special characters and quoting)
  • Updated QIR v2 code generation to handle IR function calls and definitions
  • Added tests covering function call patterns and dominator graph updates

Supporting Changes

  • Updated resource counting analyzer (qsc_rca) to handle IR function bodies
  • Updated circuit generation, capabilities checker, and RIR passes for IR function compatibility
  • Added test utilities for multi-body RIR programs

Notes

  • This PR handles user-package callables only. Support for non-use packages is deferred to follow-up PRs as specified.
  • SSA Multi-Body is valuable because it makes SSA code correct for programs with more than one callable body, instead of relying on a fragile single-body assumption; that improves reliability today and avoids refactoring later as more IR-function/capability paths move through SSA.

@idavis idavis marked this pull request as draft June 16, 2026 19:01
@idavis idavis force-pushed the iadavis/simple-ir-functions branch 2 times, most recently from 7135d28 to 0f8556f Compare June 17, 2026 17:11
@idavis idavis force-pushed the iadavis/simple-ir-functions branch from 4045608 to a3f04f1 Compare June 17, 2026 22:27
@idavis idavis self-assigned this Jun 17, 2026
@idavis idavis marked this pull request as ready for review June 17, 2026 22:29
/// capability that gates IR-function emission.
#[must_use]
pub fn get_rir_program_with_adaptive_profile(source: &str) -> Program {
get_rir_program_with_capabilities(source, Profile::Adaptive.into())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

we have other tests (mostly in loops.rs, but a few others too) that just call get_rir_program_with_capabilities passing adaptive. I think we should either update those to use this helper or just remove the helper and call the function with the parameter directly, for consistency. Same with the below.

args.iter()
.map(|arg| {
let value = match arg {
Arg::Discard(value) => value,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This isn't really specific to your changes, but looking at how Arg::Discard is used, I don't think it needs to be handled here. We would never be emitting a call to an ir function with a discard, as that concept doesn't exist in LLVM. And even for existing usage, I feel like the right thing to do would be to transform away discards at an earlier step, but such a transform doesn't exist today. Perhaps we can consider something like that in the future with an FIR transform (especially post tuple decomposition), but for now leaving this is in is fine. Perhaps worth a follow up investigation issue or something?

Comment on lines +16 to +39
pub(crate) fn assert_panics_with(expected_substring: &str, operation: impl FnOnce()) {
let _hook_guard = PANIC_HOOK_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);

let previous_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(operation));
std::panic::set_hook(previous_hook);

let payload =
result.expect_err("expected the operation to panic, but it completed without panicking");
let message = match payload.downcast::<String>() {
Ok(message) => *message,
Err(payload) => match payload.downcast::<&str>() {
Ok(message) => (*message).to_string(),
Err(_) => "(non-string panic payload)".to_string(),
},
};
assert!(
message.contains(expected_substring),
"panic message did not contain the expected substring.\n expected substring: {expected_substring}\n actual message: {message}"
);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this is text-identical to the assert_panics_with in the qsc_fir_transforms crate. I wonder if there is a reasonable place to put this so that both can depend on it? Maybe qsc_data_structures?


declare void @__quantum__rt__tuple_record_output(i64, ptr)

attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="0" "required_num_results"="0" }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

it occurs to me looking at this one that the required_num_qubits is 0 by design, but I wonder if might make more sense to make that whole part of the attributes somethign that gets filled in so we can skip it when dynamic qubit allocation is used. Double checking the spec, it says the attribute is "not required" when using the dynamic qubits extension. Likewise, the dynamic_qubit_management flag below should get set to true. Not critical right now as this feature is not yet exposed, so it can get included now or have a follow up issue filed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

alternatively, we can wait until it gets incorporated into Adaptive by default and then just hard-code true and drop the required_num_qubits entirely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants