Skip to content

FIR Transforms#3187

Open
idavis wants to merge 35 commits into
mainfrom
iadavis/qsc_fir_transforms
Open

FIR Transforms#3187
idavis wants to merge 35 commits into
mainfrom
iadavis/qsc_fir_transforms

Conversation

@idavis
Copy link
Copy Markdown
Collaborator

@idavis idavis commented Apr 29, 2026

Summary

This PR adds FIR passes to enable broader code generation scenarios.

QIR does not support:

  • Function pointers (and thus dynamic dispatch)
  • Structs
  • Tuples
  • Generics

RIR doesn't currently support:

  • Multiple returns: return_unify tries to remove this constraint but there are some odd things we can't deal with.

The passes peel each unsupported piece off in the pipepline.

  • Monomorphize cleans up generics
  • Return unify gets rid of multiple returns and allows us to better understand control flow
  • defunctionalization gets rid of callable exprs
  • erase_utds rewrites the FIR so that it uses tuples in place of structs
  • lower_tuple_comparison handles a special case of binop replacing it with short-circuiting element-wise comparisons which can be codegen'd
  • sroa and arg_promote work together to get rid of all possible tuple element usage
  • dce and gc passes clean up code that isn't called any longer so that RCA doesn't pay attention to it
  • there are some mini passes as well that collapse very specific patterns like the defunc prepass

Aside from the passes, this PR also tries to unify how the code goes through RCA and codegen compilation. There are some side effects which leak into circuits as we have to generate new functions as part of the passes that we don't necessarily want reflected in the circuit representation.

Suggested Review Assignment

Reviewer Best-fit parts
@swernli Core FIR transform pipeline, qsc_fir_transforms, QIR/codegen integration, partial eval, RCA, RIR, circuit behavior, root Cargo changes
@minestarks Broad compiler integration, qsc/qsc_circuit/qsc_frontend/qsc_lowerer/qsc_openqasm_compiler/qsc_passes, Python/package-facing changes, RIR, circuit behavior, root Cargo changes
@billti Python package tests/snapshots, fuzz target, wasm diagnostic touchpoint, resource estimator test, default-owned samples/index_map fallout
@ScottCarda-MS Language service and npm snapshot changes

Crate organization

Integrating qsc_fir_transforms with qsc_passes was going to make the PR look much bigger with a lot of moved files. My plan was to merge qsc_fir_tranforms into qsc_passes and organize them by HIR and FIR. This way we'd have a clean refactoring PR with no functional changes. This PR is already very large and I thought this integration was just too much to add.

Error types

ErrorKind::FirTransform will merge with ErrorKind::Pass in source/compiler/qsc/src/compile.rs in a follow up PR unless we want to differentiate between HIR and FIR passes at this level. We may want to differentiate at the qsc_passes level but merge them at this level as diagnostic transparent pass errors. The same follows for Error::Pass and Error::FirTransform in source/compiler/qsc/src/interpret.rs.

Interpret

This crate has two major changes. First the codegen module has a lot of added code for preparing the compilation. When we have both callables with interpret values (which may themselves be callables/structs/tuples which may contain the same complicated values) and entry expressions, we need to update the compilation in very different ways. For callables we need to effectively generate a new synthetic entry expr which can use the interpreter values. There is a case when dealing with closures where we need to partially abandon this pass and use a fallback of pinned non-entry-reachable items which are passed into the pipeline for processing. Entry expresssions are the easy path and just work as normal heading into the pipeline.

The interpret module does some setup work to help the codegen module.

The openqasm module has some fixes that are related to profile not being plumbed correctly. We weren't handling the user's specified profile and the codes annotated profile correct when used together and making the assumtion that if it was missing from the code that the profile was unrestricted. You'll see this update propagated into the Python and parser.

qsc_fir

The big addition here is the assigner. The FIR transforms do a lot of code generation and mutation, but it is additive. When we are generating new code, we need consistent, non-overlapping ids, for blocks, exprs, items, etc. This assigner update allows us to create an assigner from a package which finds the next values of each id needed so that we can safely allocate.

