-
Notifications
You must be signed in to change notification settings - Fork 406
Expand file tree
/
Copy pathremove_if_else.rs
More file actions
398 lines (362 loc) · 14.6 KB
/
Copy pathremove_if_else.rs
File metadata and controls
398 lines (362 loc) · 14.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
use std::collections::hash_map::Entry;
use fxhash::FxHashMap as HashMap;
use crate::ssa::ir::function::RuntimeType;
use crate::ssa::ir::instruction::Hint;
use crate::ssa::ir::value::ValueId;
use crate::ssa::{
Ssa,
ir::{
dfg::DataFlowGraph,
function::Function,
instruction::{Instruction, Intrinsic},
types::Type,
value::Value,
},
opt::flatten_cfg::value_merger::ValueMerger,
};
impl Ssa {
/// This pass removes `inc_rc` and `dec_rc` instructions
/// as long as there are no `array_set` instructions to an array
/// of the same type in between.
///
/// Note that this pass is very conservative since the array_set
/// instruction does not need to be to the same array. This is because
/// the given array may alias another array (e.g. function parameters or
/// a `load`ed array from a reference).
#[tracing::instrument(level = "trace", skip(self))]
pub(crate) fn remove_if_else(mut self) -> Ssa {
for function in self.functions.values_mut() {
function.remove_if_else();
}
self
}
}
impl Function {
pub(crate) fn remove_if_else(&mut self) {
// This should match the check in flatten_cfg
if matches!(self.runtime(), RuntimeType::Brillig(_)) {
// skip
} else {
Context::default().remove_if_else(self);
}
#[cfg(debug_assertions)]
remove_if_else_post_check(self);
}
}
#[derive(Default)]
struct Context {
slice_sizes: HashMap<ValueId, u32>,
}
impl Context {
fn remove_if_else(&mut self, function: &mut Function) {
let block = function.entry_block();
// Make sure this optimization runs when there's only one block
assert_eq!(function.dfg[block].successors().count(), 0);
function.simple_reachable_blocks_optimization(|context| {
let instruction_id = context.instruction_id;
let instruction = context.instruction();
match instruction {
Instruction::IfElse { then_condition, then_value, else_condition, else_value } => {
let then_condition = *then_condition;
let else_condition = *else_condition;
let then_value = *then_value;
let else_value = *else_value;
let typ = context.dfg.type_of_value(then_value);
assert!(!matches!(typ, Type::Numeric(_)));
let call_stack = context.dfg.get_instruction_call_stack_id(instruction_id);
let mut value_merger =
ValueMerger::new(context.dfg, block, &mut self.slice_sizes, call_stack);
let value = value_merger.merge_values(
then_condition,
else_condition,
then_value,
else_value,
);
let _typ = context.dfg.type_of_value(value);
let results = context.dfg.instruction_results(instruction_id);
let result = results[0];
// let result = match typ {
// Type::Array(..) => results[0],
// Type::Slice(..) => results[1],
// other => unreachable!("IfElse instructions should only have arrays or slices at this point. Found {other:?}"),
// };
context.remove_current_instruction();
context.replace_value(result, value);
}
Instruction::Call { func, arguments } => {
if let Value::Intrinsic(intrinsic) = context.dfg[*func] {
let results = context.dfg.instruction_results(instruction_id);
match slice_capacity_change(context.dfg, intrinsic, arguments, results) {
SizeChange::None => (),
SizeChange::SetTo(value, new_capacity) => {
self.slice_sizes.insert(value, new_capacity);
}
SizeChange::Inc { old, new } => {
let old_capacity = self.get_or_find_capacity(context.dfg, old);
self.slice_sizes.insert(new, old_capacity + 1);
}
SizeChange::Dec { old, new } => {
let old_capacity = self.get_or_find_capacity(context.dfg, old);
// We use a saturating sub here as calling `pop_front` or `pop_back` on a zero-length slice
// would otherwise underflow.
self.slice_sizes.insert(new, old_capacity.saturating_sub(1));
}
}
}
}
Instruction::ArraySet { array, .. } => {
let results = context.dfg.instruction_results(instruction_id);
let result = if results.len() == 2 { results[1] } else { results[0] };
let old_capacity = self.get_or_find_capacity(context.dfg, *array);
self.slice_sizes.insert(result, old_capacity);
}
_ => (),
}
});
}
fn get_or_find_capacity(&mut self, dfg: &DataFlowGraph, value: ValueId) -> u32 {
match self.slice_sizes.entry(value) {
Entry::Occupied(entry) => return *entry.get(),
Entry::Vacant(entry) => {
if let Some((array, typ)) = dfg.get_array_constant(value) {
let length = array.len() / typ.element_types().len();
return *entry.insert(length as u32);
}
if let Type::Array(_, length) = dfg.type_of_value(value) {
return *entry.insert(length);
}
}
}
let dbg_value = &dfg[value];
unreachable!("No size for slice {value} = {dbg_value:?}")
}
}
enum SizeChange {
None,
SetTo(ValueId, u32),
// These two variants store the old and new slice ids
// not their lengths which should be old_len = new_len +/- 1
Inc { old: ValueId, new: ValueId },
Dec { old: ValueId, new: ValueId },
}
/// Find the change to a slice's capacity an instruction would have
fn slice_capacity_change(
dfg: &DataFlowGraph,
intrinsic: Intrinsic,
arguments: &[ValueId],
results: &[ValueId],
) -> SizeChange {
match intrinsic {
Intrinsic::SlicePushBack | Intrinsic::SlicePushFront | Intrinsic::SliceInsert => {
// Expecting: len, slice = ...
assert_eq!(results.len(), 2);
let old = arguments[1];
let new = results[1];
assert!(matches!(dfg.type_of_value(old), Type::Slice(_)));
assert!(matches!(dfg.type_of_value(new), Type::Slice(_)));
SizeChange::Inc { old, new }
}
Intrinsic::SlicePopBack | Intrinsic::SliceRemove => {
let old = arguments[1];
let new = results[1];
assert!(matches!(dfg.type_of_value(old), Type::Slice(_)));
assert!(matches!(dfg.type_of_value(new), Type::Slice(_)));
SizeChange::Dec { old, new }
}
Intrinsic::SlicePopFront => {
let old = arguments[1];
let new = results[results.len() - 1];
assert!(matches!(dfg.type_of_value(old), Type::Slice(_)));
assert!(matches!(dfg.type_of_value(new), Type::Slice(_)));
SizeChange::Dec { old, new }
}
Intrinsic::AsSlice => {
assert_eq!(arguments.len(), 1);
assert_eq!(results.len(), 2);
let length = match dfg.type_of_value(arguments[0]) {
Type::Array(_, length) => length,
other => unreachable!("slice_capacity_change expected array, found {other:?}"),
};
assert!(matches!(dfg.type_of_value(results[1]), Type::Slice(_)));
SizeChange::SetTo(results[1], length)
}
// These cases don't affect slice capacities
Intrinsic::AssertConstant
| Intrinsic::StaticAssert
| Intrinsic::ApplyRangeConstraint
| Intrinsic::ArrayLen
| Intrinsic::ArrayAsStrUnchecked
| Intrinsic::StrAsBytes
| Intrinsic::BlackBox(_)
| Intrinsic::Hint(Hint::BlackBox)
| Intrinsic::AsWitness
| Intrinsic::IsUnconstrained
| Intrinsic::DerivePedersenGenerators
| Intrinsic::ToBits(_)
| Intrinsic::ToRadix(_)
| Intrinsic::ArrayRefCount
| Intrinsic::SliceRefCount
| Intrinsic::FieldLessThan => SizeChange::None,
}
}
/// Post-check condition for [Function::remove_if_else].
///
/// Succeeds if:
/// - `func` is a Brillig function, OR
/// - `func` does not contain any if-else instructions.
///
/// Otherwise panics.
#[cfg(debug_assertions)]
fn remove_if_else_post_check(func: &Function) {
// Brillig functions should be unaffected.
if func.runtime().is_brillig() {
return;
}
// Otherwise there should be no if-else instructions in any reachable block.
for block_id in func.reachable_blocks() {
let instruction_ids = func.dfg[block_id].instructions();
for instruction_id in instruction_ids {
if matches!(func.dfg[*instruction_id], Instruction::IfElse { .. }) {
panic!("IfElse instruction still remains in ACIR function");
}
}
}
}
#[cfg(test)]
mod tests {
use crate::{assert_ssa_snapshot, ssa::ssa_gen::Ssa};
#[test]
fn merge_basic_arrays() {
// This is the flattened SSA for the following Noir logic:
// ```
// fn main(x: bool, mut y: [u32; 2]) {
// if x {
// y[0] = 1;
// y[1] = 2;
// }
//
// let z = y[0] + y[1];
// assert(z == 3);
// }
// ```
let src = "
acir(inline) predicate_pure fn main f0 {
b0(v0: u1, v1: [u32; 2]):
v2 = allocate -> &mut [u32; 2]
enable_side_effects v0
v5 = array_set v1, index u32 0, value u32 1
v7 = array_set v5, index u32 1, value u32 2
v8 = not v0
v9 = if v0 then v7 else (if v8) v1
enable_side_effects u1 1
v11 = array_get v9, index u32 0 -> u32
v12 = array_get v9, index u32 1 -> u32
v13 = add v11, v12
v15 = eq v13, u32 3
constrain v13 == u32 3
return
}
";
let mut ssa = Ssa::from_str(src).unwrap();
ssa = ssa.remove_if_else();
// In case our if block is never activated, we need to fetch each value from the original array.
// We then should create a new array where each value can be mapped to `(then_condition * then_value) + (!then_condition * else_value)`.
// The `then_value` and `else_value` for an array will be every element of the array. Thus, we should see array_get operations
// on the original array as well as the new values we are writing to the array.
assert_ssa_snapshot!(ssa, @r"
acir(inline) predicate_pure fn main f0 {
b0(v0: u1, v1: [u32; 2]):
v2 = allocate -> &mut [u32; 2]
enable_side_effects v0
v5 = array_set v1, index u32 0, value u32 1
v7 = array_set v5, index u32 1, value u32 2
v8 = not v0
v9 = array_get v1, index u32 0 -> u32
v10 = cast v0 as u32
v11 = cast v8 as u32
v12 = unchecked_mul v11, v9
v13 = unchecked_add v10, v12
v14 = array_get v1, index u32 1 -> u32
v15 = cast v0 as u32
v16 = cast v8 as u32
v17 = unchecked_mul v15, u32 2
v18 = unchecked_mul v16, v14
v19 = unchecked_add v17, v18
v20 = make_array [v13, v19] : [u32; 2]
enable_side_effects u1 1
v22 = array_get v20, index u32 0 -> u32
v23 = array_get v20, index u32 1 -> u32
v24 = add v22, v23
v26 = eq v24, u32 3
constrain v24 == u32 3
return
}
");
}
#[test]
fn try_merge_only_changed_indices() {
// This is the flattened SSA for the following Noir logic:
// ```
// fn main(x: bool, mut y: [u32; 2]) {
// if x {
// y[0] = 1;
// }
//
// let z = y[0] + y[1];
// assert(z == 1);
// }
// ```
let src = "
acir(inline) predicate_pure fn main f0 {
b0(v0: u1, v1: [u32; 2]):
v2 = allocate -> &mut [u32; 2]
enable_side_effects v0
v5 = array_set v1, index u32 0, value u32 1
v6 = not v0
v7 = if v0 then v5 else (if v6) v1
enable_side_effects u1 1
v9 = array_get v7, index u32 0 -> u32
v10 = array_get v7, index u32 1 -> u32
v11 = add v9, v10
v12 = eq v11, u32 1
constrain v11 == u32 1
return
}
";
let mut ssa = Ssa::from_str(src).unwrap();
ssa = ssa.remove_if_else();
// We attempt to optimize array mergers to only handle where an array was modified,
// rather than merging the entire array. As we only modify the `y` array at a single index,
// we instead only map the if predicate onto the the numeric value we are looking to write,
// and then write into the array directly.
assert_ssa_snapshot!(ssa, @r"
acir(inline) predicate_pure fn main f0 {
b0(v0: u1, v1: [u32; 2]):
v2 = allocate -> &mut [u32; 2]
enable_side_effects v0
v5 = array_set v1, index u32 0, value u32 1
v6 = not v0
v7 = array_get v1, index u32 0 -> u32
v8 = cast v0 as u32
v9 = cast v6 as u32
v10 = unchecked_mul v9, v7
v11 = unchecked_add v8, v10
v12 = array_get v5, index u32 1 -> u32
v13 = array_get v1, index u32 1 -> u32
v14 = cast v0 as u32
v15 = cast v6 as u32
v16 = unchecked_mul v14, v12
v17 = unchecked_mul v15, v13
v18 = unchecked_add v16, v17
v19 = make_array [v11, v18] : [u32; 2]
enable_side_effects u1 1
v21 = array_get v19, index u32 0 -> u32
v22 = array_get v19, index u32 1 -> u32
v23 = add v21, v22
v24 = eq v23, u32 1
constrain v23 == u32 1
return
}
");
}
}