Skip to content

Commit d7f8962

Browse files
authored
Mutable variables in dynamic branches prevent full constant folding in partial evaluation (#2089)
This fixes the bug by having partial evaluation more explicitly track different variable mappings to literals across branches and recombining those mappings that match (ie: are constant) when all branches are done. This also includes partial eval and RIR SSA pass fixes to correctly support immutable and mutable copies of dynamic variables. New test cases for several combinations of constant folding at partial eval are included, as well as a new test case confirming RIR SSA fix. Fixes #2087
1 parent 189523e commit d7f8962

File tree

14 files changed

+1206
-393
lines changed

14 files changed

+1206
-393
lines changed

compiler/qsc/src/codegen/tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,8 +1230,8 @@ mod adaptive_ri_profile {
12301230
block_2:
12311231
br label %block_3
12321232
block_3:
1233-
%var_3 = phi i64 [0, %block_1], [1, %block_2]
1234-
call void @__quantum__rt__int_record_output(i64 %var_3, i8* null)
1233+
%var_4 = phi i64 [0, %block_1], [1, %block_2]
1234+
call void @__quantum__rt__int_record_output(i64 %var_4, i8* null)
12351235
ret void
12361236
}
12371237

compiler/qsc_partial_eval/src/evaluation_context.rs

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,10 @@ pub struct Scope {
9696
pub args_value_kind: Vec<ValueKind>,
9797
/// The classical environment of the callable, which holds values corresponding to local variables.
9898
pub env: Env,
99-
// Consider optimizing `hybrid_vars` and `mutable_vars` by removing them and enlightening the evaluator on how to
100-
// properly handle `Value::Var`, which could be either `static` or `dynamic`.
10199
/// Map that holds the values of local variables.
102100
hybrid_vars: FxHashMap<LocalVarId, Value>,
103-
/// Maps variable IDs to mutable variables, which contain their current kind.
104-
mutable_vars: FxHashMap<VariableId, MutableKind>,
101+
/// Maps variable IDs to static literal values, if any.
102+
static_vars: FxHashMap<VariableId, Literal>,
105103
/// Number of currently active blocks (starting from where this scope was created).
106104
active_block_count: usize,
107105
}
@@ -161,18 +159,37 @@ impl Scope {
161159
env,
162160
active_block_count: 1,
163161
hybrid_vars,
164-
mutable_vars: FxHashMap::default(),
162+
static_vars: FxHashMap::default(),
165163
}
166164
}
167165