Testing

Some tests have been added to seemingly random places. These tests were added after I broke things and didn't know as no tests were failing. They are there to prevent regressions.

New instruction frem

The frem instruction is added to support OpenQASM dynamic angle support. Hopefully it will be added to the adaptive profile soon. Without this instruction we cannot do runtime angle calculations in OpenQASM as the angle type requires this computation.

Codegen

The qir codegen now requires RCA to have been already done before calling into fir_to_rir. We had too many places where we were or were not running RCA and then having to run it after the fact. This made it difficult to know when RCA was actually taking place. There are a few refactorings around this so that we have this more consolidated, but we might want to take a deeper step towards unifying in the future.

Circuits

Transformed callables are cloned into the user package. In order to maintain the same visualization as before, we have to detect whether we are in a 'synthetic' callable context so that we don't emit the call as a grouping context.

Partial eval

There is a lot of code in partial eval for dealing with return statements. I've documented source/compiler/qsc_partial_eval/src/evaluation_context.rs indicating that this is no longer required, but such a refactoring adds a lot of risk and code change which is better defferred to a follow up PR.

LLVM IR Changes

There are a few test files which are updated as the passes enable better code generation options that were impossible to handle before and were forced to be inlined.

Performance

The FIR transforms can be made faster, but they take less than 1/5 the time of the regular compilation and 1/15 as much time as RCA, so they are fast enough for the moment.

Random looking changes

source/compiler/qsc_frontend/src/closure.rs - documented here as the exact shape of closures has downstream effects and we can't vary from this structure without also changing many other sites.
source/compiler/qsc_frontend/src/resolve.rs - fixes a bug in type resolution where supplying an explicit : Qubit type on use statements leads to the var's pat type being error.

@idavis idavis self-assigned this Apr 29, 2026
Comment thread source/compiler/qsc_eval/src/lib.rs Outdated
Comment thread source/index_map/src/lib.rs Outdated
Comment thread source/compiler/qsc_codegen/src/qir/v1.rs Outdated
Comment thread source/compiler/qsc_partial_eval/src/evaluation_context.rs
Comment thread source/compiler/qsc/src/interpret.rs
Comment thread source/compiler/qsc/src/codegen/tests.rs Outdated
Comment thread source/compiler/qsc/src/codegen.rs Outdated
Comment thread source/compiler/qsc/src/codegen.rs Outdated
Comment thread source/compiler/qsc/src/codegen/tests.rs Outdated
Comment thread source/compiler/qsc/src/codegen.rs Outdated
Comment thread source/compiler/qsc/src/codegen.rs Outdated
Comment on lines +1178 to +1189
/// Pin-based fallback for callable args containing closures with captures.
///
/// Seeds concrete (non-arrow-input) callables into the entry for reachability,
/// pins arrow-input callables and the target for DCE survival, and lets
/// `fir_to_qir_from_callable` handle specialization at QIR generation time.
fn prepare_codegen_fir_from_callable_args_pinned(
package_store: &PackageStore,
callable: qsc_hir::hir::ItemId,
_args: &Value,
capabilities: TargetCapabilityFlags,
mut concrete_callables: FxHashSet<qsc_fir::fir::StoreItemId>,
) -> Result<CodegenFir, Vec<Error>> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not using _args here? Maybe can delete the _args parameter?

Comment thread source/compiler/qsc_fir/src/assigner.rs Outdated
assigner.set_next_stmt(StmtId::from(max + 1));
}

// NodeId — scan callable and spec decls
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.

As it turns out, NodeId is only used in three places in FIR where it is set as an id, but then never read. I think we can drop it from FIR entirely.

@idavis idavis marked this pull request as ready for review May 18, 2026 17:06
Comment thread source/compiler/qsc/src/lib.rs
@@ -65,6 +87,38 @@ fn test_single_qubit() {
);
}

