Skip to content
Closed
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
2 changes: 1 addition & 1 deletion source/compiler/qsc_fir/src/fir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ impl From<(PackageId, BlockId)> for StoreBlockId {
}

/// A unique identifier for an expression within a package store.
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)]
pub struct StoreExprId {
/// The package ID.
pub package: PackageId,
Expand Down
22 changes: 16 additions & 6 deletions source/compiler/qsc_partial_eval/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1620,11 +1620,13 @@ impl<'a> PartialEvaluator<'a> {
// moved into the call scope; these are used to generate the `Instruction::Call` at the call
// site instead of inlining the body. Eligible callees only have scalar/qubit leaf
// parameters, so the operand mapping below cannot encounter composite values.
let ir_function_arg_operands = if matches!(self.get_expr_compute_kind(call_expr_id),
ComputeKind::Dynamic {runtime_features, ..} if runtime_features.contains(RuntimeFeatureFlags::MustBeInlined))
|| self.in_parallel_expr()
let ir_function_arg_operands = if self.is_must_inline_call(
store_item_id,
functor_app,
(self.get_current_package_id(), call_expr_id).into(),
) || self.in_parallel_expr()
{
// A call site that has the `MustBeInlined` runtime feature flag set is not eligible to be emitted as an IR function
// A call site that has been marked for inlining is not eligible to be emitted as an IR function
// based on Runtime Capabilities Analysis, so we fall through to the inline path
// OR we are currently in a parrallel expression which requires that all calls are inlined to ensure the whole parallel
// ends up in a single block.
Expand Down Expand Up @@ -2161,8 +2163,6 @@ impl<'a> PartialEvaluator<'a> {
.contains(TargetCapabilityFlags::CallSupport)
{
// In this case, we know the target can't support any IR function calls, so always return false.
// This is needed because when the target does not have that support we don't emit the `MustBeInlined`
// capability on the call site.
return false;
}

Expand Down Expand Up @@ -3521,6 +3521,16 @@ impl<'a> PartialEvaluator<'a> {
.is_unresolved_callee_expr(store_expr_id, self.in_parallel_scope())
}

fn is_must_inline_call(
&self,
callee: StoreItemId,
functor_app: FunctorApp,
expr: StoreExprId,
) -> bool {
self.compute_properties
.is_must_inline_call(&(callee, functor_app).into(), &expr)
}

fn get_call_compute_kind(&self, callable_scope: &Scope) -> ComputeKind {
let store_item_id = StoreItemId::from((
callable_scope.package_id,
Expand Down
11 changes: 10 additions & 1 deletion source/compiler/qsc_rca/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ impl From<(LocalItemId, FunctorSetValue)> for LocalSpecId {
}
}

#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub struct GlobalSpecId {
pub callable: StoreItemId,
pub functor_set_value: FunctorSetValue,
Expand All @@ -101,6 +101,15 @@ impl From<(StoreItemId, FunctorSetValue)> for GlobalSpecId {
}
}

impl From<(StoreItemId, FunctorApp)> for GlobalSpecId {
fn from(value: (StoreItemId, FunctorApp)) -> Self {
Self {
callable: value.0,
functor_set_value: value.1.functor_set_value(),
}
}
}

pub trait FunctorAppExt {
fn functor_set_value(&self) -> FunctorSetValue;
}
Expand Down
72 changes: 43 additions & 29 deletions source/compiler/qsc_rca/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ impl<'a> Analyzer<'a> {

fn analyze_expr_call(
&mut self,
call_expr: ExprId,
callee_expr_id: ExprId,
args_expr_id: ExprId,
expr_type: &Ty,
Expand All @@ -392,14 +393,9 @@ impl<'a> Analyzer<'a> {
value_kind,
}
} else {
self.analyze_expr_call_with_static_callee(callee_expr_id, args_expr_id)
self.analyze_expr_call_with_static_callee(call_expr, callee_expr_id, args_expr_id)
};

// Cache the `MustBeInlined` runtime feature flag if it was set on the compute kind of the call expression.
// This allows it to be added again later after aggregating the runtime features of the callee and arguments expressions,
// which may have cleared that flag.
let must_inline = matches!(compute_kind, ComputeKind::Dynamic { runtime_features, .. } if runtime_features.contains(RuntimeFeatureFlags::MustBeInlined));

// If this call happens within a dynamic scope, there might be additional runtime features being used.
let application_instance = self.get_current_application_instance();
if !application_instance.active_dynamic_scopes.is_empty() {
Expand Down Expand Up @@ -445,20 +441,13 @@ impl<'a> Analyzer<'a> {
// Aggregate the runtime features of the callee and arguments expressions.
compute_kind.aggregate_runtime_features(callee_expr_compute_kind, ValueKind::Constant);
compute_kind.aggregate_runtime_features(args_expr_compute_kind, ValueKind::Constant);

if must_inline
&& let ComputeKind::Dynamic {
runtime_features, ..
} = &mut compute_kind
{
*runtime_features |= RuntimeFeatureFlags::MustBeInlined;
}

compute_kind
}

#[allow(clippy::too_many_lines)]
fn analyze_expr_call_with_spec_callee(
&mut self,
call_expr: ExprId,
callee: &Callee,
callable_decl: &'a CallableDecl,
args_expr_id: ExprId,
Expand Down Expand Up @@ -519,6 +508,7 @@ impl<'a> Analyzer<'a> {
.target_capabilities
.contains(TargetCapabilityFlags::CallSupport)
&& self.check_must_inline(
callee_id,
callable_decl,
&arg_compute_kinds,
&mut compute_kind,
Expand Down Expand Up @@ -579,19 +569,34 @@ impl<'a> Analyzer<'a> {
}
}

if must_inline
&& let ComputeKind::Dynamic {
runtime_features, ..
} = &mut compute_kind
if must_inline && !matches!(compute_kind, ComputeKind::Static) {
// This is a dynamic call expression that must be inlined, so track the call expr id
// in the map to ensure that partial eval will inline it.
self.package_store_compute_properties
.insert_must_inline_call_expr((self.get_current_package_id(), call_expr).into());
}

// If the callee must always be inlined, mark the current item as requiring inlining too.
// This ensures that whole call stack gets inlined into the entry point if a must-inline callable is invoked.
if self
.package_store_compute_properties
.is_must_inline_callable(callee_id)
&& let AnalysisContext::Item(item_context) = self.get_current_context()
{
*runtime_features |= RuntimeFeatureFlags::MustBeInlined;
let functor = item_context
.current_spec_context
.as_ref()
.map_or(FunctorSetValue::Empty, |c| c.functor_set_value);
self.package_store_compute_properties
.insert_must_inline_callable((item_context.id, functor).into());
}

compute_kind
}

fn check_must_inline(
&self,
callee_id: GlobalSpecId,
callable_decl: &'a CallableDecl,
arg_compute_kinds: &[ComputeKind],
compute_kind: &mut ComputeKind,
Expand All @@ -610,8 +615,8 @@ impl<'a> Analyzer<'a> {
&& if let ComputeKind::Dynamic {
runtime_features, ..
} = &ir_function_compute_kind
// ...and the computed runtime features of the resulting IR function does not require qubit allocation when the target doesn't support it...
&& (!runtime_features.contains(RuntimeFeatureFlags::QubitAllocation) || self.target_capabilities.contains(TargetCapabilityFlags::DynamicQubitAllocation))
// ...and the call is not one that we've marked as must-inline...
&& !self.package_store_compute_properties.is_must_inline_callable(callee_id)
// ...and the computed runtime features of the function do not involve call to unresolved callee...
&& !runtime_features.contains(RuntimeFeatureFlags::CallToUnresolvedCallee)
// ...and those computed runtime features are all supported by the target capabilities...
Expand All @@ -635,6 +640,7 @@ impl<'a> Analyzer<'a> {

fn analyze_expr_call_with_static_callee(
&mut self,
call_expr: ExprId,
callee_expr_id: ExprId,
args_expr_id: ExprId,
) -> ComputeKind {
Expand Down Expand Up @@ -690,6 +696,7 @@ impl<'a> Analyzer<'a> {
};
match global_callee {
Global::Callable(callable_decl) => self.analyze_expr_call_with_spec_callee(
call_expr,
&callee,
callable_decl,
args_expr_id,
Expand Down Expand Up @@ -1396,6 +1403,19 @@ impl<'a> Analyzer<'a> {
}
};

if matches!(callable_context.kind, CallableKind::Operation)
&& is_qubit_allocation_output(&callable_context.output_type)
&& !self
.target_capabilities
.contains(TargetCapabilityFlags::DynamicQubitAllocation)
{
// The only intrinsic operations whose output is a qubit (or an array of qubits) are the qubit
// allocate/borrow intrinsics. Tag the allocate leaf so that qubit allocation is always inlined
// and propagated bottom-up to every transitive caller.
self.package_store_compute_properties
.insert_must_inline_callable(body_specialization_id);
}

// Insert the generator set in the entry corresponding to the body specialization of the callable.
self.package_store_compute_properties.insert_spec(
body_specialization_id,
Expand Down Expand Up @@ -2161,7 +2181,7 @@ impl<'a> Visitor<'a> for Analyzer<'a> {
}
ExprKind::Block(block_id) => self.analyze_expr_block(*block_id),
ExprKind::Call(callee_expr_id, args_expr_id) => {
self.analyze_expr_call(*callee_expr_id, *args_expr_id, &expr.ty)
self.analyze_expr_call(expr_id, *callee_expr_id, *args_expr_id, &expr.ty)
}
ExprKind::Closure(..) => ComputeKind::Static,
ExprKind::Fail(msg_expr_id) => self.analyze_expr_fail(*msg_expr_id),
Expand Down Expand Up @@ -2646,12 +2666,6 @@ fn derive_instrinsic_operation_application_generator_set(
if callable_context.attrs.contains(&Attr::Reset) {
inherent_runtime_features |= RuntimeFeatureFlags::CallToCustomReset;
}
// The only intrinsic operations whose output is a qubit (or an array of qubits) are the qubit
// allocate/borrow intrinsics. Tag the allocate leaf so that qubit allocation is surfaced as an
// inherent runtime feature and propagated bottom-up to every transitive caller.
if is_qubit_allocation_output(&callable_context.output_type) {
inherent_runtime_features |= RuntimeFeatureFlags::QubitAllocation;
}

// The compute kind of intrinsic operations is always dynamic.
let inherent_compute_kind = ComputeKind::Dynamic {
Expand Down
28 changes: 17 additions & 11 deletions source/compiler/qsc_rca/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ use std::{
};

pub use crate::analyzer::Analyzer;
use crate::common::GlobalSpecId;

/// A trait to look for the compute properties of elements in a package store.
pub trait ComputePropertiesLookup {
Expand Down Expand Up @@ -66,6 +67,12 @@ pub struct PackageStoreComputeProperties {
// The alternative compute properties for each package if the callables are invoked
// from within a parallel expression, keyed by package ID.
parallel_props: IndexMap<PackageId, PackageComputeProperties>,
// The set of global callables that must be inlined (usually due to qubit allocation) and
// cause any caller to also require inlining.
must_inline_callables: FxHashSet<GlobalSpecId>,
// The set of call expressions that must be inlined either because of the callable being invoked,
// or because of the combination of callable and runtime features of the arguments to the call expr.
must_inline_call_exprs: FxHashSet<StoreExprId>,
}

impl ComputePropertiesLookup for PackageStoreComputeProperties {
Expand Down Expand Up @@ -144,6 +151,11 @@ impl PackageStoreComputeProperties {
.unresolved_callee_exprs
.contains(&id.expr)
}

#[must_use]
pub fn is_must_inline_call(&self, id: &GlobalSpecId, expr: &StoreExprId) -> bool {
self.must_inline_callables.contains(id) || self.must_inline_call_exprs.contains(expr)
}
}

/// The compute properties of a package.
Expand Down Expand Up @@ -557,11 +569,10 @@ impl ComputeKind {
};

// Determine the aggregated runtime features, use the value kind equivalent from self or the default.
// We don't propagate the `MustBeInlined` runtime feature because it is only relevant at call expressions.
match self {
Self::Static => {
*self = ComputeKind::Dynamic {
runtime_features: runtime_features & !RuntimeFeatureFlags::MustBeInlined,
runtime_features,
value_kind: default_value_kind,
}
}
Expand All @@ -570,7 +581,6 @@ impl ComputeKind {
..
} => {
*self_runtime_features |= runtime_features;
self_runtime_features.remove(RuntimeFeatureFlags::MustBeInlined);
}
}
}
Expand Down Expand Up @@ -650,7 +660,7 @@ bitflags! {
/// Runtime features represent anything a program can do that is more complex than executing quantum operations on
/// statically allocated qubits and using constant arguments.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RuntimeFeatureFlags: u64 {
pub struct RuntimeFeatureFlags: u32 {
/// Use of a dynamic `Bool`.
const UseOfDynamicBool = 1 << 0;
/// Use of a dynamic `Int`.
Expand Down Expand Up @@ -709,16 +719,12 @@ bitflags! {
const CallToCustomReset = 1 << 27;
/// Use of a dynamic generic parameter.
const UseOfDynamicGeneric = 1 << 28;
/// A callable allocates qubits (directly or transitively).
const QubitAllocation = 1 << 29;
/// A dynamic release of a qubit.
const UseOfDynamicQubitRelease = 1 << 30;
/// A callable whose required features mean it must be inlined rather than emitted as an IR function.
const MustBeInlined = 1 << 31;
const UseOfDynamicQubitRelease = 1 << 29;
/// Use of dynamic branching in a parallel expression.
const UseOfDynamicBranchingInParallelExpr = 1 << 32;
const UseOfDynamicBranchingInParallelExpr = 1 << 30;
/// Use of a dynamic limit in a parallel expression.
const UseOfDynamicLimitInParallelExpr = 1 << 33;
const UseOfDynamicLimitInParallelExpr = 1 << 31;
}
}

Expand Down
25 changes: 25 additions & 0 deletions source/compiler/qsc_rca/src/scaffolding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use qsc_fir::{
},
ty::FunctorSetValue,
};
use rustc_hash::FxHashSet;

/// Scaffolding used to build the package store compute properties.
#[derive(Debug)]
Expand All @@ -23,6 +24,12 @@ pub struct InternalPackageStoreComputeProperties {
// The alternative compute properties for each package if the callables are invoked
// from within a parallel expression, keyed by package ID.
parallel_props: IndexMap<PackageId, InternalPackageComputeProperties>,
// The set of global callables that must be inlined (usually due to qubit allocation) and
// cause any caller to also require inlining.
must_inline_callables: FxHashSet<GlobalSpecId>,
// The set of call expressions that must be inlined either because of the callable being invoked,
// or because of the combination of callable and runtime features of the arguments to the call expr.
must_inline_call_exprs: FxHashSet<StoreExprId>,
}

impl From<PackageStoreComputeProperties> for InternalPackageStoreComputeProperties {
Expand Down Expand Up @@ -70,6 +77,8 @@ impl From<PackageStoreComputeProperties> for InternalPackageStoreComputeProperti
Self {
props: scaffolding,
parallel_props: parallel_scaffolding,
must_inline_callables: value.must_inline_callables,
must_inline_call_exprs: value.must_inline_call_exprs,
}
}
}
Expand Down Expand Up @@ -122,6 +131,8 @@ impl From<InternalPackageStoreComputeProperties> for PackageStoreComputeProperti
Self {
props: package_store_compute_properties,
parallel_props: parallel_package_store_compute_properties,
must_inline_callables: value.must_inline_callables,
must_inline_call_exprs: value.must_inline_call_exprs,
}
}
}
Expand Down Expand Up @@ -227,6 +238,8 @@ impl InternalPackageStoreComputeProperties {
Self {
props: packages,
parallel_props: parallel_packages,
must_inline_callables: Default::default(),
must_inline_call_exprs: Default::default(),
}
}

Expand Down Expand Up @@ -278,6 +291,18 @@ impl InternalPackageStoreComputeProperties {
.stmts
.insert(id.stmt, value);
}

pub(crate) fn insert_must_inline_callable(&mut self, id: GlobalSpecId) {
self.must_inline_callables.insert(id);
}

pub(crate) fn is_must_inline_callable(&self, id: GlobalSpecId) -> bool {
self.must_inline_callables.contains(&id)
}

pub(crate) fn insert_must_inline_call_expr(&mut self, id: StoreExprId) {
self.must_inline_call_exprs.insert(id);
}
}

/// Scaffolding used to build the compute properties of a package.
Expand Down
Loading