From 64588416083ba056197745d6a449098aac79b4ff Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Tue, 28 Jul 2026 23:24:44 -0700 Subject: [PATCH] Support emission of locally constant arrays with dynamic contents This change adds additional handling in partial eval for arrays on entry to a new scope during function calls. Specificaly, any arguments that are arrays are recursively checked for contents that are `Value::Var` which indicates that the contents of the array are dynamic at runtime. However, since Q# callable arguments are read-only, these arrays can be treated as "locally constant" and emitted as stored values on scope entry. This then allows later instructions that might need to refer to those arrays (for now, just index instructions) to use those stored values in the same manner that globally constant arrays are used from the data section of the program. Since the existing RIR passes are already capable of eliminating unused store instructions, these stored arrays only persist into the final emitted program when they are actually needed for program functionality. Notably, this unblocks the emission of `Adjoint Std.TableLookup.Select` callable when using the `Adaptive` profile. --- source/compiler/qsc/src/codegen/tests.rs | 5 +- .../qsc_circuit/src/rir_to_circuit.rs | 1 + source/compiler/qsc_codegen/src/qir/v1.rs | 4 +- source/compiler/qsc_codegen/src/qir/v2.rs | 34 ++++ .../src/evaluation_context.rs | 9 +- source/compiler/qsc_partial_eval/src/lib.rs | 155 +++++++++++++++--- source/compiler/qsc_rca/src/core.rs | 10 +- source/compiler/qsc_rca/src/tests/arrays.rs | 6 +- .../qsc_rir/src/passes/insert_alloca_load.rs | 21 +++ .../src/passes/prune_unneeded_stores.rs | 9 + .../compiler/qsc_rir/src/passes/ssa_check.rs | 5 +- .../compiler/qsc_rir/src/passes/type_check.rs | 18 ++ source/compiler/qsc_rir/src/rir.rs | 21 +++ source/compiler/qsc_rir/src/utils.rs | 13 ++ 14 files changed, 272 insertions(+), 39 deletions(-) diff --git a/source/compiler/qsc/src/codegen/tests.rs b/source/compiler/qsc/src/codegen/tests.rs index e51a038fee8..545d19464d3 100644 --- a/source/compiler/qsc/src/codegen/tests.rs +++ b/source/compiler/qsc/src/codegen/tests.rs @@ -5779,10 +5779,7 @@ fn foreign_table_lookup_callable_generates_qir() { use address = Qubit[1]; use output = Qubit[1]; Std.TableLookup.Select([[false], [true]], address, output); - - // Adjoint of Select does not work until support for static sized, dynamic content arrays is added. - // See related issue: https://github.com/microsoft/qdk/issues/3388 - // Adjoint Std.TableLookup.Select([[false], [true]], address, output); + Adjoint Std.TableLookup.Select([[false], [true]], address, output); } "#}; for profile in [Profile::AdaptiveRI, Profile::AdaptiveRIF, Profile::Adaptive] { diff --git a/source/compiler/qsc_circuit/src/rir_to_circuit.rs b/source/compiler/qsc_circuit/src/rir_to_circuit.rs index 1816207a59d..cff83b3806b 100644 --- a/source/compiler/qsc_circuit/src/rir_to_circuit.rs +++ b/source/compiler/qsc_circuit/src/rir_to_circuit.rs @@ -423,6 +423,7 @@ fn process_variables( store_expr_in_variable(&mut state.variables, *variable, expr)?; } instruction @ (Instruction::Store(..) + | Instruction::StoreArray(..) | Instruction::BitwiseNot(..) | Instruction::Alloca(..) | Instruction::Load(..) diff --git a/source/compiler/qsc_codegen/src/qir/v1.rs b/source/compiler/qsc_codegen/src/qir/v1.rs index 528424c1143..26f7abe2bad 100644 --- a/source/compiler/qsc_codegen/src/qir/v1.rs +++ b/source/compiler/qsc_codegen/src/qir/v1.rs @@ -221,7 +221,9 @@ impl ToQir for rir::Instruction { rir::Instruction::Srem(lhs, rhs, variable) => { binop_to_qir("srem", lhs, rhs, *variable, program) } - rir::Instruction::Store(_, _) => unimplemented!("store should be removed by pass"), + rir::Instruction::Store(_, _) | rir::Instruction::StoreArray(_, _) => { + unimplemented!("store should be removed by pass") + } rir::Instruction::Sub(lhs, rhs, variable) => { binop_to_qir("sub", lhs, rhs, *variable, program) } diff --git a/source/compiler/qsc_codegen/src/qir/v2.rs b/source/compiler/qsc_codegen/src/qir/v2.rs index f9f1a77f4c6..56b7533e5d4 100644 --- a/source/compiler/qsc_codegen/src/qir/v2.rs +++ b/source/compiler/qsc_codegen/src/qir/v2.rs @@ -228,6 +228,9 @@ impl ToQir for rir::Instruction { rir::Instruction::Store(operand, variable) => { store_to_qir(*operand, *variable, program) } + rir::Instruction::StoreArray(operands, variable) => { + store_array_to_qir(operands, *variable, program) + } rir::Instruction::Sub(lhs, rhs, variable) => { binop_to_qir("sub", lhs, rhs, *variable, program) } @@ -291,6 +294,37 @@ fn store_to_qir(operand: rir::Operand, variable: rir::Variable, program: &rir::P ) } +fn store_array_to_qir( + operands: &[rir::Operand], + variable: rir::Variable, + program: &rir::Program, +) -> String { + // Since SSA variables cannot be used in array literals, we cannot store the whole + // array into the alloca'ed space in one instruction. Instead, iterate through the array + // and create temporary variables for each memory location via getelementptr, then store + // the corresponding value into the array. + // This expands the single `StoreArray` instruction into 2N instructions, where N is the + // number of elements in the array. + let var_ty = get_variable_ty(variable); + let mut qir = String::new(); + let var_str = ToQir::::to_qir(&variable.variable_id, program); + for (i, operand) in operands.iter().enumerate() { + let temp_var = format!("{var_str}_{i}"); + writeln!( + qir, + " {temp_var} = getelementptr {var_ty}, ptr {var_str}, i64 0, i64 {i}" + ) + .expect("writing to string should succeed"); + write!( + qir, + " store {}, ptr {temp_var}", + ToQir::::to_qir(operand, program) + ) + .expect("writing to string should succeed"); + } + qir +} + fn load_to_qir(var_from: rir::Variable, var_to: rir::Variable, program: &rir::Program) -> String { let var_to_ty = get_variable_ty(var_to); format!( diff --git a/source/compiler/qsc_partial_eval/src/evaluation_context.rs b/source/compiler/qsc_partial_eval/src/evaluation_context.rs index 9d437d07bfb..f0ae3ffd8dc 100644 --- a/source/compiler/qsc_partial_eval/src/evaluation_context.rs +++ b/source/compiler/qsc_partial_eval/src/evaluation_context.rs @@ -10,7 +10,7 @@ use qsc_fir::fir::{LocalItemId, LocalVarId, PackageId}; use qsc_rca::{ComputeKind, RuntimeFeatureFlags, ValueKind}; use qsc_rir::rir::{BlockId, Literal, VariableId}; use rustc_hash::FxHashMap; -use std::collections::hash_map::Entry; +use std::{collections::hash_map::Entry, rc::Rc}; use crate::{ScopeDbgContext, is_static_value}; @@ -24,7 +24,7 @@ pub struct EvaluationContext { impl EvaluationContext { /// Creates a new evaluation context. pub fn new(package_id: PackageId, initial_block: BlockId) -> Self { - let entry_callable_scope = Scope::new(package_id, None, Vec::new(), None); + let entry_callable_scope = Scope::new(package_id, None, Vec::new(), None, Vec::new()); Self { active_blocks: vec![BlockNode { id: initial_block, @@ -118,6 +118,9 @@ pub struct Scope { active_block_count: usize, /// Debug context, used for generating debug metadata. pub(crate) dbg_context: ScopeDbgContext, + /// Any locally constant arrays associated with this scope, along with the variable ID + /// they are assigned (which happens after the scope is created). + pub(crate) arrays: Vec<(Rc>, Option)>, } impl Scope { @@ -127,6 +130,7 @@ impl Scope { callable: Option<(LocalItemId, FunctorApp)>, args: Vec, ctls_arg: Option, + arrays: Vec>>, ) -> Self { // Create the environment for the classical evaluator. // A default classical evaluator environment is created with one scope. However, we need to push an additional @@ -184,6 +188,7 @@ impl Scope { hybrid_vars, static_vars: FxHashMap::default(), dbg_context: ScopeDbgContext::default(), + arrays: arrays.into_iter().map(|array| (array, None)).collect(), } } diff --git a/source/compiler/qsc_partial_eval/src/lib.rs b/source/compiler/qsc_partial_eval/src/lib.rs index d262441cb4f..2aeb1da63bb 100644 --- a/source/compiler/qsc_partial_eval/src/lib.rs +++ b/source/compiler/qsc_partial_eval/src/lib.rs @@ -59,7 +59,7 @@ pub use qsc_rir::{ }, }; use rustc_hash::{FxHashMap, FxHashSet}; -use std::{collections::hash_map::Entry, rc::Rc, result::Result}; +use std::{collections::hash_map::Entry, mem::take, rc::Rc, result::Result}; use thiserror::Error; /// Partially evaluates a program with the specified entry expression. @@ -216,6 +216,10 @@ struct PartialEvaluator<'a> { dbg_context: DbgContext, } +// The resolved arguments of a callable invocation, including the resolved argument values, the optional +// resolved control qubits, and any array arguments with dynamic content of type `Value::Var`. +type ResolvedArguments = (Vec, Option, Vec>>); + #[derive(Clone, Copy)] pub struct PartialEvalConfig { pub generate_debug_metadata: bool, @@ -1592,7 +1596,7 @@ impl<'a> PartialEvaluator<'a> { ); None }; - let (args, ctls_arg) = self.resolve_args( + let (args, ctls_arg, arrays) = self.resolve_args( (store_item_id.package, callable_decl.input).into(), args_value.clone(), Some(args_span), @@ -1649,6 +1653,7 @@ impl<'a> PartialEvaluator<'a> { Some((store_item_id.item, functor_app)), args, ctls_arg, + arrays, ); self.check_unresolved_call_capabilities(call_expr_id, callee_expr_id, &call_scope)?; @@ -1788,7 +1793,7 @@ impl<'a> PartialEvaluator<'a> { panic!("global call to intrinsic function not supported"); }; - let (args, ctls_arg) = self.resolve_args( + let (args, ctls_arg, arrays) = self.resolve_args( (store_item_id.package, callable_decl.input).into(), args, None, @@ -1800,6 +1805,7 @@ impl<'a> PartialEvaluator<'a> { Some((store_item_id.item, FunctorApp::default())), args, ctls_arg, + arrays, ); // We generate instructions differently depending on whether we are calling an intrinsic or a specialization @@ -2017,7 +2023,7 @@ impl<'a> PartialEvaluator<'a> { let callable_id = self.get_or_insert_callable(callable); // Resolve the call arguments, create the call instruction and insert it to the current block. - let (args, ctls_arg) = self + let (args, ctls_arg, _) = self .resolve_args( (store_item_id.package, callable_decl.input).into(), args_value, @@ -2070,6 +2076,39 @@ impl<'a> PartialEvaluator<'a> { spec_decl: &SpecDecl, ) -> Result { self.eval_context.push_scope(call_scope); + + // Some arguments may include arrays with dynamic content, which we want to allow later instructions to index into. + // To support this, we treat these as locally constant arrays and emit an RIR instruction to store their contents into a new + // variable. Later instructions can then refer to this argument array based on it's Rc pointer and emit index instructions + // with dynamic indices into that array. We know this is safe for arrays passed as arguments because they are read-only + // by definition. + let mut local_constant_arrays = take(&mut self.eval_context.get_current_scope_mut().arrays); + for (array, var_id) in &mut local_constant_arrays { + let new_var_id = self.resource_manager.next_var(); + *var_id = Some(new_var_id); + let operands = array + .iter() + .map(|value| self.map_eval_value_to_rir_operand(value)) + .collect::>(); + let rir::Ty::Prim(elem_ty) = operands + .first() + .expect("array should have at least one element") + .get_type() + else { + panic!("array element type should be a primitive type"); + }; + self.get_current_rir_block_mut() + .0 + .push(Instruction::StoreArray( + operands, + rir::Variable { + variable_id: new_var_id, + ty: rir::Ty::Array(array.len(), elem_ty), + }, + )); + } + self.eval_context.get_current_scope_mut().arrays = local_constant_arrays; + let block_value = self.try_eval_block(spec_decl.block)?.into_value(); let popped_scope = self.eval_context.pop_scope(); assert!( @@ -2414,6 +2453,7 @@ impl<'a> PartialEvaluator<'a> { Some((store_item_id.item, functor_app)), body_args, None, + Vec::new(), ); self.eval_context.push_block_node(BlockNode { id: body_block_id, @@ -3757,7 +3797,7 @@ impl<'a> PartialEvaluator<'a> { args_span: Option, ctls: Option<(StorePatId, u8)>, fixed_args: Option>, - ) -> Result<(Vec, Option), Error> { + ) -> Result { let mut value = value; let ctls_arg = if let Some((ctls_pat_id, ctls_count)) = ctls { let mut ctls = vec![]; @@ -3800,15 +3840,23 @@ impl<'a> PartialEvaluator<'a> { }; let pat = self.package_store.get_pat(store_pat_id); - let args = match &pat.kind { - PatKind::Discard => vec![Arg::Discard(value)], + let (args, arrays) = match &pat.kind { + PatKind::Discard => (vec![Arg::Discard(value)], Vec::new()), PatKind::Bind(ident) => { + let arrays = if let Value::Array(array) = &value { + // If the value is an array, recursively check if any of the contents are variables, which would + // make the array a dynamic content array, and add them to the list of tracked arrays + // that we want to emit store instructions for. + get_arrays_with_dynamic_content(array) + } else { + Vec::new() + }; let variable = Variable { name: ident.name.clone(), value, span: ident.span, }; - vec![Arg::Var(ident.id, variable)] + (vec![Arg::Var(ident.id, variable)], arrays) } PatKind::Tuple(pats) => { let values = value.unwrap_tuple(); @@ -3818,10 +3866,11 @@ impl<'a> PartialEvaluator<'a> { "pattern tuple and value tuple have different arity" ); let mut args = Vec::new(); + let mut arrays = Vec::new(); let pat_value_tuples = pats.iter().zip(values.to_vec()); for (pat_id, value) in pat_value_tuples { // At this point we should no longer have control qubits so pass None. - let (mut element_args, None) = self + let (mut element_args, None, mut elem_arrays) = self .resolve_args( (store_pat_id.package, *pat_id).into(), value, @@ -3834,11 +3883,12 @@ impl<'a> PartialEvaluator<'a> { panic!("no control qubits are expected"); }; args.append(&mut element_args); + arrays.append(&mut elem_arrays); } - args + (args, arrays) } }; - Ok((args, ctls_arg)) + Ok((args, ctls_arg, arrays)) } fn try_eval_block(&mut self, block_id: BlockId) -> Result { @@ -4654,22 +4704,59 @@ impl<'a> PartialEvaluator<'a> { array_package_span: PackageSpan, array_elem_ty: &Ty, ) -> Result { - let array_literal = convert_to_array_literal(array, array_package_span, array_elem_ty)?; - let array_elem_ty = array_literal.ty; - - let const_array_id = if let Some(idx) = self - .program - .array_literals + let (array_operand, array_elem_ty) = if let Some((_, var_id)) = self + .eval_context + .get_current_scope() + .arrays .iter() - .position(|a| a == &array_literal) + .find(|(stored_array, _)| Rc::ptr_eq(stored_array, array)) { - idx + // The array being indexed matches one of the dynamic content arrays tracked as a locally constant argument + // to the current scope. This means we can emit an index instruction pointed at the variable created on entry + // to the current scope. + let Some(var_id) = var_id else { + panic!("array variable ID not found in current scope"); + }; + let Ok(rir::Ty::Prim(elem_rir_prim_ty)) = map_fir_type_to_rir_type(array_elem_ty) + else { + return Err(Error::Unexpected( + "array with non-primitive RIR type".to_string(), + array_package_span, + )); + }; + ( + Operand::Variable(rir::Variable { + variable_id: *var_id, + ty: rir::Ty::Array(array.len(), elem_rir_prim_ty), + }), + elem_rir_prim_ty, + ) } else { - let idx = self.program.array_literals.len(); - self.program.array_literals.push(array_literal); - idx - }; + // The array being indexed is not one tracked by the scope, so try to convert it into a static array literal + // that will be stored in the global section of the program. If it matches an existing array literal, we can + // reuse that identifier, otherwise a new one is generated and stored into the program. + // The index instruction will then be emitted to index into that array literal. + let array_literal = convert_to_array_literal(array, array_package_span, array_elem_ty)?; + let array_elem_ty = array_literal.ty; + + let const_array_id = if let Some(idx) = self + .program + .array_literals + .iter() + .position(|a| a == &array_literal) + { + idx + } else { + let idx = self.program.array_literals.len(); + self.program.array_literals.push(array_literal); + idx + }; + ( + Operand::Literal(Literal::Array(const_array_id)), + array_elem_ty, + ) + }; let variable_id = self.resource_manager.next_var(); let rir_variable = rir::Variable { variable_id, @@ -4677,7 +4764,7 @@ impl<'a> PartialEvaluator<'a> { }; self.get_current_rir_block_mut().0.push(Instruction::Index( - Operand::Literal(Literal::Array(const_array_id)), + array_operand, Operand::Variable(map_eval_var_to_rir_var(var)), rir_variable, )); @@ -5132,3 +5219,23 @@ fn convert_to_array_literal( ty: elem_rir_prim_ty, }) } + +/// Recursively traverse the given array to collect any arrays with dynamic contents (arrays that contain RIR variables), +/// including the given array itself, and collect any into a flat vector. This allows them to be tracked as locally +/// constant arrays used as arguments to the current scope. +fn get_arrays_with_dynamic_content(array: &Rc>) -> Vec>> { + let mut arrays_with_dynamic_content = Vec::new(); + for elem in array.iter() { + match elem { + Value::Array(inner) => { + arrays_with_dynamic_content.append(&mut get_arrays_with_dynamic_content(inner)); + } + Value::Var(_) => { + arrays_with_dynamic_content.push(Rc::clone(array)); + break; + } + _ => {} + } + } + arrays_with_dynamic_content +} diff --git a/source/compiler/qsc_rca/src/core.rs b/source/compiler/qsc_rca/src/core.rs index 5a4d6efe717..24f0213c583 100644 --- a/source/compiler/qsc_rca/src/core.rs +++ b/source/compiler/qsc_rca/src/core.rs @@ -182,12 +182,12 @@ impl<'a> Analyzer<'a> { let mut default_value_kind = ValueKind::Constant; // If we are within a dynamic scope, the compute kind of the assign index expression must be variable and an additional - // runtime feature is used to mark the array itself as dynamically sized. + // runtime feature is used to mark the array itself as dynamic. if !application_instance.active_dynamic_scopes.is_empty() { default_value_kind = ValueKind::Variable; replacement_value_compute_kind = replacement_value_compute_kind.aggregate(ComputeKind::Dynamic { - runtime_features: RuntimeFeatureFlags::UseOfDynamicallySizedArray, + runtime_features: RuntimeFeatureFlags::UseOfDynamicArray, value_kind: ValueKind::Constant, }); } @@ -829,8 +829,10 @@ impl<'a> Analyzer<'a> { // If the index expression is variable, the value kind of the expression is at least dynamic (possibly variable) // and an additional runtime feature is used. - if let ComputeKind::Dynamic { value_kind, .. } = &index_expr_compute_kind - && *value_kind == ValueKind::Variable + if let ComputeKind::Dynamic { + value_kind: ValueKind::Variable, + .. + } = &index_expr_compute_kind { let mut dynamic_runtime_features = RuntimeFeatureFlags::UseOfDynamicIndex; diff --git a/source/compiler/qsc_rca/src/tests/arrays.rs b/source/compiler/qsc_rca/src/tests/arrays.rs index a5d725755be..9d541d85818 100644 --- a/source/compiler/qsc_rca/src/tests/arrays.rs +++ b/source/compiler/qsc_rca/src/tests/arrays.rs @@ -375,7 +375,7 @@ fn check_rca_for_mutable_array_assign_index_in_dynamic_context() { &expect![[r#" ApplicationsGeneratorSet: inherent: Dynamic: - runtime_features: RuntimeFeatureFlags(UseOfDynamicallySizedArray) + runtime_features: RuntimeFeatureFlags(UseOfDynamicArray) value_kind: Variable dynamic_param_applications: "#]], ); @@ -400,7 +400,7 @@ fn check_rca_for_mutable_array_assign_index_dynamic_content_in_dynamic_context() &expect![[r#" ApplicationsGeneratorSet: inherent: Dynamic: - runtime_features: RuntimeFeatureFlags(UseOfDynamicallySizedArray | QubitAllocation) + runtime_features: RuntimeFeatureFlags(UseOfDynamicArray | QubitAllocation) value_kind: Variable dynamic_param_applications: "#]], ); @@ -425,7 +425,7 @@ fn check_rca_for_mutable_array_assign_index_dynamic_nested_array_content_in_dyna &expect![[r#" ApplicationsGeneratorSet: inherent: Dynamic: - runtime_features: RuntimeFeatureFlags(UseOfDynamicallySizedArray | QubitAllocation) + runtime_features: RuntimeFeatureFlags(UseOfDynamicArray | QubitAllocation) value_kind: Variable dynamic_param_applications: "#]], ); diff --git a/source/compiler/qsc_rir/src/passes/insert_alloca_load.rs b/source/compiler/qsc_rir/src/passes/insert_alloca_load.rs index 545755287d2..327244cf00b 100644 --- a/source/compiler/qsc_rir/src/passes/insert_alloca_load.rs +++ b/source/compiler/qsc_rir/src/passes/insert_alloca_load.rs @@ -89,6 +89,27 @@ fn add_alloca_load_to_block( *next_var_id = next_var_id.successor(); continue; } + Instruction::StoreArray(operands, var) => { + vars_to_alloca.insert(var.variable_id, *var); + let new_operands = operands + .iter() + .map(|operand| { + map_or_load_operand( + operand, + &mut var_map, + &mut block.0, + next_var_id, + should_load_operand(operand, vars_to_alloca), + ) + }) + .collect(); + block.0.push(Instruction::StoreArray(new_operands, *var)); + // Drop the cached load for this variable so a later read in this + // block reloads the freshly stored value instead of a stale one. + var_map.remove(&var.variable_id); + *next_var_id = next_var_id.successor(); + continue; + } // Replace any arguments with the new values of stored variables. Instruction::Call(_, args, _, _) => { diff --git a/source/compiler/qsc_rir/src/passes/prune_unneeded_stores.rs b/source/compiler/qsc_rir/src/passes/prune_unneeded_stores.rs index a7b31e81cfe..b2061d7a38a 100644 --- a/source/compiler/qsc_rir/src/passes/prune_unneeded_stores.rs +++ b/source/compiler/qsc_rir/src/passes/prune_unneeded_stores.rs @@ -99,6 +99,7 @@ fn process_callable(program: &mut Program, callable_id: CallableId) { } } +#[allow(clippy::too_many_lines)] fn check_var_usage( program: &mut Program, block_id: crate::rir::BlockId, @@ -115,6 +116,14 @@ fn check_var_usage( } stored_vars.insert(variable.variable_id); } + Instruction::StoreArray(operands, variable) => { + for operand in operands { + if let crate::rir::Operand::Variable(var) = operand { + used_vars.insert(var.variable_id); + } + } + stored_vars.insert(variable.variable_id); + } Instruction::Call(_, operands, variable, _) => { if let Some(var) = variable diff --git a/source/compiler/qsc_rir/src/passes/ssa_check.rs b/source/compiler/qsc_rir/src/passes/ssa_check.rs index d396a5f5f7b..7bd0eea96b8 100644 --- a/source/compiler/qsc_rir/src/passes/ssa_check.rs +++ b/source/compiler/qsc_rir/src/passes/ssa_check.rs @@ -252,7 +252,10 @@ fn get_variable_uses(program: &Program) -> IndexMap { + Instruction::StoreArray(..) + | Instruction::Alloca(..) + | Instruction::Load(..) + | Instruction::Index(..) => { panic!("Unexpected advanced instruction at {block_id:?}, instruction {idx}") } } diff --git a/source/compiler/qsc_rir/src/passes/type_check.rs b/source/compiler/qsc_rir/src/passes/type_check.rs index 0075e2d7f8e..05591dbdb66 100644 --- a/source/compiler/qsc_rir/src/passes/type_check.rs +++ b/source/compiler/qsc_rir/src/passes/type_check.rs @@ -81,6 +81,24 @@ fn check_instr_types(program: &Program, instr: &Instruction) { assert_eq!(index.get_type(), Ty::Prim(Prim::Integer)); } + Instruction::StoreArray(operands, var) => { + let Ty::Array(size, elem_ty) = &var.ty else { + panic!("expected variable to be of array type"); + }; + assert_eq!( + operands.len(), + *size, + "expected number of operands to match array size" + ); + for opr in operands { + assert_eq!( + opr.get_type(), + Ty::Prim(*elem_ty), + "expected operand type to match array element type" + ); + } + } + Instruction::Convert(_, _) | Instruction::Jump(_) | Instruction::Alloca(..) diff --git a/source/compiler/qsc_rir/src/rir.rs b/source/compiler/qsc_rir/src/rir.rs index 9c04582b93d..026199213f7 100644 --- a/source/compiler/qsc_rir/src/rir.rs +++ b/source/compiler/qsc_rir/src/rir.rs @@ -376,6 +376,7 @@ impl Display for FcmpConditionCode { #[derive(Clone, Debug)] pub enum Instruction { Store(Operand, Variable), + StoreArray(Vec, Variable), Call( CallableId, Vec, @@ -432,6 +433,9 @@ impl Display for Instruction { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match &self { Self::Store(value, variable) => write_unary_instruction(f, "Store", value, *variable)?, + Self::StoreArray(value, variable) => { + write_store_array_instruction(f, value, *variable)?; + } Self::Jump(block_id) => write!(f, "Jump({})", block_id.0)?, Self::Call(callable_id, args, variable, metadata) => { write_call(f, *callable_id, args, *variable, metadata.as_deref())?; @@ -786,6 +790,23 @@ impl PartialEq for ArrayLiteral { } } +fn write_store_array_instruction( + f: &mut Formatter, + value: &[Operand], + variable: Variable, +) -> fmt::Result { + let mut indent = set_indentation(indented(f), 0); + write!(indent, "{variable} = StoreArray [")?; + for (index, operand) in value.iter().enumerate() { + write!(indent, "{operand}")?; + if index != value.len() - 1 { + write!(indent, ", ")?; + } + } + write!(indent, "]")?; + Ok(()) +} + fn write_binary_instruction( f: &mut Formatter, instruction: &str, diff --git a/source/compiler/qsc_rir/src/utils.rs b/source/compiler/qsc_rir/src/utils.rs index 07a63d7bed9..08dfab3ec47 100644 --- a/source/compiler/qsc_rir/src/utils.rs +++ b/source/compiler/qsc_rir/src/utils.rs @@ -110,6 +110,7 @@ pub fn get_variable_assignments(program: &Program) -> IndexMap { @@ -155,6 +156,18 @@ pub(crate) fn map_variable_use_in_block( continue; } } + Instruction::StoreArray(operand, var) => { + if var_stor_to_keep.contains(&var.variable_id) { + // Only keep stores to variables that are in the set to keep. + *operand = operand + .iter() + .map(|op| op.mapped(var_map)) + .collect::>(); + } else { + // Otherwise drop the store array by continuing the loop. + continue; + } + } // Replace any arguments with the new values of stored variables. Instruction::Call(_, args, _, _) => {