Skip to content

Commit 03b9e71

Browse files
AztecBotsirasistantTomAFrench
authored
feat: Sync from noir (#8653)
Automated pull of development from the [noir](https://github.com/noir-lang/noir) programming language, a dependency of Aztec. BEGIN_COMMIT_OVERRIDE fix: Allow macros to change types on each iteration of a comptime loop (noir-lang/noir#6105) chore: Schnorr signature verification in Noir (noir-lang/noir#5437) feat: Implement solver for mov_registers_to_registers (noir-lang/noir#6089) END_COMMIT_OVERRIDE --------- Co-authored-by: sirasistant <sirasistant@gmail.com> Co-authored-by: TomAFrench <tom@tomfren.ch>
1 parent 48a715b commit 03b9e71

13 files changed

Lines changed: 435 additions & 137 deletions

File tree

.noir-sync-commit

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1df102a1ee0eb39dcbada50e10b226c7f7be0f26
1+
0864e7c945089cc06f8cc9e5c7d933c465d8c892
Lines changed: 275 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,290 @@
11
use acvm::{acir::brillig::MemoryAddress, AcirField};
2+
use fxhash::{FxHashMap as HashMap, FxHashSet as HashSet};
23

34
use super::{debug_show::DebugToString, registers::RegisterAllocator, BrilligContext};
45

56
impl<F: AcirField + DebugToString, Registers: RegisterAllocator> BrilligContext<F, Registers> {
67
/// This function moves values from a set of registers to another set of registers.
7-
/// It first moves all sources to new allocated registers to avoid overwriting.
8+
/// The only requirement is that every destination needs to be written at most once.
89
pub(crate) fn codegen_mov_registers_to_registers(
910
&mut self,
1011
sources: Vec<MemoryAddress>,
1112
destinations: Vec<MemoryAddress>,
1213
) {
13-
let new_sources: Vec<_> = sources
14-
.iter()
15-
.map(|source| {
16-
let new_source = self.allocate_register();
17-
self.mov_instruction(new_source, *source);
18-
new_source
19-
})
14+
assert_eq!(sources.len(), destinations.len());
15+
// Remove all no-ops
16+
let movements: Vec<_> = sources
17+
.into_iter()
18+
.zip(destinations)
19+
.filter(|(source, destination)| source != destination)
2020
.collect();
21-
for (new_source, destination) in new_sources.iter().zip(destinations.iter()) {
22-
self.mov_instruction(*destination, *new_source);
23-
self.deallocate_register(*new_source);
21+
22+
// Now we need to detect all cycles.
23+
// First build a map of the movements. Note that a source could have multiple destinations
24+
let mut movements_map: HashMap<MemoryAddress, HashSet<_>> =
25+
movements.into_iter().fold(HashMap::default(), |mut map, (source, destination)| {
26+
map.entry(source).or_default().insert(destination);
27+
map
28+
});
29+
30+
let destinations_set: HashSet<_> = movements_map.values().flatten().copied().collect();
31+
assert_eq!(
32+
destinations_set.len(),
33+
movements_map.values().flatten().count(),
34+
"Multiple moves to the same register found"
35+
);
36+
37+
let mut loop_detector = LoopDetector::default();
38+
loop_detector.collect_loops(&movements_map);
39+
let loops = loop_detector.loops;
40+
// In order to break the loops we need to store one register from each in a temporary and then use that temporary as source.
41+
let mut temporaries = Vec::with_capacity(loops.len());
42+
for loop_found in loops {
43+
let temp_register = self.allocate_register();
44+
temporaries.push(temp_register);
45+
let first_source = loop_found.iter().next().unwrap();
46+
self.mov_instruction(temp_register, *first_source);
47+
let destinations_of_temp = movements_map.remove(first_source).unwrap();
48+
movements_map.insert(temp_register, destinations_of_temp);
49+
}
50+
// After removing loops we should have an DAG with each node having only one ancestor (but could have multiple successors)
51+
// Now we should be able to move the registers just by performing a DFS on the movements map
52+
let heads: Vec<_> = movements_map
53+
.keys()
54+
.filter(|source| !destinations_set.contains(source))
55+
.copied()
56+
.collect();
57+
for head in heads {
58+
self.perform_movements(&movements_map, head);
59+
}
60+
61+
// Deallocate all temporaries
62+
for temp in temporaries {
63+
self.deallocate_register(temp);
2464
}
2565
}
66+
67+
fn perform_movements(
68+
&mut self,
69+
movements: &HashMap<MemoryAddress, HashSet<MemoryAddress>>,
70+
current_source: MemoryAddress,
71+
) {
72+
if let Some(destinations) = movements.get(&current_source) {
73+
for destination in destinations {
74+
self.perform_movements(movements, *destination);
75+
}
76+
for destination in destinations {
77+
self.mov_instruction(*destination, current_source);
78+
}
79+
}
80+
}
81+
}
82+
83+
#[derive(Default)]
84+
struct LoopDetector {
85+
visited_sources: HashSet<MemoryAddress>,
86+
loops: Vec<im::OrdSet<MemoryAddress>>,
87+
}
88+
89+
impl LoopDetector {
90+
fn collect_loops(&mut self, movements: &HashMap<MemoryAddress, HashSet<MemoryAddress>>) {
91+
for source in movements.keys() {
92+
self.find_loop_recursive(*source, movements, im::OrdSet::default());
93+
}
94+
}
95+
96+
fn find_loop_recursive(
97+
&mut self,
98+
source: MemoryAddress,
99+
movements: &HashMap<MemoryAddress, HashSet<MemoryAddress>>,
100+
mut previous_sources: im::OrdSet<MemoryAddress>,
101+
) {
102+
if self.visited_sources.contains(&source) {
103+
return;
104+
}
105+
// Mark as visited
106+
self.visited_sources.insert(source);
107+
108+
previous_sources.insert(source);
109+
// Get all destinations
110+
if let Some(destinations) = movements.get(&source) {
111+
for destination in destinations {
112+
if previous_sources.contains(destination) {
113+
// Found a loop
114+
let loop_sources = previous_sources.clone();
115+
self.loops.push(loop_sources);
116+
} else {
117+
self.find_loop_recursive(*destination, movements, previous_sources.clone());
118+
}
119+
}
120+
}
121+
}
122+
}
123+
124+
#[cfg(test)]
125+
mod tests {
126+
use acvm::{
127+
acir::brillig::{MemoryAddress, Opcode},
128+
FieldElement,
129+
};
130+
use fxhash::{FxHashMap as HashMap, FxHashSet as HashSet};
131+
132+
use crate::{
133+
brillig::brillig_ir::{artifact::Label, registers::Stack, BrilligContext},
134+
ssa::ir::function::FunctionId,
135+
};
136+
137+
// Tests for the loop finder
138+
139+
fn generate_movements_map(
140+
movements: Vec<(usize, usize)>,
141+
) -> HashMap<MemoryAddress, HashSet<MemoryAddress>> {
142+
movements.into_iter().fold(HashMap::default(), |mut map, (source, destination)| {
143+
map.entry(MemoryAddress(source)).or_default().insert(MemoryAddress(destination));
144+
map
145+
})
146+
}
147+
148+
#[test]
149+
fn test_loop_detector_basic_loop() {
150+
let movements = vec![(0, 1), (1, 2), (2, 3), (3, 0)];
151+
let movements_map = generate_movements_map(movements);
152+
let mut loop_detector = super::LoopDetector::default();
153+
loop_detector.collect_loops(&movements_map);
154+
assert_eq!(loop_detector.loops.len(), 1);
155+
assert_eq!(loop_detector.loops[0].len(), 4);
156+
}
157+
158+
#[test]
159+
fn test_loop_detector_no_loop() {
160+
let movements = vec![(0, 1), (1, 2), (2, 3), (3, 4)];
161+
let movements_map = generate_movements_map(movements);
162+
let mut loop_detector = super::LoopDetector::default();
163+
loop_detector.collect_loops(&movements_map);
164+
assert_eq!(loop_detector.loops.len(), 0);
165+
}
166+
167+
#[test]
168+
fn test_loop_detector_loop_with_branch() {
169+
let movements = vec![(0, 1), (1, 2), (2, 0), (0, 3), (3, 4)];
170+
let movements_map = generate_movements_map(movements);
171+
let mut loop_detector = super::LoopDetector::default();
172+
loop_detector.collect_loops(&movements_map);
173+
assert_eq!(loop_detector.loops.len(), 1);
174+
assert_eq!(loop_detector.loops[0].len(), 3);
175+
}
176+
177+
#[test]
178+
fn test_loop_detector_two_loops() {
179+
let movements = vec![(0, 1), (1, 2), (2, 0), (3, 4), (4, 5), (5, 3)];
180+
let movements_map = generate_movements_map(movements);
181+
let mut loop_detector = super::LoopDetector::default();
182+
loop_detector.collect_loops(&movements_map);
183+
assert_eq!(loop_detector.loops.len(), 2);
184+
assert_eq!(loop_detector.loops[0].len(), 3);
185+
assert_eq!(loop_detector.loops[1].len(), 3);
186+
}
187+
188+
// Tests for mov_registers_to_registers
189+
190+
fn movements_to_source_and_destinations(
191+
movements: Vec<(usize, usize)>,
192+
) -> (Vec<MemoryAddress>, Vec<MemoryAddress>) {
193+
let sources = movements.iter().map(|(source, _)| MemoryAddress::from(*source)).collect();
194+
let destinations =
195+
movements.iter().map(|(_, destination)| MemoryAddress::from(*destination)).collect();
196+
(sources, destinations)
197+
}
198+
199+
pub(crate) fn create_context() -> BrilligContext<FieldElement, Stack> {
200+
let mut context = BrilligContext::new(true);
201+
context.enter_context(Label::function(FunctionId::test_new(0)));
202+
context
203+
}
204+
205+
#[test]
206+
#[should_panic(expected = "Multiple moves to the same register found")]
207+
fn test_mov_registers_to_registers_overwrite() {
208+
let movements = vec![(10, 11), (12, 11), (10, 13)];
209+
let (sources, destinations) = movements_to_source_and_destinations(movements);
210+
let mut context = create_context();
211+
212+
context.codegen_mov_registers_to_registers(sources, destinations);
213+
}
214+
215+
#[test]
216+
fn test_mov_registers_to_registers_no_loop() {
217+
let movements = vec![(10, 11), (11, 12), (12, 13), (13, 14)];
218+
let (sources, destinations) = movements_to_source_and_destinations(movements);
219+
let mut context = create_context();
220+
221+
context.codegen_mov_registers_to_registers(sources, destinations);
222+
let opcodes = context.artifact().byte_code;
223+
assert_eq!(
224+
opcodes,
225+
vec![
226+
Opcode::Mov { destination: MemoryAddress(14), source: MemoryAddress(13) },
227+
Opcode::Mov { destination: MemoryAddress(13), source: MemoryAddress(12) },
228+
Opcode::Mov { destination: MemoryAddress(12), source: MemoryAddress(11) },
229+
Opcode::Mov { destination: MemoryAddress(11), source: MemoryAddress(10) },
230+
]
231+
);
232+
}
233+
#[test]
234+
fn test_mov_registers_to_registers_no_op_filter() {
235+
let movements = vec![(10, 11), (11, 11), (11, 12)];
236+
let (sources, destinations) = movements_to_source_and_destinations(movements);
237+
let mut context = create_context();
238+
239+
context.codegen_mov_registers_to_registers(sources, destinations);
240+
let opcodes = context.artifact().byte_code;
241+
assert_eq!(
242+
opcodes,
243+
vec![
244+
Opcode::Mov { destination: MemoryAddress(12), source: MemoryAddress(11) },
245+
Opcode::Mov { destination: MemoryAddress(11), source: MemoryAddress(10) },
246+
]
247+
);
248+
}
249+
250+
#[test]
251+
fn test_mov_registers_to_registers_loop() {
252+
let movements = vec![(10, 11), (11, 12), (12, 13), (13, 10)];
253+
let (sources, destinations) = movements_to_source_and_destinations(movements);
254+
let mut context = create_context();
255+
256+
context.codegen_mov_registers_to_registers(sources, destinations);
257+
let opcodes = context.artifact().byte_code;
258+
assert_eq!(
259+
opcodes,
260+
vec![
261+
Opcode::Mov { destination: MemoryAddress(3), source: MemoryAddress(10) },
262+
Opcode::Mov { destination: MemoryAddress(10), source: MemoryAddress(13) },
263+
Opcode::Mov { destination: MemoryAddress(13), source: MemoryAddress(12) },
264+
Opcode::Mov { destination: MemoryAddress(12), source: MemoryAddress(11) },
265+
Opcode::Mov { destination: MemoryAddress(11), source: MemoryAddress(3) }
266+
]
267+
);
268+
}
269+
270+
#[test]
271+
fn test_mov_registers_to_registers_loop_and_branch() {
272+
let movements = vec![(10, 11), (11, 12), (12, 10), (10, 13), (13, 14)];
273+
let (sources, destinations) = movements_to_source_and_destinations(movements);
274+
let mut context = create_context();
275+
276+
context.codegen_mov_registers_to_registers(sources, destinations);
277+
let opcodes = context.artifact().byte_code;
278+
assert_eq!(
279+
opcodes,
280+
vec![
281+
Opcode::Mov { destination: MemoryAddress(3), source: MemoryAddress(10) }, // Temporary
282+
Opcode::Mov { destination: MemoryAddress(14), source: MemoryAddress(13) }, // Branch
283+
Opcode::Mov { destination: MemoryAddress(10), source: MemoryAddress(12) }, // Loop
284+
Opcode::Mov { destination: MemoryAddress(12), source: MemoryAddress(11) }, // Loop
285+
Opcode::Mov { destination: MemoryAddress(13), source: MemoryAddress(3) }, // Finish branch
286+
Opcode::Mov { destination: MemoryAddress(11), source: MemoryAddress(3) } // Finish loop
287+
]
288+
);
289+
}
26290
}

noir/noir-repo/compiler/noirc_frontend/src/elaborator/types.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -633,7 +633,7 @@ impl<'context> Elaborator<'context> {
633633
0
634634
}
635635

636-
pub fn unify(
636+
pub(super) fn unify(
637637
&mut self,
638638
actual: &Type,
639639
expected: &Type,
@@ -644,6 +644,22 @@ impl<'context> Elaborator<'context> {
644644
}
645645
}
646646

647+
/// Do not apply type bindings even after a successful unification.
648+
/// This function is used by the interpreter for some comptime code
649+
/// which can change types e.g. on each iteration of a for loop.
650+
pub fn unify_without_applying_bindings(
651+
&mut self,
652+
actual: &Type,
653+
expected: &Type,
654+
file: fm::FileId,
655+
make_error: impl FnOnce() -> TypeCheckError,
656+
) {
657+
let mut bindings = TypeBindings::new();
658+
if actual.try_unify(expected, &mut bindings).is_err() {
659+
self.errors.push((make_error().into(), file));
660+
}
661+
}
662+
647663
/// Wrapper of Type::unify_with_coercions using self.errors
648664
pub(super) fn unify_with_coercions(
649665
&mut self,

noir/noir-repo/compiler/noirc_frontend/src/hir/comptime/interpreter.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,9 +1303,11 @@ impl<'local, 'interner> Interpreter<'local, 'interner> {
13031303
// Macro calls are typed as type variables during type checking.
13041304
// Now that we know the type we need to further unify it in case there
13051305
// are inconsistencies or the type needs to be known.
1306+
// We don't commit any type bindings made this way in case the type of
1307+
// the macro result changes across loop iterations.
13061308
let expected_type = self.elaborator.interner.id_type(id);
13071309
let actual_type = result.get_type();
1308-
self.unify(&actual_type, &expected_type, location);
1310+
self.unify_without_binding(&actual_type, &expected_type, location);
13091311
}
13101312
Ok(result)
13111313
}
@@ -1319,16 +1321,14 @@ impl<'local, 'interner> Interpreter<'local, 'interner> {
13191321
}
13201322
}
13211323

1322-
fn unify(&mut self, actual: &Type, expected: &Type, location: Location) {
1323-
// We need to swap out the elaborator's file since we may be
1324-
// in a different one currently, and it uses that for the error location.
1325-
let old_file = std::mem::replace(&mut self.elaborator.file, location.file);
1326-
self.elaborator.unify(actual, expected, || TypeCheckError::TypeMismatch {
1327-
expected_typ: expected.to_string(),
1328-
expr_typ: actual.to_string(),
1329-
expr_span: location.span,
1324+
fn unify_without_binding(&mut self, actual: &Type, expected: &Type, location: Location) {
1325+
self.elaborator.unify_without_applying_bindings(actual, expected, location.file, || {
1326+
TypeCheckError::TypeMismatch {
1327+
expected_typ: expected.to_string(),
1328+
expr_typ: actual.to_string(),
1329+
expr_span: location.span,
1330+
}
13301331
});
1331-
self.elaborator.file = old_file;
13321332
}
13331333

13341334
fn evaluate_method_call(

0 commit comments

Comments
 (0)