#[test]
fn test_explicitly_annotated_single_qubit_rewrite_preserves_binding_name_and_types() {
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 test and the one above are effectively identical... they don't verify anything different just use different mechanisms to do so.

let qir = generate_qir_from_ast(
package,
unit.source_map,
unit.profile.unwrap_or(Profile::Unrestricted),
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.

I get that this is only used for tests, but it seems odd for the default for QIR generation to be a profile that we know will fail QIR generation. Should this be Adaptive_RIF?

Comment on lines +857 to +870
Value::Array(vs) => {
let mut lowered_ids = Vec::with_capacity(vs.len());
for v in vs.iter() {
lowered_ids.push(lower_value_to_expr(package, assigner, v, callable_types));
}
let elem_ty = lowered_ids.first().map_or(qsc_fir::ty::Ty::Err, |id| {
package.exprs.get(*id).expect("just inserted").ty.clone()
});
(
qsc_fir::fir::ExprKind::Array(lowered_ids),
qsc_fir::ty::Ty::Array(Box::new(elem_ty)),
)
}
Value::Range(r) => {
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.

Since we know some folks invoke Q# callables with very large arrays (RE and chemistry scenarios, for example), we may pay a high cost of generating a large array literal into the synthetic entry expression only for it to be mostly ignored (since the synthetic entry is used for analysis in the passes and not execution). It might be worth trying to detect this case and avoid emitting constant arrays when not needed.

/// 3. Asserts the two results match (both succeed with equal values, or
/// both fail).
#[cfg(test)]
#[allow(dead_code)]
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.

Looks like this allow isn't needed anymore.

testutil = ["qsc_frontend", "qsc_hir", "qsc_passes"]

[dev-dependencies]
qsc_fir_transforms = { path = ".", features = ["testutil"] }
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 started talking about this, and I see why it's needed now to make the testutil functionality available via the public API to scenario tests. It seems like there might be another way around that (maybe moving the tests, maybe moving the utils), but it's not critical for this PR.

Comment thread source/compiler/qsc_fir_transforms/tests/pipeline_integration.rs Outdated
Comment thread source/compiler/qsc_fir_transforms/src/reachability.rs Outdated
Comment thread source/compiler/qsc_fir_transforms/src/reachability/tests.rs Outdated
Comment thread source/compiler/qsc_fir_transforms/src/reachability/tests.rs
Comment thread source/compiler/qsc_fir_transforms/src/monomorphize.rs Outdated
Comment thread source/compiler/qsc_fir_transforms/README.md Outdated
Comment thread source/compiler/qsc_fir_transforms/README.md
@idavis idavis force-pushed the iadavis/qsc_fir_transforms branch from 3203541 to dcecb3d Compare June 2, 2026 23:21
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.

Okay, am I hallucinating or did we already have a Q# pretty printer that we used for OpenQASM tests?

/// `CallableImpl` input counts) would flip that implicit assertion into a skew
/// panic before the explicit `ComputeKind` check below is reached.
#[test]
fn flag_fallback_value_kind_after_dynamic_scope_return() {
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 test could make use of the pattern used in callables::check_rca_for_callable_block_with_dynamic_unreachable_binding, specifically having check_callable_compute_properties validate the callable properties via expect.

@@ -1363,3 +1363,24 @@ fn call_to_intrinsic_operation_that_takes_qubit_array_should_fail() {
}
"});
}

#[test]
#[should_panic(
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.

I'm just noticing that this pattern (which other tests in this file use) is unnecessary: there is a helper that checks for partial evaluation errors: get_partial_evaluation_error. This test and the three above it could be converted to that pattern rather than should_panic which makes test output cleaner and continues the expect-test patterns.


impl InvariantLevel {
/// Returns `true` when this level is at or after monomorphization.
fn is_post_mono_or_later(self) -> bool {
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 that since the InvariantLevel enum is strictly ordered, you could derive PartialOrd and Ord and then accomplish these checks via simple >= checks.

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.

4 participants