168-
/// Gets a mutable variable.
169-
pub fn find_mutable_kind(&self, var_id: VariableId) -> Option<&MutableKind> {
170-
self.mutable_vars.get(&var_id)
166+
/// Gets the static literal value for the given variable.
167+
pub fn get_static_value(&self, var_id: VariableId) -> Option<&Literal> {
168+
self.static_vars.get(&var_id)
171169
}
172170

173-
/// Gets a mutable mutable variable.
174-
pub fn find_mutable_var_mut(&mut self, var_id: VariableId) -> Option<&mut MutableKind> {
175-
self.mutable_vars.get_mut(&var_id)
171+
/// Removes the static literal value for the given variable.
172+
pub fn remove_static_value(&mut self, var_id: VariableId) {
173+
self.static_vars.remove(&var_id);
174+
}
175+
176+
/// Clones the static literal value mappings, which allows callers to cache the mappings across branches.
177+
pub fn clone_static_var_mappings(&self) -> FxHashMap<VariableId, Literal> {
178+
self.static_vars.clone()
179+
}
180+
181+
/// Sets the static literal value mappings to the given mapping, overwriting any existing mappings.
182+
pub fn set_static_var_mappings(&mut self, static_vars: FxHashMap<VariableId, Literal>) {
183+
self.static_vars = static_vars;
184+
}
185+
186+
/// Keeps only the static literal value mappings that are also present in the provided other mapping.
187+
pub fn keep_matching_static_var_mappings(
188+
&mut self,
189+
other_mappings: &FxHashMap<VariableId, Literal>,
190+
) {
191+
self.static_vars
192+
.retain(|var_id, lit| Some(&*lit) == other_mappings.get(var_id));
176193
}
177194

178195
/// Gets the value of a hybrid local variable.
@@ -197,11 +214,8 @@ impl Scope {
197214
}
198215

199216
// Insert a variable into the mutable variables map.
200-
pub fn insert_mutable_var(&mut self, var_id: VariableId, mutable_kind: MutableKind) {
201-
let Entry::Vacant(vacant) = self.mutable_vars.entry(var_id) else {
202-
panic!("mutable variable should not already exist");
203-
};
204-
vacant.insert(mutable_kind);
217+
pub fn insert_static_var_mapping(&mut self, var_id: VariableId, literal: Literal) {
218+
self.static_vars.insert(var_id, literal);
205219
}
206220

207221
/// Determines whether we are currently evaluating a branch within the scope.
@@ -317,9 +331,3 @@ fn map_eval_value_to_value_kind(value: &Value) -> ValueKind {
317331
| Value::String(_) => ValueKind::Element(RuntimeKind::Static),
318332
}
319333
}
320-
321-
#[derive(Clone, Copy, Debug)]
322-
pub enum MutableKind {
323-
Static(Literal),
324-
Dynamic,
325-
}

compiler/qsc_partial_eval/src/lib.rs

Lines changed: 72 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ mod management;
1313

1414
use core::panic;
1515
use evaluation_context::{
16-
Arg, BlockNode, BranchControlFlow, EvalControlFlow, EvaluationContext, MutableKind, Scope,
16+
Arg, BlockNode, BranchControlFlow, EvalControlFlow, EvaluationContext, Scope,
1717
};
1818
use management::{QuantumIntrinsicsChecker, ResourceManager};
1919
use miette::Diagnostic;
@@ -51,7 +51,7 @@ use qsc_rir::{
5151
builder,
5252
rir::{
5353
self, Callable, CallableId, CallableType, ConditionCode, Instruction, Literal, Operand,
54-
Program,
54+
Program, VariableId,
5555
},
5656
};
5757
use rustc_hash::FxHashMap;
@@ -204,8 +204,19 @@ impl<'a> PartialEvaluator<'a> {
204204
fn bind_value_to_ident(&mut self, mutability: Mutability, ident: &Ident, value: Value) {
205205
// We do slightly different things depending on the mutability of the identifier.
206206
match mutability {
207-
Mutability::Immutable => self.bind_value_to_immutable_ident(ident, value),
208207
Mutability::Mutable => self.bind_value_to_mutable_ident(ident, value),
208+
Mutability::Immutable => {
209+
let current_scope = self.eval_context.get_current_scope();
210+
if matches!(value, Value::Var(var) if current_scope.get_static_value(var.id.into()).is_none())
211+
{
212+
// An immutable identifier is being bound to a dynamic value, so treat the identifier as mutable.
213+
// This allows it to represent a point-in-time copy of the mutable value during evaluation.
214+
self.bind_value_to_mutable_ident(ident, value);
215+
} else {
216+
// The value is static, so bind it to the classical map.
217+
self.bind_value_to_immutable_ident(ident, value);
218+
}
219+
}
209220
};
210221
}
211222

@@ -226,11 +237,13 @@ impl<'a> PartialEvaluator<'a> {
226237
}
227238

228239
// Always bind the value to the hybrid map but do it differently depending of the value type.
229-
if let Some((var_id, mutable_kind)) = self.try_create_mutable_variable(ident.id, &value) {
230-
// Keep track of whether the mutable variable is static or dynamic.
231-
self.eval_context
232-
.get_current_scope_mut()
233-
.insert_mutable_var(var_id, mutable_kind);
240+
if let Some((var_id, literal)) = self.try_create_mutable_variable(ident.id, &value) {
241+
// If the variable maps to a know static literal, track that mapping.
242+
if let Some(literal) = literal {
243+
self.eval_context
244+
.get_current_scope_mut()
245+
.insert_static_var_mapping(var_id, literal);
246+
}
234247
} else {
235248
self.bind_value_in_hybrid_map(ident, value);
236249
}
@@ -1510,6 +1523,8 @@ impl<'a> PartialEvaluator<'a> {
15101523
};
15111524

15121525
// Evaluate the body expression.
1526+
// First, we cache the current static variable mappings so that we can restore them later.
1527+
let cached_mappings = self.clone_current_static_var_map();
15131528
let if_true_branch_control_flow =
15141529
self.eval_expr_if_branch(body_expr_id, continuation_block_node_id, maybe_if_expr_var)?;
15151530
let if_true_block_id = match if_true_branch_control_flow {
@@ -1519,16 +1534,28 @@ impl<'a> PartialEvaluator<'a> {
15191534

15201535
// Evaluate the otherwise expression (if any), and determine the block to branch to if the condition is false.
15211536
let if_false_block_id = if let Some(otherwise_expr_id) = otherwise_expr_id {
1537+
// Cache the mappings after the true block so we can compare afterwards.
1538+
let post_if_true_mappings = self.clone_current_static_var_map();
1539+
// Restore the cached mappings from before evaluating the true block.
1540+
self.overwrite_current_static_var_map(cached_mappings);
15221541
let if_false_branch_control_flow = self.eval_expr_if_branch(
15231542
otherwise_expr_id,
15241543
continuation_block_node_id,
15251544
maybe_if_expr_var,
15261545
)?;
1546+
// Only keep the static mappings that are the same in both blocks; when they are different,
1547+
// the variable is no longer static across the if expression.
1548+
self.keep_matching_static_var_mappings(&post_if_true_mappings);
15271549
match if_false_branch_control_flow {
15281550
BranchControlFlow::Block(block_id) => block_id,
15291551
BranchControlFlow::Return(value) => return Ok(EvalControlFlow::Return(value)),
15301552
}
15311553
} else {
1554+
// Only keep the static mappings that are the same after the true block as before; when they are different,
1555+
// the variable is no longer static across the if expression.
1556+
self.keep_matching_static_var_mappings(&cached_mappings);
1557+
1558+
// Since there is no otherwise block, we branch to the continuation block.
15321559
continuation_block_node_id
15331560
};
15341561

@@ -1814,9 +1841,7 @@ impl<'a> PartialEvaluator<'a> {
18141841
// the variable if it is static at this moment.
18151842
if let Value::Var(var) = bound_value {
18161843
let current_scope = self.eval_context.get_current_scope();
1817-
if let Some(MutableKind::Static(literal)) =
1818-
current_scope.find_mutable_kind(var.id.into())
1819-
{
1844+
if let Some(literal) = current_scope.get_static_value(var.id.into()) {
18201845
map_rir_literal_to_eval_value(*literal)
18211846
} else {
18221847
bound_value.clone()
@@ -2229,7 +2254,7 @@ impl<'a> PartialEvaluator<'a> {
22292254
&mut self,
22302255
local_var_id: LocalVarId,
22312256
value: &Value,
2232-
) -> Option<(rir::VariableId, MutableKind)> {
2257+
) -> Option<(rir::VariableId, Option<Literal>)> {
22332258
// Check if we can create a mutable variable for this value.
22342259
let var_ty = try_get_eval_var_type(value)?;
22352260

@@ -2249,13 +2274,13 @@ impl<'a> PartialEvaluator<'a> {
22492274
let store_ins = Instruction::Store(value_operand, rir_var);
22502275
self.get_current_rir_block_mut().0.push(store_ins);
22512276

2252-
// Create a mutable variable.
2253-
let mutable_kind = match value_operand {
2254-
Operand::Literal(literal) => MutableKind::Static(literal),
2255-
Operand::Variable(_) => MutableKind::Dynamic,
2277+
// Create a mutable variable, mapping it to the static value if any.
2278+
let static_value = match value_operand {
2279+
Operand::Literal(literal) => Some(literal),
2280+
Operand::Variable(_) => None,
22562281
};
22572282

2258-
Some((var_id, mutable_kind))
2283+
Some((var_id, static_value))
22592284
}
22602285

22612286
fn get_or_insert_callable(&mut self, callable: Callable) -> CallableId {
@@ -2625,14 +2650,16 @@ impl<'a> PartialEvaluator<'a> {
26252650

26262651
// If this is a mutable variable, make sure to update whether it is static or dynamic.
26272652
let current_scope = self.eval_context.get_current_scope_mut();
2628-
if matches!(rhs_operand, Operand::Variable(_))
2629-
|| current_scope.is_currently_evaluating_branch()
2630-
{
2631-
if let Some(mutable_kind) = current_scope.find_mutable_var_mut(rir_var.variable_id)
2632-
{
2633-
*mutable_kind = MutableKind::Dynamic;
2653+
match rhs_operand {
2654+
Operand::Literal(literal) => {
2655+
// The variable maps to a static literal here, so track that literal value.
2656+
current_scope.insert_static_var_mapping(rir_var.variable_id, literal);
26342657
}
2635-
}
2658+
Operand::Variable(_) => {
2659+
// The variable is not known to be some literal value, so remove the static mapping.
2660+
current_scope.remove_static_value(rir_var.variable_id);
2661+
}
2662+
};
26362663
} else {
26372664
// Verify that we are not updating a value that does not have a backing variable from a dynamic branch
26382665
// because it is unsupported.
@@ -2918,6 +2945,27 @@ impl<'a> PartialEvaluator<'a> {
29182945
_ => panic!("{value} cannot be mapped to a RIR operand"),
29192946
}
29202947
}
2948+
2949+
fn clone_current_static_var_map(&self) -> FxHashMap<VariableId, Literal> {
2950+
self.eval_context
2951+
.get_current_scope()
2952+
.clone_static_var_mappings()
2953+
}
2954+
2955+
fn overwrite_current_static_var_map(&mut self, static_vars: FxHashMap<VariableId, Literal>) {
2956+
self.eval_context
2957+
.get_current_scope_mut()
2958+
.set_static_var_mappings(static_vars);
2959+
}
2960+
2961+
fn keep_matching_static_var_mappings(
2962+
&mut self,
2963+
other_mappings: &FxHashMap<VariableId, Literal>,
2964+
) {
2965+
self.eval_context
2966+
.get_current_scope_mut()
2967+
.keep_matching_static_var_mappings(other_mappings);
2968+
}
29212969
}
29222970

29232971
fn eval_un_op_with_literals(un_op: UnOp, value: Value) -> Value {

0 commit comments

Comments
 (0)