FIR Transforms#3187
Conversation
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
…e values as input. Co-authored-by: Copilot <copilot@github.com>
…r codegen. Use pinning as a fallback for stateful captures Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
| fn clone_fir_package(package: &Package) -> Package { | ||
| Package { | ||
| items: package.items.clone(), | ||
| entry: package.entry, | ||
| entry_exec_graph: package.entry_exec_graph.clone(), | ||
| blocks: package.blocks.clone(), | ||
| exprs: package.exprs.clone(), | ||
| pats: package.pats.clone(), | ||
| stmts: package.stmts.clone(), | ||
| } | ||
| } |
There was a problem hiding this comment.
Why not just add #[derive(Clone)] to package?
| fn clone_fir_store(fir_store: &qsc_fir::fir::PackageStore) -> qsc_fir::fir::PackageStore { | ||
| let mut cloned_store = qsc_fir::fir::PackageStore::new(); | ||
| for (package_id, package) in fir_store { | ||
| cloned_store.insert(package_id, clone_fir_package(package)); | ||
| } | ||
| cloned_store | ||
| } |
There was a problem hiding this comment.
Same here, could implement Clone for qsc_fir::fir::PackageStore.
| /// 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>> { |
There was a problem hiding this comment.
Not using _args here? Maybe can delete the _args parameter?
| assigner.set_next_block(BlockId::from(max + 1)); | ||
| } |
There was a problem hiding this comment.
Since IndexMap is ordered, we can cheat by just using next_back()
| assigner.set_next_block(BlockId::from(max + 1)); | |
| } | |
| let max_block = package.blocks.iter().next_back(); | |
| if let Some((max, _)) = max_block { | |
| assigner.set_next_block(max.successor()); | |
| } |
| assigner.set_next_stmt(StmtId::from(max + 1)); | ||
| } | ||
|
|
||
| // NodeId — scan callable and spec decls |
There was a problem hiding this comment.
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.
| pub mod fir_transforms { | ||
| pub use qsc_fir_transforms::{ | ||
| PipelineResult, PipelineStage, defunctionalize, run_pipeline_to_with_diagnostics, | ||
| run_pipeline_with_diagnostics, | ||
| }; | ||
| } |
There was a problem hiding this comment.
Looks like most of these are unused, so only run_pipeline_with_diagnostics needs to be preserved:
| pub mod fir_transforms { | |
| pub use qsc_fir_transforms::{ | |
| PipelineResult, PipelineStage, defunctionalize, run_pipeline_to_with_diagnostics, | |
| run_pipeline_with_diagnostics, | |
| }; | |
| } | |
| pub mod fir_transforms { | |
| pub use qsc_fir_transforms::run_pipeline_with_diagnostics; | |
| } |
| @@ -65,6 +87,38 @@ fn test_single_qubit() { | |||
| ); | |||
| } | |||
|
|
|||
| #[test] | |||
| fn test_explicitly_annotated_single_qubit_rewrite_preserves_binding_name_and_types() { | |||
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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?
| 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) => { |
There was a problem hiding this comment.
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)] |
There was a problem hiding this comment.
Looks like this allow isn't needed anymore.
| testutil = ["qsc_frontend", "qsc_hir", "qsc_passes"] | ||
|
|
||
| [dev-dependencies] | ||
| qsc_fir_transforms = { path = ".", features = ["testutil"] } |
There was a problem hiding this comment.
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.
| qsc_fir::assigner::Assigner, | ||
| ); | ||
|
|
||
| const EXCESSIVE_SPECIALIZATIONS_SOURCE: &str = r#" |
There was a problem hiding this comment.
nit, this could be inlined, as it's the only case pulled out into a const and isn't actually reused across tests.
| /// | ||
| /// Panics if the package has no entry expression. | ||
| #[must_use] | ||
| pub fn collect_reachable_package_closure_from_entry( |
There was a problem hiding this comment.
This appears to be dead code...
There was a problem hiding this comment.
Along those lines, it's worth doing a pass over the crate to update visibility so that only things really needed across crates are pub and the rest are pub(crate) which should enable clippy to warn on unused code.
| fn entry_expression_followed() { | ||
| // A single entry point with no calls — only Main is reachable. |
There was a problem hiding this comment.
This test is technically redundant with unreachable_callable_excluded so only one of them is required.
| } | ||
|
|
||
| #[test] | ||
| fn lambda_in_entry_expression() { |
There was a problem hiding this comment.
It would also be good to include a test that confirms that callables used within lambda bodies are seen as reachable. Something like:
#[test]
fn callable_only_in_closure_body() {
check(
indoc! {"
namespace Test {
function Other() : Unit {}
@EntryPoint()
function Main() : Unit {
let f = () -> Other();
}
}
"},
&expect![[r#"
<lambda>
Main
Other"#]],
);
}|
|
||
| // Temporarily take the target package out of the store so we can hold | ||
| // `&source_pkg` (for cross-package) and `&mut target_pkg` simultaneously. | ||
| let empty_pkg = empty_package(); |
There was a problem hiding this comment.
This could just be Package::default()
| let source_decl = match &source_item.kind { | ||
| ItemKind::Callable(decl) => decl.as_ref(), | ||
| _ => continue, | ||
| }; | ||
| let body_pkg = extract_callable_body(source_pkg, source_decl); |
There was a problem hiding this comment.
it's possible this could let-else with a panic rather than a continue.
| // entry yet. Those new specializations may reference newly-cloned | ||
| // closure items that are also unreachable from entry until call sites | ||
| // are redirected. | ||
| let mut walked_items: FxHashSet<LocalItemId> = local_item_ids.iter().copied().collect(); |
There was a problem hiding this comment.
| let mut walked_items: FxHashSet<LocalItemId> = local_item_ids.iter().copied().collect(); | |
| let mut walked_items: FxHashSet<LocalItemId> = local_item_ids.into_iter().collect(); |
| // closure items that are also unreachable from entry until call sites | ||
| // are redirected. | ||
| let mut walked_items: FxHashSet<LocalItemId> = local_item_ids.iter().copied().collect(); | ||
| walked_items.extend(new_item_ids.iter().copied()); |
There was a problem hiding this comment.
| walked_items.extend(new_item_ids.iter().copied()); | |
| walked_items.extend(new_item_ids.iter()); |
| package_id: PackageId, | ||
| specializations: &[Specialization], | ||
| ) -> Vec<ExprId> { | ||
| let reachable = collect_reachable_from_entry(store, package_id); |
There was a problem hiding this comment.
It may be possible to reuse the reachable set computed in discover_instantiations since in theory it should be the same as what we get here.
| for (pkg_id, package) in store { | ||
| for (item_id, item) in &package.items { | ||
| if let ItemKind::Ty(_, udt) = &item.kind { | ||
| cache.insert((pkg_id, item_id), udt.get_pure_ty()); |
There was a problem hiding this comment.
not sure if this provides any specific benefit, but I couldn't help but notice that this key is essentially a StoreItemId which already support impl From<(PackageId, LocalItemId)> for StoreItemId. So the UdtCache type could be defined as FxHashMap<StoreItemId, Ty> instead of having the raw tuple.
| mutated_exprs | ||
| } else { | ||
| let mut cloner = FirCloner::new(store.get(pkg_id)); | ||
| erase_udts_in_package(store.get_mut(pkg_id), &udt_cache, &mut cloner) |
There was a problem hiding this comment.
not critical, but worth noting for possible future perf efforts: this will perform udt erasure across the whole package while technically only the UDTs needed for the reachable subset of items need to be erased. If erase_udts_in_package where changed to erase_udts_in_item then this could iterate over reachable and potentially reduce the workload quite significantly (in particular, the whole stdlib would go through erasure where item-based iteration could likely avoid much of the stdlib).
There was a problem hiding this comment.
oh, I see. Iterating over the whole package down in erase_udts_in_package is easier then identifying the subset of exprs that correspond to the reachable items. So this one might be a toss up, perf-wise, as you'd have to do extra iteration to find the subset of exprs, pats, and items that are reachable from the reachable set.
| let structurally_mutated_external_specs: Vec<_> = structurally_mutated_specs | ||
| .into_iter() | ||
| .filter(|spec_id| spec_id.callable.package != package_id) | ||
| .collect(); |
There was a problem hiding this comment.
Since only erase_udts populates structurally_mutated_external_specs it seems possible that other passes that modify signatures or expressions might miss having their exec graphs rebuilt.
|
|
||
| use crate::EMPTY_EXEC_RANGE; | ||
|
|
||
| /// Runs the SROA pass on the entry-reachable portion of a package. |
There was a problem hiding this comment.
nitpick: this name is technically accurage, because here "aggregates" really means fixed length aggregates, of which Q# only supports tuples (arrays are really more like vectors with variable size). But it almost implies arrays are handled, which makes me wonder if "srot" for scalar replacement of tuples or something might be more clear.
Summary
This PR adds FIR passes to enable broader code generation scenarios.
QIR does not support:
RIR doesn't currently support:
return_unifytries 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.
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
Crate organization
Integrating
qsc_fir_transformswithqsc_passeswas going to make the PR look much bigger with a lot of moved files. My plan was to mergeqsc_fir_tranformsintoqsc_passesand organize them byHIRandFIR. 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::FirTransformwill merge withErrorKind::Passinsource/compiler/qsc/src/compile.rsin a follow up PR unless we want to differentiate betweenHIRandFIRpasses at this level. We may want to differentiate at theqsc_passeslevel but merge them at this level as diagnostic transparent pass errors. The same follows forError::PassandError::FirTransforminsource/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
fremThe
freminstruction 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 theangletype 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.rsindicating 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
: Qubittype onusestatements leads to the var's pat type being error.