From ec75e8ec59e0f2a2169aea67372411ede4074d09 Mon Sep 17 00:00:00 2001 From: guipublic <47281315+guipublic@users.noreply.github.com> Date: Thu, 24 Oct 2024 23:19:27 +0200 Subject: [PATCH 01/30] feat: remove 'single use' intermediate variables (#6268) # Description ## Problem\* Resolves #6085 ## Summary\* This PR tries to benefit from Barretenberg's 'big-add gates' support, which was enabled by PR https://github.com/AztecProtocol/aztec-packages/pull/8960 It's a simple optimisation which removes intermediate variables usually created by the CSatTransformer if they are not re-used elsewhere. The PR assumes that the backend is able to handle infinite width, but still requires the CSatTransformer, which is not really consistent. I plan to make follow-up PRs to get rid of this (but I can't guarantee it will work). ## Additional Context I tested the optimisation on all 'execution_sucess' test cases. In most cases, there were no change at all, while in some cases we would win one or two (10 max) on the circuit size. However, in a few cases, listed below in the form "test case: circuit size with 'intermediate var' optimisation vs no optimisation", it can be more significant: 7_function: 2955 vs 2992 bit_shifts_runtime: 5451 vs 5761 eddsa: 65805 vs 70406 hashmap: 135023 vs 150661 nested_array_dynamic: 12594 vs 12922 nested_array_in_slice: 5371 vs 5449 poseidon_bn254_hash: 1028 vs 1060 poseidon_bn254_hash_width_3: 1028 vs 1495 poseidonsponge_x5_254: 1244 vs 1307 regression_5252: 76491 vs 83862 sha256_var_size_regression: 74093 vs 74529 sha2_byte: 93998 vs 94006 slice_dynamic_index: 6308 vs 6419 slices: 3835 vs 3874 to_be_bytes: 135 vs 143 to_bytes_consistent: 6 vs 51 to_bytes_integration: 434 vs 484 u128: 4662 vs 4707 u16_support: 3023 vs 3057 ## Documentation\* Check one: - [X] No documentation needed. - [ ] Documentation included in this PR. - [ ] **[For Experimental Features]** Documentation to be submitted in a separate PR. # PR Checklist\* - [X] I have tested the changes locally. - [X] I have formatted the changes with [Prettier](https://prettier.io/) and/or `cargo fmt` on default settings. Co-authored-by: Tom French <15848336+TomAFrench@users.noreply.github.com> --- .../opcodes/black_box_function_call.rs | 12 ++ .../compiler/optimizers/merge_expressions.rs | 202 ++++++++++++++++++ acvm-repo/acvm/src/compiler/optimizers/mod.rs | 2 + .../acvm/src/compiler/transformers/mod.rs | 16 +- 4 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 acvm-repo/acvm/src/compiler/optimizers/merge_expressions.rs diff --git a/acvm-repo/acir/src/circuit/opcodes/black_box_function_call.rs b/acvm-repo/acir/src/circuit/opcodes/black_box_function_call.rs index e06286d179e..7e76530b3ca 100644 --- a/acvm-repo/acir/src/circuit/opcodes/black_box_function_call.rs +++ b/acvm-repo/acir/src/circuit/opcodes/black_box_function_call.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use crate::native_types::Witness; use crate::{AcirField, BlackBoxFunc}; @@ -389,6 +391,16 @@ impl BlackBoxFuncCall { BlackBoxFuncCall::BigIntToLeBytes { outputs, .. } => outputs.to_vec(), } } + + pub fn get_input_witnesses(&self) -> HashSet { + let mut result = HashSet::new(); + for input in self.get_inputs_vec() { + if let ConstantOrWitnessEnum::Witness(w) = input.input() { + result.insert(w); + } + } + result + } } const ABBREVIATION_LIMIT: usize = 5; diff --git a/acvm-repo/acvm/src/compiler/optimizers/merge_expressions.rs b/acvm-repo/acvm/src/compiler/optimizers/merge_expressions.rs new file mode 100644 index 00000000000..4291b997fde --- /dev/null +++ b/acvm-repo/acvm/src/compiler/optimizers/merge_expressions.rs @@ -0,0 +1,202 @@ +use std::collections::{HashMap, HashSet}; + +use acir::{ + circuit::{brillig::BrilligInputs, directives::Directive, opcodes::BlockId, Circuit, Opcode}, + native_types::{Expression, Witness}, + AcirField, +}; + +pub(crate) struct MergeExpressionsOptimizer { + resolved_blocks: HashMap>, +} + +impl MergeExpressionsOptimizer { + pub(crate) fn new() -> Self { + MergeExpressionsOptimizer { resolved_blocks: HashMap::new() } + } + /// This pass analyzes the circuit and identifies intermediate variables that are + /// only used in two gates. It then merges the gate that produces the + /// intermediate variable into the second one that uses it + /// Note: This pass is only relevant for backends that can handle unlimited width + pub(crate) fn eliminate_intermediate_variable( + &mut self, + circuit: &Circuit, + acir_opcode_positions: Vec, + ) -> (Vec>, Vec) { + // Keep track, for each witness, of the gates that use it + let circuit_inputs = circuit.circuit_arguments(); + self.resolved_blocks = HashMap::new(); + let mut used_witness: HashMap> = HashMap::new(); + for (i, opcode) in circuit.opcodes.iter().enumerate() { + let witnesses = self.witness_inputs(opcode); + if let Opcode::MemoryInit { block_id, .. } = opcode { + self.resolved_blocks.insert(*block_id, witnesses.clone()); + } + for w in witnesses { + // We do not simplify circuit inputs + if !circuit_inputs.contains(&w) { + used_witness.entry(w).or_default().insert(i); + } + } + } + + let mut modified_gates: HashMap> = HashMap::new(); + let mut new_circuit = Vec::new(); + let mut new_acir_opcode_positions = Vec::new(); + // For each opcode, try to get a target opcode to merge with + for (i, opcode) in circuit.opcodes.iter().enumerate() { + if !matches!(opcode, Opcode::AssertZero(_)) { + new_circuit.push(opcode.clone()); + new_acir_opcode_positions.push(acir_opcode_positions[i]); + continue; + } + let opcode = modified_gates.get(&i).unwrap_or(opcode).clone(); + let mut to_keep = true; + let input_witnesses = self.witness_inputs(&opcode); + for w in input_witnesses.clone() { + let empty_gates = HashSet::new(); + let gates_using_w = used_witness.get(&w).unwrap_or(&empty_gates); + // We only consider witness which are used in exactly two arithmetic gates + if gates_using_w.len() == 2 { + let gates_using_w: Vec<_> = gates_using_w.iter().collect(); + let mut b = *gates_using_w[1]; + if b == i { + b = *gates_using_w[0]; + } else { + // sanity check + assert!(i == *gates_using_w[0]); + } + let second_gate = modified_gates.get(&b).unwrap_or(&circuit.opcodes[b]).clone(); + if let (Opcode::AssertZero(expr_define), Opcode::AssertZero(expr_use)) = + (opcode.clone(), second_gate) + { + if let Some(expr) = Self::merge(&expr_use, &expr_define, w) { + // sanity check + assert!(i < b); + modified_gates.insert(b, Opcode::AssertZero(expr)); + to_keep = false; + // Update the 'used_witness' map to account for the merge. + for w2 in Self::expr_wit(&expr_define) { + if !circuit_inputs.contains(&w2) { + let mut v = used_witness[&w2].clone(); + v.insert(b); + v.remove(&i); + used_witness.insert(w2, v); + } + } + // We need to stop here and continue with the next opcode + // because the merge invalidate the current opcode + break; + } + } + } + } + + if to_keep { + if modified_gates.contains_key(&i) { + new_circuit.push(modified_gates[&i].clone()); + } else { + new_circuit.push(opcode.clone()); + } + new_acir_opcode_positions.push(acir_opcode_positions[i]); + } + } + (new_circuit, new_acir_opcode_positions) + } + + fn expr_wit(expr: &Expression) -> HashSet { + let mut result = HashSet::new(); + result.extend(expr.mul_terms.iter().flat_map(|i| vec![i.1, i.2])); + result.extend(expr.linear_combinations.iter().map(|i| i.1)); + result + } + + fn brillig_input_wit(&self, input: &BrilligInputs) -> HashSet { + let mut result = HashSet::new(); + match input { + BrilligInputs::Single(expr) => { + result.extend(Self::expr_wit(expr)); + } + BrilligInputs::Array(exprs) => { + for expr in exprs { + result.extend(Self::expr_wit(expr)); + } + } + BrilligInputs::MemoryArray(block_id) => { + let witnesses = self.resolved_blocks.get(block_id).expect("Unknown block id"); + result.extend(witnesses); + } + } + result + } + + // Returns the input witnesses used by the opcode + fn witness_inputs(&self, opcode: &Opcode) -> HashSet { + let mut witnesses = HashSet::new(); + match opcode { + Opcode::AssertZero(expr) => Self::expr_wit(expr), + Opcode::BlackBoxFuncCall(bb_func) => bb_func.get_input_witnesses(), + Opcode::Directive(Directive::ToLeRadix { a, .. }) => Self::expr_wit(a), + Opcode::MemoryOp { block_id: _, op, predicate } => { + //index et value, et predicate + let mut witnesses = HashSet::new(); + witnesses.extend(Self::expr_wit(&op.index)); + witnesses.extend(Self::expr_wit(&op.value)); + if let Some(p) = predicate { + witnesses.extend(Self::expr_wit(p)); + } + witnesses + } + + Opcode::MemoryInit { block_id: _, init, block_type: _ } => { + init.iter().cloned().collect() + } + Opcode::BrilligCall { inputs, .. } => { + for i in inputs { + witnesses.extend(self.brillig_input_wit(i)); + } + witnesses + } + Opcode::Call { id: _, inputs, outputs: _, predicate } => { + for i in inputs { + witnesses.insert(*i); + } + if let Some(p) = predicate { + witnesses.extend(Self::expr_wit(p)); + } + witnesses + } + } + } + + // Merge 'expr' into 'target' via Gaussian elimination on 'w' + // Returns None if the expressions cannot be merged + fn merge( + target: &Expression, + expr: &Expression, + w: Witness, + ) -> Option> { + // Check that the witness is not part of multiplication terms + for m in &target.mul_terms { + if m.1 == w || m.2 == w { + return None; + } + } + for m in &expr.mul_terms { + if m.1 == w || m.2 == w { + return None; + } + } + + for k in &target.linear_combinations { + if k.1 == w { + for i in &expr.linear_combinations { + if i.1 == w { + return Some(target.add_mul(-(k.0 / i.0), expr)); + } + } + } + } + None + } +} diff --git a/acvm-repo/acvm/src/compiler/optimizers/mod.rs b/acvm-repo/acvm/src/compiler/optimizers/mod.rs index e20ad97a108..1947a80dc35 100644 --- a/acvm-repo/acvm/src/compiler/optimizers/mod.rs +++ b/acvm-repo/acvm/src/compiler/optimizers/mod.rs @@ -5,10 +5,12 @@ use acir::{ // mod constant_backpropagation; mod general; +mod merge_expressions; mod redundant_range; mod unused_memory; pub(crate) use general::GeneralOptimizer; +pub(crate) use merge_expressions::MergeExpressionsOptimizer; pub(crate) use redundant_range::RangeOptimizer; use tracing::info; diff --git a/acvm-repo/acvm/src/compiler/transformers/mod.rs b/acvm-repo/acvm/src/compiler/transformers/mod.rs index 4e29681cbed..b11d054a57b 100644 --- a/acvm-repo/acvm/src/compiler/transformers/mod.rs +++ b/acvm-repo/acvm/src/compiler/transformers/mod.rs @@ -10,7 +10,9 @@ mod csat; pub(crate) use csat::CSatTransformer; pub use csat::MIN_EXPRESSION_WIDTH; -use super::{transform_assert_messages, AcirTransformationMap}; +use super::{ + optimizers::MergeExpressionsOptimizer, transform_assert_messages, AcirTransformationMap, +}; /// Applies [`ProofSystemCompiler`][crate::ProofSystemCompiler] specific optimizations to a [`Circuit`]. pub fn transform( @@ -166,6 +168,16 @@ pub(super) fn transform_internal( // The transformer does not add new public inputs ..acir }; - + let mut merge_optimizer = MergeExpressionsOptimizer::new(); + let (opcodes, new_acir_opcode_positions) = + merge_optimizer.eliminate_intermediate_variable(&acir, new_acir_opcode_positions); + // n.b. we do not update current_witness_index after the eliminate_intermediate_variable pass, the real index could be less. + let acir = Circuit { + current_witness_index, + expression_width, + opcodes, + // The optimizer does not add new public inputs + ..acir + }; (acir, new_acir_opcode_positions) } From 600ffeb05cd2538604570f3e2aa1b2e560e577c3 Mon Sep 17 00:00:00 2001 From: Tom French Date: Thu, 24 Oct 2024 22:47:40 +0100 Subject: [PATCH 02/30] chore: add some tests for type aliases --- compiler/noirc_frontend/src/tests.rs | 1 + compiler/noirc_frontend/src/tests/aliases.rs | 52 ++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 compiler/noirc_frontend/src/tests/aliases.rs diff --git a/compiler/noirc_frontend/src/tests.rs b/compiler/noirc_frontend/src/tests.rs index 286986e5d61..e06881127fd 100644 --- a/compiler/noirc_frontend/src/tests.rs +++ b/compiler/noirc_frontend/src/tests.rs @@ -1,5 +1,6 @@ #![cfg(test)] +mod aliases; mod bound_checks; mod imports; mod metaprogramming; diff --git a/compiler/noirc_frontend/src/tests/aliases.rs b/compiler/noirc_frontend/src/tests/aliases.rs new file mode 100644 index 00000000000..2ca460ca635 --- /dev/null +++ b/compiler/noirc_frontend/src/tests/aliases.rs @@ -0,0 +1,52 @@ +use super::assert_no_errors; + +#[test] +fn allows_usage_of_type_alias_as_argument_type() { + let src = r#" + type Foo = Field; + + fn accepts_a_foo(x: Foo) { + assert_eq(x, 42); + } + + fn main() { + accepts_a_foo(42); + } + "#; + assert_no_errors(src); +} + +#[test] +fn allows_usage_of_type_alias_as_return_type() { + let src = r#" + type Foo = Field; + + fn returns_a_foo() -> Foo { + 42 + } + + fn main() { + let _ = returns_a_foo(); + } + "#; + assert_no_errors(src); +} + +// This is a regression test for https://github.com/noir-lang/noir/issues/6347 +#[test] +#[should_panic = r#"ResolverError(Expected { span: Span(Span { start: ByteIndex(95), end: ByteIndex(98) }), expected: "type", got: "type alias" }"#] +fn allows_destructuring_a_type_alias_of_a_struct() { + let src = r#" + struct Foo { + inner: Field + } + + type Bar = Foo; + + fn main() { + let Bar { inner } = Foo { inner: 42 }; + assert_eq(inner, 42); + } + "#; + assert_no_errors(src); +} From 81c612f281cddf41d12ea62d9f610eab05ad1973 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Fri, 25 Oct 2024 09:56:57 +0100 Subject: [PATCH 03/30] feat(perf): Use [u32;16] for message block in sha256 (#6324) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description ## Problem\* Resolves #6304 Follows https://github.com/noir-lang/noir/pull/6318 ## Summary\* Changes the `MSG_BLOCK` type in `sha256.nr` from `[u8; 64]` to `[u32; 16]` to: * save a step of having to convert before compression * reduce the number of array writes (which currently incur copying) in favour of more arithmetic operations (byte packing) ## Additional Context ### Testing ``` cargo test -p nargo_cli --test stdlib-tests -- run_stdlib_tests sha256 cargo test -p nargo_cli --test stdlib-props fuzz_sha256 ``` NB we can run e.g. `-- run_stdlib_tests msg_just_under_block` to execute a specific test. ### Benchmarks ```shell cargo bench -p nargo_cli --bench criterion sha256_long ``` On my machine it shows that it got moderately faster: ```console ❯ cargo bench -p nargo_cli --bench criterion sha256_long ... bench_sha256_long_execute time: [1.3413 ms 1.3477 ms 1.3555 ms] change: [-13.718% -13.172% -12.577%] (p = 0.00 < 0.05) Performance has improved. Found 2 outliers among 20 measurements (10.00%) 2 (10.00%) high mild bench_sha256_long_execute_brillig time: [244.52 µs 259.88 µs 280.67 µs] change: [-26.479% -23.096% -20.123%] (p = 0.00 < 0.05) Performance has improved. Found 4 outliers among 20 measurements (20.00%) 4 (20.00%) low severe ``` ## Documentation\* Check one: - [x] No documentation needed. - [ ] Documentation included in this PR. - [ ] **[For Experimental Features]** Documentation to be submitted in a separate PR. # PR Checklist\* - [x] I have tested the changes locally. - [ ] I have formatted the changes with [Prettier](https://prettier.io/) and/or `cargo fmt` on default settings. --- noir_stdlib/src/hash/sha256.nr | 429 +++++++++++++++++++++--- tooling/nargo_cli/tests/stdlib-tests.rs | 2 +- 2 files changed, 377 insertions(+), 54 deletions(-) diff --git a/noir_stdlib/src/hash/sha256.nr b/noir_stdlib/src/hash/sha256.nr index b5d2624d3e7..a3ac0b9e5da 100644 --- a/noir_stdlib/src/hash/sha256.nr +++ b/noir_stdlib/src/hash/sha256.nr @@ -12,6 +12,20 @@ global MSG_SIZE_PTR = 56; // Size of the message block when packed as 4-byte integer array. global INT_BLOCK_SIZE = 16; +// A `u32` integer consists of 4 bytes. +global INT_SIZE = 4; + +// Index of the integer in the `INT_BLOCK` where the length is written. +global INT_SIZE_PTR = MSG_SIZE_PTR / INT_SIZE; + +// Magic numbers for bit shifting. +// Works with actual bit shifting as well as the compiler turns them into * and / +// but circuit execution appears to be 10% faster this way. +global TWO_POW_8 = 256; +global TWO_POW_16 = TWO_POW_8 * 256; +global TWO_POW_24 = TWO_POW_16 * 256; +global TWO_POW_32 = TWO_POW_24 as u64 * 256; + // Index of a byte in a 64 byte block; ie. 0..=63 type BLOCK_BYTE_PTR = u32; @@ -19,8 +33,8 @@ type BLOCK_BYTE_PTR = u32; type INT_BLOCK = [u32; INT_BLOCK_SIZE]; // A message block is a slice of the original message of a fixed size, -// potentially padded with zeroes. -type MSG_BLOCK = [u8; BLOCK_SIZE]; +// potentially padded with zeros, with neighbouring 4 bytes packed into integers. +type MSG_BLOCK = INT_BLOCK; // The hash is 32 bytes. type HASH = [u8; 32]; @@ -50,16 +64,19 @@ pub fn digest(msg: [u8; N]) -> HASH { pub fn sha256_var(msg: [u8; N], message_size: u64) -> HASH { let message_size = message_size as u32; let num_blocks = N / BLOCK_SIZE; - let mut msg_block: MSG_BLOCK = [0; BLOCK_SIZE]; + let mut msg_block: MSG_BLOCK = [0; INT_BLOCK_SIZE]; + // Intermediate hash, starting with the canonical initial value let mut h: STATE = [ 1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225, - ]; // Intermediate hash, starting with the canonical initial value - let mut msg_byte_ptr = 0; // Pointer into msg_block + ]; + // Pointer into msg_block on a 64 byte scale + let mut msg_byte_ptr = 0; for i in 0..num_blocks { let msg_start = BLOCK_SIZE * i; let (new_msg_block, new_msg_byte_ptr) = unsafe { build_msg_block(msg, message_size, msg_start) }; + if msg_start < message_size { msg_block = new_msg_block; } @@ -77,7 +94,7 @@ pub fn sha256_var(msg: [u8; N], message_size: u64) -> HASH { // If the block is filled, compress it. // An un-filled block is handled after this loop. if (msg_start < message_size) & (msg_byte_ptr == BLOCK_SIZE) { - h = sha256_compression(msg_u8_to_u32(msg_block), h); + h = sha256_compression(msg_block, h); } } @@ -114,49 +131,39 @@ pub fn sha256_var(msg: [u8; N], message_size: u64) -> HASH { // Pad the rest such that we have a [u32; 2] block at the end representing the length // of the message, and a block of 1 0 ... 0 following the message (i.e. [1 << 7, 0, ..., 0]). // Here we rely on the fact that everything beyond the available input is set to 0. - msg_block[msg_byte_ptr] = 1 << 7; - let last_block = msg_block; + msg_block = update_block_item( + msg_block, + msg_byte_ptr, + |msg_item| set_item_byte_then_zeros(msg_item, msg_byte_ptr, 1 << 7), + ); msg_byte_ptr = msg_byte_ptr + 1; + let last_block = msg_block; // If we don't have room to write the size, compress the block and reset it. if msg_byte_ptr > MSG_SIZE_PTR { - h = sha256_compression(msg_u8_to_u32(msg_block), h); + h = sha256_compression(msg_block, h); // `attach_len_to_msg_block` will zero out everything after the `msg_byte_ptr`. msg_byte_ptr = 0; } msg_block = unsafe { attach_len_to_msg_block(msg_block, msg_byte_ptr, message_size) }; - if !crate::runtime::is_unconstrained() { + if !is_unconstrained() { verify_msg_len(msg_block, last_block, msg_byte_ptr, message_size); } hash_final_block(msg_block, h) } -// Convert 64-byte array to array of 16 u32s -fn msg_u8_to_u32(msg: MSG_BLOCK) -> INT_BLOCK { - let mut msg32: INT_BLOCK = [0; INT_BLOCK_SIZE]; - - for i in 0..INT_BLOCK_SIZE { - let mut msg_field: Field = 0; - for j in 0..4 { - msg_field = msg_field * 256 + msg[64 - 4 * (i + 1) + j] as Field; - } - msg32[15 - i] = msg_field as u32; - } - - msg32 -} - // Take `BLOCK_SIZE` number of bytes from `msg` starting at `msg_start`. -// Returns the block and the length that has been copied rather than padded with zeroes. +// Returns the block and the length that has been copied rather than padded with zeros. unconstrained fn build_msg_block( msg: [u8; N], message_size: u32, msg_start: u32, ) -> (MSG_BLOCK, BLOCK_BYTE_PTR) { - let mut msg_block: MSG_BLOCK = [0; BLOCK_SIZE]; + let mut msg_block: MSG_BLOCK = [0; INT_BLOCK_SIZE]; + // We insert `BLOCK_SIZE` bytes (or up to the end of the message) let block_input = if msg_start + BLOCK_SIZE > message_size { if message_size < msg_start { @@ -169,29 +176,73 @@ unconstrained fn build_msg_block( } else { BLOCK_SIZE }; - for k in 0..block_input { - msg_block[k] = msg[msg_start + k]; + + // Figure out the number of items in the int array that we have to pack. + // e.g. if the input is [0,1,2,3,4,5] then we need to pack it as 2 items: [0123, 4500] + let mut int_input = block_input / INT_SIZE; + if block_input % INT_SIZE != 0 { + int_input = int_input + 1; + }; + + for i in 0..int_input { + let mut msg_item: u32 = 0; + // Always construct the integer as 4 bytes, even if it means going beyond the input. + for j in 0..INT_SIZE { + let k = i * INT_SIZE + j; + let msg_byte = if k < block_input { + msg[msg_start + k] + } else { + 0 + }; + msg_item = lshift8(msg_item, 1) + msg_byte as u32; + } + msg_block[i] = msg_item; } + + // Returning the index as if it was a 64 byte array. + // We have to project it down to 16 items and bit shifting to get a byte back if we need it. (msg_block, block_input) } // Verify the block we are compressing was appropriately constructed by `build_msg_block` // and matches the input data. Returns the index of the first unset item. +// If `message_size` is less than `msg_start` then this is called with the old non-empty block; +// in that case we can skip verification, ie. no need to check that everything is zero. fn verify_msg_block( msg: [u8; N], message_size: u32, msg_block: MSG_BLOCK, msg_start: u32, ) -> BLOCK_BYTE_PTR { - let mut msg_byte_ptr: u32 = 0; // Message byte pointer + let mut msg_byte_ptr = 0; let mut msg_end = msg_start + BLOCK_SIZE; if msg_end > N { msg_end = N; } + // We might have to go beyond the input to pad the fields. + if msg_end % INT_SIZE != 0 { + msg_end = msg_end + INT_SIZE - msg_end % INT_SIZE; + } - for k in msg_start..msg_end { - if k < message_size { - assert_eq(msg_block[msg_byte_ptr], msg[k]); + // Reconstructed packed item. + let mut msg_item: u32 = 0; + + // Inclusive at the end so that we can compare the last item. + let mut i: u32 = 0; + for k in msg_start..=msg_end { + if k % INT_SIZE == 0 { + // If we consumed some input we can compare against the block. + if (msg_start < message_size) & (k > msg_start) { + assert_eq(msg_block[i], msg_item as u32); + i = i + 1; + msg_item = 0; + } + } + // Shift the accumulator + msg_item = lshift8(msg_item, 1); + // If we have input to consume, add it at the rightmost position. + if k < message_size & k < msg_end { + msg_item = msg_item + msg[k] as u32; msg_byte_ptr = msg_byte_ptr + 1; } } @@ -199,69 +250,261 @@ fn verify_msg_block( msg_byte_ptr } -// Verify the block we are compressing was appropriately padded with zeroes by `build_msg_block`. +// Verify the block we are compressing was appropriately padded with zeros by `build_msg_block`. // This is only relevant for the last, potentially partially filled block. fn verify_msg_block_padding(msg_block: MSG_BLOCK, msg_byte_ptr: BLOCK_BYTE_PTR) { + // Check all the way to the end of the block. + verify_msg_block_zeros(msg_block, msg_byte_ptr, INT_BLOCK_SIZE); +} + +// Verify that a region of ints in the message block are (partially) zeroed, +// up to an (exclusive) maximum which can either be the end of the block +// or just where the size is to be written. +fn verify_msg_block_zeros( + msg_block: MSG_BLOCK, + mut msg_byte_ptr: BLOCK_BYTE_PTR, + max_int_byte_ptr: u32, +) { // This variable is used to get around the compiler under-constrained check giving a warning. // We want to check against a constant zero, but if it does not come from the circuit inputs // or return values the compiler check will issue a warning. let zero = msg_block[0] - msg_block[0]; - for i in 0..BLOCK_SIZE { - if i >= msg_byte_ptr { + // First integer which is supposed to be (partially) zero. + let mut int_byte_ptr = msg_byte_ptr / INT_SIZE; + + // Check partial zeros. + let modulo = msg_byte_ptr % INT_SIZE; + if modulo != 0 { + let zeros = INT_SIZE - modulo; + let mask = if zeros == 3 { + TWO_POW_24 + } else if zeros == 2 { + TWO_POW_16 + } else { + TWO_POW_8 + }; + assert_eq(msg_block[int_byte_ptr] % mask, zero); + int_byte_ptr = int_byte_ptr + 1; + } + + // Check the rest of the items. + for i in 0..max_int_byte_ptr { + if i >= int_byte_ptr { assert_eq(msg_block[i], zero); } } } +// Verify that up to the byte pointer the two blocks are equal. +// At the byte pointer the new block can be partially zeroed. +fn verify_msg_block_equals_last( + msg_block: MSG_BLOCK, + last_block: MSG_BLOCK, + mut msg_byte_ptr: BLOCK_BYTE_PTR, +) { + // msg_byte_ptr is the position at which they are no longer have to be the same. + // First integer which is supposed to be (partially) zero contains that pointer. + let mut int_byte_ptr = msg_byte_ptr / INT_SIZE; + + // Check partial zeros. + let modulo = msg_byte_ptr % INT_SIZE; + if modulo != 0 { + // Reconstruct the partially zero item from the last block. + let last_field = last_block[int_byte_ptr]; + let mut msg_item: u32 = 0; + // Reset to where they are still equal. + msg_byte_ptr = msg_byte_ptr - modulo; + for i in 0..INT_SIZE { + msg_item = lshift8(msg_item, 1); + if i < modulo { + msg_item = msg_item + get_item_byte(last_field, msg_byte_ptr) as u32; + msg_byte_ptr = msg_byte_ptr + 1; + } + } + assert_eq(msg_block[int_byte_ptr], msg_item); + } + + for i in 0..INT_SIZE_PTR { + if i < int_byte_ptr { + assert_eq(msg_block[i], last_block[i]); + } + } +} + +// Apply a function on the block item which the pointer indicates. +fn update_block_item( + mut msg_block: MSG_BLOCK, + msg_byte_ptr: BLOCK_BYTE_PTR, + f: fn[Env](u32) -> u32, +) -> MSG_BLOCK { + let i = msg_byte_ptr / INT_SIZE; + msg_block[i] = f(msg_block[i]); + msg_block +} + +// Set the rightmost `zeros` number of bytes to 0. +fn set_item_zeros(item: u32, zeros: u8) -> u32 { + lshift8(rshift8(item, zeros), zeros) +} + +// Replace one byte in the item with a value, and set everything after it to zero. +fn set_item_byte_then_zeros(msg_item: u32, msg_byte_ptr: BLOCK_BYTE_PTR, msg_byte: u8) -> u32 { + let zeros = INT_SIZE - msg_byte_ptr % INT_SIZE; + let zeroed_item = set_item_zeros(msg_item, zeros as u8); + let new_item = byte_into_item(msg_byte, msg_byte_ptr); + zeroed_item + new_item +} + +// Get a byte of a message item according to its overall position in the `BLOCK_SIZE` space. +fn get_item_byte(mut msg_item: u32, msg_byte_ptr: BLOCK_BYTE_PTR) -> u8 { + // How many times do we have to shift to the right to get to the position we want? + let max_shifts = INT_SIZE - 1; + let shifts = max_shifts - msg_byte_ptr % INT_SIZE; + msg_item = rshift8(msg_item, shifts as u8); + // At this point the byte we want is in the rightmost position. + msg_item as u8 +} + +// Project a byte into a position in a field based on the overall block pointer. +// For example putting 1 into pointer 5 would be 100, because overall we would +// have [____, 0100] with indexes [0123,4567]. +fn byte_into_item(msg_byte: u8, msg_byte_ptr: BLOCK_BYTE_PTR) -> u32 { + let mut msg_item = msg_byte as u32; + // How many times do we have to shift to the left to get to the position we want? + let max_shifts = INT_SIZE - 1; + let shifts = max_shifts - msg_byte_ptr % INT_SIZE; + lshift8(msg_item, shifts as u8) +} + +// Construct a field out of 4 bytes. +fn make_item(b0: u8, b1: u8, b2: u8, b3: u8) -> u32 { + let mut item = b0 as u32; + item = lshift8(item, 1) + b1 as u32; + item = lshift8(item, 1) + b2 as u32; + item = lshift8(item, 1) + b3 as u32; + item +} + +// Shift by 8 bits to the left between 0 and 4 times. +// Checks `is_unconstrained()` to just use a bitshift if we're running in an unconstrained context, +// otherwise multiplies by 256. +fn lshift8(item: u32, shifts: u8) -> u32 { + if is_unconstrained() { + if item == 0 { + 0 + } else { + // Brillig wouldn't shift 0<<4 without overflow. + item << (8 * shifts) + } + } else { + // We can do a for loop up to INT_SIZE or an if-else. + if shifts == 0 { + item + } else if shifts == 1 { + item * TWO_POW_8 + } else if shifts == 2 { + item * TWO_POW_16 + } else if shifts == 3 { + item * TWO_POW_24 + } else { + // Doesn't make sense, but it's most likely called on 0 anyway. + 0 + } + } +} + +// Shift by 8 bits to the right between 0 and 4 times. +// Checks `is_unconstrained()` to just use a bitshift if we're running in an unconstrained context, +// otherwise divides by 256. +fn rshift8(item: u32, shifts: u8) -> u32 { + if is_unconstrained() { + item >> (8 * shifts) + } else { + // Division wouldn't work on `Field`. + if shifts == 0 { + item + } else if shifts == 1 { + item / TWO_POW_8 + } else if shifts == 2 { + item / TWO_POW_16 + } else if shifts == 3 { + item / TWO_POW_24 + } else { + 0 + } + } +} + // Zero out all bytes between the end of the message and where the length is appended, // then write the length into the last 8 bytes of the block. unconstrained fn attach_len_to_msg_block( mut msg_block: MSG_BLOCK, - msg_byte_ptr: BLOCK_BYTE_PTR, + mut msg_byte_ptr: BLOCK_BYTE_PTR, message_size: u32, ) -> MSG_BLOCK { // We assume that `msg_byte_ptr` is less than 57 because if not then it is reset to zero before calling this function. - // In any case, fill blocks up with zeros until the last 64 (i.e. until msg_byte_ptr = 56). - for i in msg_byte_ptr..MSG_SIZE_PTR { + // In any case, fill blocks up with zeros until the last 64 bits (i.e. until msg_byte_ptr = 56). + // There can be one item which has to be partially zeroed. + let modulo = msg_byte_ptr % INT_SIZE; + if modulo != 0 { + // Index of the block in which we find the item we need to partially zero. + let i = msg_byte_ptr / INT_SIZE; + let zeros = INT_SIZE - modulo; + msg_block[i] = set_item_zeros(msg_block[i], zeros as u8); + msg_byte_ptr = msg_byte_ptr + zeros; + } + + // The rest can be zeroed without bit shifting anything. + for i in (msg_byte_ptr / INT_SIZE)..INT_SIZE_PTR { msg_block[i] = 0; } + // Set the last two 4 byte ints as the first/second half of the 8 bytes of the length. let len = 8 * message_size; let len_bytes: [u8; 8] = (len as Field).to_be_bytes(); - for i in 0..8 { - msg_block[MSG_SIZE_PTR + i] = len_bytes[i]; + for i in 0..=1 { + let shift = i * 4; + msg_block[INT_SIZE_PTR + i] = make_item( + len_bytes[shift], + len_bytes[shift + 1], + len_bytes[shift + 2], + len_bytes[shift + 3], + ); } msg_block } -// Verify that the message length was correctly written by `attach_len_to_msg_block`. +// Verify that the message length was correctly written by `attach_len_to_msg_block`, +// and that everything between the byte pointer and the size pointer was zeroed, +// and that everything before the byte pointer was untouched. fn verify_msg_len( msg_block: MSG_BLOCK, last_block: MSG_BLOCK, msg_byte_ptr: BLOCK_BYTE_PTR, message_size: u32, ) { - for i in 0..MSG_SIZE_PTR { - let predicate = (i < msg_byte_ptr) as u8; - let expected_byte = predicate * last_block[i]; - assert_eq(msg_block[i], expected_byte); - } + // Check zeros up to the size pointer. + verify_msg_block_zeros(msg_block, msg_byte_ptr, INT_SIZE_PTR); + + // Check that up to the pointer we match the last block. + verify_msg_block_equals_last(msg_block, last_block, msg_byte_ptr); // We verify the message length was inserted correctly by reversing the byte decomposition. - let len = 8 * message_size; - let mut reconstructed_len: Field = 0; - for i in MSG_SIZE_PTR..BLOCK_SIZE { - reconstructed_len = 256 * reconstructed_len + msg_block[i] as Field; + let mut reconstructed_len: u64 = 0; + for i in INT_SIZE_PTR..INT_BLOCK_SIZE { + reconstructed_len = reconstructed_len * TWO_POW_32; + reconstructed_len = reconstructed_len + msg_block[i] as u64; } - assert_eq(reconstructed_len, len as Field); + let len = 8 * message_size as u64; + assert_eq(reconstructed_len, len); } // Perform the final compression, then transform the `STATE` into `HASH`. fn hash_final_block(msg_block: MSG_BLOCK, mut state: STATE) -> HASH { let mut out_h: HASH = [0; 32]; // Digest as sequence of bytes // Hash final padded block - state = sha256_compression(msg_u8_to_u32(msg_block), state); + state = sha256_compression(msg_block, state); // Return final hash as byte array for j in 0..8 { @@ -275,6 +518,11 @@ fn hash_final_block(msg_block: MSG_BLOCK, mut state: STATE) -> HASH { } mod tests { + use super::{ + attach_len_to_msg_block, build_msg_block, byte_into_item, get_item_byte, lshift8, make_item, + set_item_byte_then_zeros, set_item_zeros, + }; + use super::{INT_BLOCK, INT_BLOCK_SIZE, MSG_BLOCK}; use super::sha256_var; #[test] @@ -510,4 +758,79 @@ mod tests { assert_eq(var_length_hash_575, fixed_length_hash); assert_eq(var_length_hash_576, fixed_length_hash); } + + #[test] + fn test_get_item_byte() { + let fld = make_item(10, 20, 30, 40); + assert_eq(fld, 0x0a141e28); + assert_eq(get_item_byte(fld, 0), 10); + assert_eq(get_item_byte(fld, 4), 10); + assert_eq(get_item_byte(fld, 6), 30); + } + + #[test] + fn test_byte_into_item() { + let fld = make_item(0, 20, 0, 0); + assert_eq(byte_into_item(20, 1), fld); + assert_eq(byte_into_item(20, 5), fld); + } + + #[test] + fn test_set_item_zeros() { + let fld0 = make_item(10, 20, 30, 40); + let fld1 = make_item(10, 0, 0, 0); + assert_eq(set_item_zeros(fld0, 3), fld1); + assert_eq(set_item_zeros(fld0, 4), 0); + assert_eq(set_item_zeros(0, 4), 0); + } + + #[test] + fn test_set_item_byte_then_zeros() { + let fld0 = make_item(10, 20, 30, 40); + let fld1 = make_item(10, 50, 0, 0); + assert_eq(set_item_byte_then_zeros(fld0, 1, 50), fld1); + } + + #[test] + fn test_build_msg_block_start_0() { + let input = [ + 102, 114, 111, 109, 58, 114, 117, 110, 110, 105, 101, 114, 46, 108, 101, 97, 103, 117, + 101, 115, 46, 48, + ]; + assert_eq(input.len(), 22); + let (msg_block, msg_byte_ptr) = unsafe { build_msg_block(input, input.len(), 0) }; + assert_eq(msg_byte_ptr, input.len()); + assert_eq(msg_block[0], make_item(input[0], input[1], input[2], input[3])); + assert_eq(msg_block[1], make_item(input[4], input[5], input[6], input[7])); + assert_eq(msg_block[5], make_item(input[20], input[21], 0, 0)); + assert_eq(msg_block[6], 0); + } + + #[test] + fn test_build_msg_block_start_1() { + let input = [ + 102, 114, 111, 109, 58, 114, 117, 110, 110, 105, 101, 114, 46, 108, 101, 97, 103, 117, + 101, 115, 46, 48, 106, 64, 105, 99, 108, 111, 117, 100, 46, 99, 111, 109, 13, 10, 99, + 111, 110, 116, 101, 110, 116, 45, 116, 121, 112, 101, 58, 116, 101, 120, 116, 47, 112, + 108, 97, 105, 110, 59, 32, 99, 104, 97, 114, 115, 101, 116, + ]; + assert_eq(input.len(), 68); + let (msg_block, msg_byte_ptr) = unsafe { build_msg_block(input, input.len(), 64) }; + assert_eq(msg_byte_ptr, 4); + assert_eq(msg_block[0], make_item(input[64], input[65], input[66], input[67])); + assert_eq(msg_block[1], 0); + } + + #[test] + fn test_attach_len_to_msg_block() { + let input: INT_BLOCK = [ + 2152555847, 1397309779, 1936618851, 1262052426, 1936876331, 1985297723, 543702374, + 1919905082, 1131376244, 1701737517, 1417244773, 978151789, 1697470053, 1920166255, + 1849316213, 1651139939, + ]; + let msg_block = unsafe { attach_len_to_msg_block(input, 1, 448) }; + assert_eq(msg_block[0], ((1 << 7) as u32) * 256 * 256 * 256); + assert_eq(msg_block[1], 0); + assert_eq(msg_block[15], 3584); + } } diff --git a/tooling/nargo_cli/tests/stdlib-tests.rs b/tooling/nargo_cli/tests/stdlib-tests.rs index d27a2961a62..46a241b7e0b 100644 --- a/tooling/nargo_cli/tests/stdlib-tests.rs +++ b/tooling/nargo_cli/tests/stdlib-tests.rs @@ -76,7 +76,7 @@ fn run_stdlib_tests() { &bn254_blackbox_solver::Bn254BlackBoxSolver, &mut context, &test_function, - false, + true, None, Some(dummy_package.root_dir.clone()), Some(dummy_package.name.to_string()), From d8e549aad6bae0f96621c57b58a301693463f732 Mon Sep 17 00:00:00 2001 From: guipublic <47281315+guipublic@users.noreply.github.com> Date: Fri, 25 Oct 2024 11:23:02 +0200 Subject: [PATCH 04/30] chore: switch to btreeset for deterministic ordering (#6348) # Description ## Problem\* The optimisation pass from PR #6268 iterates over hash maps and hash sets, which does not guarantee deterministic ordering. This is an issue as it could lead to different results for the same ACIR input. ## Summary\* Uses BTreeMap/Set when iterating. ## Additional Context ## Documentation\* Check one: - [X] No documentation needed. - [ ] Documentation included in this PR. - [ ] **[For Experimental Features]** Documentation to be submitted in a separate PR. # PR Checklist\* - [X] I have tested the changes locally. - [X] I have formatted the changes with [Prettier](https://prettier.io/) and/or `cargo fmt` on default settings. --- .../opcodes/black_box_function_call.rs | 6 ++--- .../compiler/optimizers/merge_expressions.rs | 22 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/acvm-repo/acir/src/circuit/opcodes/black_box_function_call.rs b/acvm-repo/acir/src/circuit/opcodes/black_box_function_call.rs index 7e76530b3ca..fa51caf5155 100644 --- a/acvm-repo/acir/src/circuit/opcodes/black_box_function_call.rs +++ b/acvm-repo/acir/src/circuit/opcodes/black_box_function_call.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::collections::BTreeSet; use crate::native_types::Witness; use crate::{AcirField, BlackBoxFunc}; @@ -392,8 +392,8 @@ impl BlackBoxFuncCall { } } - pub fn get_input_witnesses(&self) -> HashSet { - let mut result = HashSet::new(); + pub fn get_input_witnesses(&self) -> BTreeSet { + let mut result = BTreeSet::new(); for input in self.get_inputs_vec() { if let ConstantOrWitnessEnum::Witness(w) = input.input() { result.insert(w); diff --git a/acvm-repo/acvm/src/compiler/optimizers/merge_expressions.rs b/acvm-repo/acvm/src/compiler/optimizers/merge_expressions.rs index 4291b997fde..ddf86f60f77 100644 --- a/acvm-repo/acvm/src/compiler/optimizers/merge_expressions.rs +++ b/acvm-repo/acvm/src/compiler/optimizers/merge_expressions.rs @@ -1,4 +1,4 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use acir::{ circuit::{brillig::BrilligInputs, directives::Directive, opcodes::BlockId, Circuit, Opcode}, @@ -7,7 +7,7 @@ use acir::{ }; pub(crate) struct MergeExpressionsOptimizer { - resolved_blocks: HashMap>, + resolved_blocks: HashMap>, } impl MergeExpressionsOptimizer { @@ -26,7 +26,7 @@ impl MergeExpressionsOptimizer { // Keep track, for each witness, of the gates that use it let circuit_inputs = circuit.circuit_arguments(); self.resolved_blocks = HashMap::new(); - let mut used_witness: HashMap> = HashMap::new(); + let mut used_witness: BTreeMap> = BTreeMap::new(); for (i, opcode) in circuit.opcodes.iter().enumerate() { let witnesses = self.witness_inputs(opcode); if let Opcode::MemoryInit { block_id, .. } = opcode { @@ -54,7 +54,7 @@ impl MergeExpressionsOptimizer { let mut to_keep = true; let input_witnesses = self.witness_inputs(&opcode); for w in input_witnesses.clone() { - let empty_gates = HashSet::new(); + let empty_gates = BTreeSet::new(); let gates_using_w = used_witness.get(&w).unwrap_or(&empty_gates); // We only consider witness which are used in exactly two arithmetic gates if gates_using_w.len() == 2 { @@ -104,15 +104,15 @@ impl MergeExpressionsOptimizer { (new_circuit, new_acir_opcode_positions) } - fn expr_wit(expr: &Expression) -> HashSet { - let mut result = HashSet::new(); + fn expr_wit(expr: &Expression) -> BTreeSet { + let mut result = BTreeSet::new(); result.extend(expr.mul_terms.iter().flat_map(|i| vec![i.1, i.2])); result.extend(expr.linear_combinations.iter().map(|i| i.1)); result } - fn brillig_input_wit(&self, input: &BrilligInputs) -> HashSet { - let mut result = HashSet::new(); + fn brillig_input_wit(&self, input: &BrilligInputs) -> BTreeSet { + let mut result = BTreeSet::new(); match input { BrilligInputs::Single(expr) => { result.extend(Self::expr_wit(expr)); @@ -131,15 +131,15 @@ impl MergeExpressionsOptimizer { } // Returns the input witnesses used by the opcode - fn witness_inputs(&self, opcode: &Opcode) -> HashSet { - let mut witnesses = HashSet::new(); + fn witness_inputs(&self, opcode: &Opcode) -> BTreeSet { + let mut witnesses = BTreeSet::new(); match opcode { Opcode::AssertZero(expr) => Self::expr_wit(expr), Opcode::BlackBoxFuncCall(bb_func) => bb_func.get_input_witnesses(), Opcode::Directive(Directive::ToLeRadix { a, .. }) => Self::expr_wit(a), Opcode::MemoryOp { block_id: _, op, predicate } => { //index et value, et predicate - let mut witnesses = HashSet::new(); + let mut witnesses = BTreeSet::new(); witnesses.extend(Self::expr_wit(&op.index)); witnesses.extend(Self::expr_wit(&op.value)); if let Some(p) = predicate { From 8bc8e6591906a175f5474d2f3425f696a576ffd4 Mon Sep 17 00:00:00 2001 From: Tom French <15848336+TomAFrench@users.noreply.github.com> Date: Fri, 25 Oct 2024 14:51:47 +0100 Subject: [PATCH 05/30] chore: add test to check that duplicate definitions generated from macros throws error (#6351) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … # Description ## Problem\* Resolves ## Summary\* This PR adds a test to sanity check that we will throw an error on duplicate named functions being injected by macros as reported in https://aztecprotocol.slack.com/archives/C0183F0V42V/p1729855935233669 ## Additional Context ## Documentation\* Check one: - [x] No documentation needed. - [ ] Documentation included in this PR. - [ ] **[For Experimental Features]** Documentation to be submitted in a separate PR. # PR Checklist\* - [x] I have tested the changes locally. - [x] I have formatted the changes with [Prettier](https://prettier.io/) and/or `cargo fmt` on default settings. --- .../src/tests/metaprogramming.rs | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/compiler/noirc_frontend/src/tests/metaprogramming.rs b/compiler/noirc_frontend/src/tests/metaprogramming.rs index a5241cc26ed..82c40203244 100644 --- a/compiler/noirc_frontend/src/tests/metaprogramming.rs +++ b/compiler/noirc_frontend/src/tests/metaprogramming.rs @@ -1,6 +1,15 @@ -use crate::hir::{ - def_collector::dc_crate::CompilationError, resolution::errors::ResolverError, - type_check::TypeCheckError, +use noirc_errors::Spanned; + +use crate::{ + ast::Ident, + hir::{ + def_collector::{ + dc_crate::CompilationError, + errors::{DefCollectorErrorKind, DuplicateType}, + }, + resolution::errors::ResolverError, + type_check::TypeCheckError, + }, }; use super::{assert_no_errors, get_program_errors}; @@ -96,3 +105,39 @@ fn allows_references_to_structs_generated_by_macros() { assert_no_errors(src); } + +#[test] +fn errors_if_macros_inject_functions_with_name_collisions() { + let src = r#" + comptime fn make_colliding_functions(_s: StructDefinition) -> Quoted { + quote { + fn foo() {} + } + } + + #[make_colliding_functions] + struct Foo {} + + #[make_colliding_functions] + struct Bar {} + + fn main() { + let _ = Foo {}; + let _ = Bar {}; + foo(); + } + "#; + + let errors = get_program_errors(src); + assert_eq!(errors.len(), 1); + assert!(matches!( + &errors[0].0, + CompilationError::DefinitionError( + DefCollectorErrorKind::Duplicate { + typ: DuplicateType::Function, + first_def: Ident(Spanned { contents, .. }), + .. + }, + ) if contents == "foo" + )); +} From 7c98b36305ffdbbaee3947723f248fa718e7a950 Mon Sep 17 00:00:00 2001 From: Tom French <15848336+TomAFrench@users.noreply.github.com> Date: Fri, 25 Oct 2024 15:09:02 +0100 Subject: [PATCH 06/30] fix: always inline `derive_generators` (#6350) # Description ## Problem\* Resolves ## Summary\* If I turn down the inliner aggressiveness I start getting failures here due to the need for `domain_generator_bytes` to be a constant. This PR adds an attribute to always inline this function. ## Additional Context ## Documentation\* Check one: - [x] No documentation needed. - [ ] Documentation included in this PR. - [ ] **[For Experimental Features]** Documentation to be submitted in a separate PR. # PR Checklist\* - [x] I have tested the changes locally. - [x] I have formatted the changes with [Prettier](https://prettier.io/) and/or `cargo fmt` on default settings. --- noir_stdlib/src/hash/mod.nr | 1 + 1 file changed, 1 insertion(+) diff --git a/noir_stdlib/src/hash/mod.nr b/noir_stdlib/src/hash/mod.nr index 15112757312..c5e6da9d76f 100644 --- a/noir_stdlib/src/hash/mod.nr +++ b/noir_stdlib/src/hash/mod.nr @@ -75,6 +75,7 @@ pub fn pedersen_hash_with_separator(input: [Field; N], separator: u3 } #[field(bn254)] +#[inline_always] pub fn derive_generators( domain_separator_bytes: [u8; M], starting_index: u32, From bcd897627c69b1ebcadc8b84abe2922ce3473c56 Mon Sep 17 00:00:00 2001 From: Maxim Vezenov Date: Fri, 25 Oct 2024 10:16:22 -0400 Subject: [PATCH 07/30] fix(ssa): Do not mark an array from a parameter mutable (#6355) # Description ## Problem\* Resolves #6349 ## Summary\* We check that we do not mark arrays mutable which comes from load instructions as that array may be used by multiple values. This is overridden by checking whether we are in the return block, in which case we do mark the array as mutable. This however breaks when we no longer inline everything. I switched the set of arrays from loads to a map of arrays from loads to whether they come from a block parameter. ## Additional Context ## Documentation\* Check one: - [X] No documentation needed. - [ ] Documentation included in this PR. - [ ] **[For Experimental Features]** Documentation to be submitted in a separate PR. # PR Checklist\* - [X] I have tested the changes locally. - [X] I have formatted the changes with [Prettier](https://prettier.io/) and/or `cargo fmt` on default settings. --- .../noirc_evaluator/src/ssa/opt/array_set.rs | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/compiler/noirc_evaluator/src/ssa/opt/array_set.rs b/compiler/noirc_evaluator/src/ssa/opt/array_set.rs index 6ae13bc085a..7035345436e 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/array_set.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/array_set.rs @@ -53,7 +53,9 @@ struct Context<'f> { is_brillig_runtime: bool, array_to_last_use: HashMap, instructions_that_can_be_made_mutable: HashSet, - arrays_from_load: HashSet, + // Mapping of an array that comes from a load and whether the address + // it was loaded from is a reference parameter. + arrays_from_load: HashMap, inner_nested_arrays: HashMap, } @@ -64,7 +66,7 @@ impl<'f> Context<'f> { is_brillig_runtime, array_to_last_use: HashMap::default(), instructions_that_can_be_made_mutable: HashSet::default(), - arrays_from_load: HashSet::default(), + arrays_from_load: HashMap::default(), inner_nested_arrays: HashMap::default(), } } @@ -113,9 +115,13 @@ impl<'f> Context<'f> { array_in_terminator = true; } }); - if (!self.arrays_from_load.contains(&array) || is_return_block) - && !array_in_terminator - { + if let Some(is_from_param) = self.arrays_from_load.get(&array) { + // If the array was loaded from a reference parameter, we cannot + // safely mark that array mutable as it may be shared by another value. + if !is_from_param && is_return_block { + self.instructions_that_can_be_made_mutable.insert(*instruction_id); + } + } else if !array_in_terminator { self.instructions_that_can_be_made_mutable.insert(*instruction_id); } } @@ -133,10 +139,12 @@ impl<'f> Context<'f> { } } } - Instruction::Load { .. } => { + Instruction::Load { address } => { let result = self.dfg.instruction_results(*instruction_id)[0]; if matches!(self.dfg.type_of_value(result), Array { .. } | Slice { .. }) { - self.arrays_from_load.insert(result); + let is_reference_param = + self.dfg.block_parameters(block_id).contains(address); + self.arrays_from_load.insert(result, is_reference_param); } } _ => (), From 4c39514fccf3595de6bdfad755b6ae2d3ef11aa1 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Fri, 25 Oct 2024 15:36:45 +0100 Subject: [PATCH 08/30] feat(test): Run test matrix on stdlib tests (#6352) --- Cargo.lock | 34 +++++++++++++++++++++++++ Cargo.toml | 1 + tooling/nargo_cli/Cargo.toml | 1 + tooling/nargo_cli/tests/stdlib-tests.rs | 18 ++++++++++--- 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dabac0a7570..3add28d3939 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2587,6 +2587,7 @@ dependencies = [ "termcolor", "termion", "test-binary", + "test-case", "thiserror", "tokio", "tokio-util 0.7.10", @@ -4423,6 +4424,39 @@ dependencies = [ "thiserror", ] +[[package]] +name = "test-case" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" +dependencies = [ + "test-case-macros", +] + +[[package]] +name = "test-case-core" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" +dependencies = [ + "cfg-if 1.0.0", + "proc-macro2", + "quote", + "syn 2.0.64", +] + +[[package]] +name = "test-case-macros" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.64", + "test-case-core", +] + [[package]] name = "textwrap" version = "0.13.4" diff --git a/Cargo.toml b/Cargo.toml index 36b4d4c2ce1..222e7dce859 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -146,6 +146,7 @@ num-bigint = "0.4" num-traits = "0.2" similar-asserts = "1.5.0" tempfile = "3.6.0" +test-case = "3.3.1" jsonrpc = { version = "0.16.0", features = ["minreq_http"] } flate2 = "1.0.24" color-eyre = "0.6.2" diff --git a/tooling/nargo_cli/Cargo.toml b/tooling/nargo_cli/Cargo.toml index d72556ab936..d48e44415b0 100644 --- a/tooling/nargo_cli/Cargo.toml +++ b/tooling/nargo_cli/Cargo.toml @@ -86,6 +86,7 @@ sha2.workspace = true sha3.workspace = true iai = "0.1.1" test-binary = "3.0.2" +test-case.workspace = true light-poseidon = "0.2.0" diff --git a/tooling/nargo_cli/tests/stdlib-tests.rs b/tooling/nargo_cli/tests/stdlib-tests.rs index 46a241b7e0b..bdc92e625ab 100644 --- a/tooling/nargo_cli/tests/stdlib-tests.rs +++ b/tooling/nargo_cli/tests/stdlib-tests.rs @@ -1,4 +1,5 @@ //! Execute unit tests in the Noir standard library. +#![allow(clippy::items_after_test_module)] use clap::Parser; use fm::FileManager; use noirc_driver::{check_crate, file_manager_with_stdlib, CompileOptions}; @@ -12,6 +13,7 @@ use nargo::{ parse_all, prepare_package, }; use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; +use test_case::test_matrix; #[derive(Parser, Debug)] #[command(ignore_errors = true)] @@ -29,14 +31,22 @@ pub struct Options { impl Options { pub fn function_name_match(&self) -> FunctionNameMatch { match self.args.as_slice() { - [_, lib] => FunctionNameMatch::Contains(lib.as_str()), + [_test_name, lib] => FunctionNameMatch::Contains(lib.as_str()), _ => FunctionNameMatch::Anything, } } } -#[test] -fn run_stdlib_tests() { +/// Inliner aggressiveness results in different SSA. +/// Inlining happens if `inline_cost - retain_cost < aggressiveness` (see `inlining.rs`). +/// NB the CLI uses maximum aggressiveness. +/// +/// Even with the same inlining aggressiveness, forcing Brillig can trigger different behaviour. +#[test_matrix( + [false, true], + [i64::MIN, 0, i64::MAX] +)] +fn run_stdlib_tests(force_brillig: bool, inliner_aggressiveness: i64) { let opts = Options::parse(); let mut file_manager = file_manager_with_stdlib(&PathBuf::from(".")); @@ -80,7 +90,7 @@ fn run_stdlib_tests() { None, Some(dummy_package.root_dir.clone()), Some(dummy_package.name.to_string()), - &CompileOptions::default(), + &CompileOptions { force_brillig, inliner_aggressiveness, ..Default::default() }, ); (test_name, status) }) From 91c08421fdc5df7edcf502fb7fc1d343bb860b03 Mon Sep 17 00:00:00 2001 From: jfecher Date: Fri, 25 Oct 2024 10:48:24 -0500 Subject: [PATCH 09/30] fix: allow type aliases in let patterns (#6356) --- .../noirc_frontend/src/elaborator/scope.rs | 18 ++++++++++++++++-- compiler/noirc_frontend/src/hir_def/types.rs | 5 +++++ compiler/noirc_frontend/src/tests.rs | 16 ++++++++++++++++ compiler/noirc_frontend/src/tests/aliases.rs | 19 ------------------- 4 files changed, 37 insertions(+), 21 deletions(-) diff --git a/compiler/noirc_frontend/src/elaborator/scope.rs b/compiler/noirc_frontend/src/elaborator/scope.rs index b83a1494ab9..d38b7a50175 100644 --- a/compiler/noirc_frontend/src/elaborator/scope.rs +++ b/compiler/noirc_frontend/src/elaborator/scope.rs @@ -269,12 +269,26 @@ impl<'context> Elaborator<'context> { } } - match self.lookup(path) { - Ok(struct_id) => { + let span = path.span; + match self.resolve_path_or_error(path) { + Ok(ModuleDefId::TypeId(struct_id)) => { let struct_type = self.get_struct(struct_id); let generics = struct_type.borrow().instantiate(self.interner); Some(Type::Struct(struct_type, generics)) } + Ok(ModuleDefId::TypeAliasId(alias_id)) => { + let alias = self.interner.get_type_alias(alias_id); + let alias = alias.borrow(); + Some(alias.instantiate(self.interner)) + } + Ok(other) => { + self.push_err(ResolverError::Expected { + expected: StructId::description(), + got: other.as_str().to_owned(), + span, + }); + None + } Err(error) => { self.push_err(error); None diff --git a/compiler/noirc_frontend/src/hir_def/types.rs b/compiler/noirc_frontend/src/hir_def/types.rs index fa2a455c06d..77030b0e048 100644 --- a/compiler/noirc_frontend/src/hir_def/types.rs +++ b/compiler/noirc_frontend/src/hir_def/types.rs @@ -590,6 +590,11 @@ impl TypeAlias { self.typ.substitute(&substitutions) } + + pub fn instantiate(&self, interner: &NodeInterner) -> Type { + let args = vecmap(&self.generics, |_| interner.next_type_variable()); + self.get_type(&args) + } } /// A shared, mutable reference to some T. diff --git a/compiler/noirc_frontend/src/tests.rs b/compiler/noirc_frontend/src/tests.rs index e06881127fd..56c4b5e9a12 100644 --- a/compiler/noirc_frontend/src/tests.rs +++ b/compiler/noirc_frontend/src/tests.rs @@ -3546,3 +3546,19 @@ fn uses_self_in_import() { "#; assert_no_errors(src); } + +#[test] +fn alias_in_let_pattern() { + let src = r#" + struct Foo { x: T } + + type Bar = Foo; + + fn main() { + let Bar { x } = Foo { x: [0] }; + // This is just to show the compiler knows this is an array. + let _: [Field; 1] = x; + } + "#; + assert_no_errors(src); +} diff --git a/compiler/noirc_frontend/src/tests/aliases.rs b/compiler/noirc_frontend/src/tests/aliases.rs index 2ca460ca635..5239abcb366 100644 --- a/compiler/noirc_frontend/src/tests/aliases.rs +++ b/compiler/noirc_frontend/src/tests/aliases.rs @@ -31,22 +31,3 @@ fn allows_usage_of_type_alias_as_return_type() { "#; assert_no_errors(src); } - -// This is a regression test for https://github.com/noir-lang/noir/issues/6347 -#[test] -#[should_panic = r#"ResolverError(Expected { span: Span(Span { start: ByteIndex(95), end: ByteIndex(98) }), expected: "type", got: "type alias" }"#] -fn allows_destructuring_a_type_alias_of_a_struct() { - let src = r#" - struct Foo { - inner: Field - } - - type Bar = Foo; - - fn main() { - let Bar { inner } = Foo { inner: 42 }; - assert_eq(inner, 42); - } - "#; - assert_no_errors(src); -} From 647f6a4bd3d00fd3b3b3e4ff17dce512287ee5b4 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Fri, 25 Oct 2024 14:01:10 -0300 Subject: [PATCH 10/30] feat: LSP auto-import will try to add to existing use statements (#6354) --- compiler/noirc_frontend/src/ast/statement.rs | 1 + .../src/parser/parser/use_tree.rs | 42 +- tooling/lsp/src/lib.rs | 1 + .../code_action/remove_unused_import.rs | 3 +- tooling/lsp/src/requests/code_action/tests.rs | 17 +- tooling/lsp/src/requests/completion.rs | 162 ++++- .../src/requests/completion/auto_import.rs | 177 +++++- tooling/lsp/src/requests/completion/tests.rs | 554 ++++++++++++++---- tooling/lsp/src/tests.rs | 32 + 9 files changed, 834 insertions(+), 155 deletions(-) create mode 100644 tooling/lsp/src/tests.rs diff --git a/compiler/noirc_frontend/src/ast/statement.rs b/compiler/noirc_frontend/src/ast/statement.rs index 038a7b76bac..a25b87b7395 100644 --- a/compiler/noirc_frontend/src/ast/statement.rs +++ b/compiler/noirc_frontend/src/ast/statement.rs @@ -327,6 +327,7 @@ pub enum PathKind { pub struct UseTree { pub prefix: Path, pub kind: UseTreeKind, + pub span: Span, } impl Display for UseTree { diff --git a/compiler/noirc_frontend/src/parser/parser/use_tree.rs b/compiler/noirc_frontend/src/parser/parser/use_tree.rs index bc95c04d46b..08834383f99 100644 --- a/compiler/noirc_frontend/src/parser/parser/use_tree.rs +++ b/compiler/noirc_frontend/src/parser/parser/use_tree.rs @@ -53,13 +53,17 @@ impl<'a> Parser<'a> { Self::parse_use_tree_in_list, ); - UseTree { prefix, kind: UseTreeKind::List(use_trees) } + UseTree { + prefix, + kind: UseTreeKind::List(use_trees), + span: self.span_since(start_span), + } } else { self.expected_token(Token::LeftBrace); - self.parse_path_use_tree_end(prefix, nested) + self.parse_path_use_tree_end(prefix, nested, start_span) } } else { - self.parse_path_use_tree_end(prefix, nested) + self.parse_path_use_tree_end(prefix, nested, start_span) } } @@ -71,6 +75,7 @@ impl<'a> Parser<'a> { return Some(UseTree { prefix: Path { segments: Vec::new(), kind: PathKind::Plain, span: start_span }, kind: UseTreeKind::Path(Ident::new("self".to_string(), start_span), None), + span: start_span, }); } @@ -89,25 +94,46 @@ impl<'a> Parser<'a> { } } - pub(super) fn parse_path_use_tree_end(&mut self, mut prefix: Path, nested: bool) -> UseTree { + pub(super) fn parse_path_use_tree_end( + &mut self, + mut prefix: Path, + nested: bool, + start_span: Span, + ) -> UseTree { if prefix.segments.is_empty() { if nested { self.expected_identifier(); } else { self.expected_label(ParsingRuleLabel::UseSegment); } - UseTree { prefix, kind: UseTreeKind::Path(Ident::default(), None) } + UseTree { + prefix, + kind: UseTreeKind::Path(Ident::default(), None), + span: self.span_since(start_span), + } } else { let ident = prefix.segments.pop().unwrap().ident; if self.eat_keyword(Keyword::As) { if let Some(alias) = self.eat_ident() { - UseTree { prefix, kind: UseTreeKind::Path(ident, Some(alias)) } + UseTree { + prefix, + kind: UseTreeKind::Path(ident, Some(alias)), + span: self.span_since(start_span), + } } else { self.expected_identifier(); - UseTree { prefix, kind: UseTreeKind::Path(ident, None) } + UseTree { + prefix, + kind: UseTreeKind::Path(ident, None), + span: self.span_since(start_span), + } } } else { - UseTree { prefix, kind: UseTreeKind::Path(ident, None) } + UseTree { + prefix, + kind: UseTreeKind::Path(ident, None), + span: self.span_since(start_span), + } } } } diff --git a/tooling/lsp/src/lib.rs b/tooling/lsp/src/lib.rs index 39e14a74007..7254e3a5f77 100644 --- a/tooling/lsp/src/lib.rs +++ b/tooling/lsp/src/lib.rs @@ -67,6 +67,7 @@ mod modules; mod notifications; mod requests; mod solver; +mod tests; mod trait_impl_method_stub_generator; mod types; mod utils; diff --git a/tooling/lsp/src/requests/code_action/remove_unused_import.rs b/tooling/lsp/src/requests/code_action/remove_unused_import.rs index f5f0b520149..c1a3c87d142 100644 --- a/tooling/lsp/src/requests/code_action/remove_unused_import.rs +++ b/tooling/lsp/src/requests/code_action/remove_unused_import.rs @@ -106,11 +106,12 @@ fn use_tree_without_unused_import( let mut prefix = use_tree.prefix.clone(); prefix.segments.extend(new_use_tree.prefix.segments); - Some(UseTree { prefix, kind: new_use_tree.kind }) + Some(UseTree { prefix, kind: new_use_tree.kind, span: use_tree.span }) } else { Some(UseTree { prefix: use_tree.prefix.clone(), kind: UseTreeKind::List(new_use_trees), + span: use_tree.span, }) }; diff --git a/tooling/lsp/src/requests/code_action/tests.rs b/tooling/lsp/src/requests/code_action/tests.rs index ef592ee9d53..848db470950 100644 --- a/tooling/lsp/src/requests/code_action/tests.rs +++ b/tooling/lsp/src/requests/code_action/tests.rs @@ -1,11 +1,11 @@ #![cfg(test)] -use crate::{notifications::on_did_open_text_document, test_utils}; +use crate::{notifications::on_did_open_text_document, test_utils, tests::apply_text_edit}; use lsp_types::{ CodeActionContext, CodeActionOrCommand, CodeActionParams, CodeActionResponse, DidOpenTextDocumentParams, PartialResultParams, Position, Range, TextDocumentIdentifier, - TextDocumentItem, TextEdit, WorkDoneProgressParams, + TextDocumentItem, WorkDoneProgressParams, }; use super::on_code_action_request; @@ -78,16 +78,3 @@ pub(crate) async fn assert_code_action(title: &str, src: &str, expected: &str) { assert_eq!(result, expected); } } - -fn apply_text_edit(src: &str, text_edit: &TextEdit) -> String { - let mut lines: Vec<_> = src.lines().collect(); - assert_eq!(text_edit.range.start.line, text_edit.range.end.line); - - let mut line = lines[text_edit.range.start.line as usize].to_string(); - line.replace_range( - text_edit.range.start.character as usize..text_edit.range.end.character as usize, - &text_edit.new_text, - ); - lines[text_edit.range.start.line as usize] = &line; - lines.join("\n") -} diff --git a/tooling/lsp/src/requests/completion.rs b/tooling/lsp/src/requests/completion.rs index 658033fc526..d0e0efd97bc 100644 --- a/tooling/lsp/src/requests/completion.rs +++ b/tooling/lsp/src/requests/completion.rs @@ -88,6 +88,42 @@ pub(crate) fn on_completion_request( future::ready(result) } +/// The position of a segment in a `use` statement. +/// We use this to determine how an auto-import should be inserted. +#[derive(Debug, Default, Copy, Clone)] +enum UseSegmentPosition { + /// The segment either doesn't exist in the source code or there are multiple segments. + /// In this case auto-import will add a new use statement. + #[default] + NoneOrMultiple, + /// The segment is the last one in the `use` statement (or nested use statement): + /// + /// use foo::bar; + /// ^^^ + /// + /// Auto-import will transform it to this: + /// + /// use foo::bar::{self, baz}; + Last { span: Span }, + /// The segment happens before another simple (ident) segment: + /// + /// use foo::bar::qux; + /// ^^^ + /// + /// Auto-import will transform it to this: + /// + /// use foo::bar::{qux, baz}; + BeforeSegment { segment_span_until_end: Span }, + /// The segment happens before a list: + /// + /// use foo::bar::{qux, another}; + /// + /// Auto-import will transform it to this: + /// + /// use foo::bar::{qux, another, baz}; + BeforeList { first_entry_span: Span, list_is_empty: bool }, +} + struct NodeFinder<'a> { files: &'a FileMap, file: FileId, @@ -115,6 +151,11 @@ struct NodeFinder<'a> { nesting: usize, /// The line where an auto_import must be inserted auto_import_line: usize, + /// Remember where each segment in a `use` statement is located. + /// The key is the full segment, so for `use foo::bar::baz` we'll have three + /// segments: `foo`, `foo::bar` and `foo::bar::baz`, where the span is just + /// for the last identifier (`foo`, `bar` and `baz` in the previous example). + use_segment_positions: HashMap, self_type: Option, in_comptime: bool, } @@ -159,6 +200,7 @@ impl<'a> NodeFinder<'a> { suggested_module_def_ids: HashSet::new(), nesting: 0, auto_import_line: 0, + use_segment_positions: HashMap::new(), self_type: None, in_comptime: false, } @@ -1035,6 +1077,123 @@ impl<'a> NodeFinder<'a> { } } + /// Determine where each segment in a `use` statement is located. + fn gather_use_tree_segments(&mut self, use_tree: &UseTree, mut prefix: String) { + let kind_string = match use_tree.prefix.kind { + PathKind::Crate => Some("crate".to_string()), + PathKind::Super => Some("super".to_string()), + PathKind::Dep | PathKind::Plain => None, + }; + if let Some(kind_string) = kind_string { + if let Some(segment) = use_tree.prefix.segments.first() { + self.insert_use_segment_position( + kind_string, + UseSegmentPosition::BeforeSegment { + segment_span_until_end: Span::from( + segment.ident.span().start()..use_tree.span.end() - 1, + ), + }, + ); + } else { + self.insert_use_segment_position_before_use_tree_kind(use_tree, kind_string); + } + } + + let prefix_segments_len = use_tree.prefix.segments.len(); + for (index, segment) in use_tree.prefix.segments.iter().enumerate() { + let ident = &segment.ident; + if !prefix.is_empty() { + prefix.push_str("::"); + }; + prefix.push_str(&ident.0.contents); + + if index < prefix_segments_len - 1 { + self.insert_use_segment_position( + prefix.clone(), + UseSegmentPosition::BeforeSegment { + segment_span_until_end: Span::from( + use_tree.prefix.segments[index + 1].ident.span().start() + ..use_tree.span.end() - 1, + ), + }, + ); + } else { + self.insert_use_segment_position_before_use_tree_kind(use_tree, prefix.clone()); + } + } + + match &use_tree.kind { + UseTreeKind::Path(ident, alias) => { + if !prefix.is_empty() { + prefix.push_str("::"); + } + prefix.push_str(&ident.0.contents); + + if alias.is_none() { + self.insert_use_segment_position( + prefix, + UseSegmentPosition::Last { span: ident.span() }, + ); + } else { + self.insert_use_segment_position(prefix, UseSegmentPosition::NoneOrMultiple); + } + } + UseTreeKind::List(use_trees) => { + for use_tree in use_trees { + self.gather_use_tree_segments(use_tree, prefix.clone()); + } + } + } + } + + fn insert_use_segment_position_before_use_tree_kind( + &mut self, + use_tree: &UseTree, + prefix: String, + ) { + match &use_tree.kind { + UseTreeKind::Path(ident, _alias) => { + self.insert_use_segment_position( + prefix, + UseSegmentPosition::BeforeSegment { + segment_span_until_end: Span::from( + ident.span().start()..use_tree.span.end() - 1, + ), + }, + ); + } + UseTreeKind::List(use_trees) => { + if let Some(first_use_tree) = use_trees.first() { + self.insert_use_segment_position( + prefix, + UseSegmentPosition::BeforeList { + first_entry_span: first_use_tree.prefix.span(), + list_is_empty: false, + }, + ); + } else { + self.insert_use_segment_position( + prefix, + UseSegmentPosition::BeforeList { + first_entry_span: Span::from( + use_tree.span.end() - 1..use_tree.span.end() - 1, + ), + list_is_empty: true, + }, + ); + } + } + } + } + + fn insert_use_segment_position(&mut self, segment: String, position: UseSegmentPosition) { + if self.use_segment_positions.get(&segment).is_none() { + self.use_segment_positions.insert(segment, position); + } else { + self.use_segment_positions.insert(segment, UseSegmentPosition::NoneOrMultiple); + } + } + fn includes_span(&self, span: Span) -> bool { span.start() as usize <= self.byte_index && self.byte_index <= span.end() as usize } @@ -1042,10 +1201,11 @@ impl<'a> NodeFinder<'a> { impl<'a> Visitor for NodeFinder<'a> { fn visit_item(&mut self, item: &Item) -> bool { - if let ItemKind::Import(..) = &item.kind { + if let ItemKind::Import(use_tree, _) = &item.kind { if let Some(lsp_location) = to_lsp_location(self.files, self.file, item.span) { self.auto_import_line = (lsp_location.range.end.line + 1) as usize; } + self.gather_use_tree_segments(use_tree, String::new()); } self.includes_span(item.span) diff --git a/tooling/lsp/src/requests/completion/auto_import.rs b/tooling/lsp/src/requests/completion/auto_import.rs index e2dd582f2f3..7a9f51cca74 100644 --- a/tooling/lsp/src/requests/completion/auto_import.rs +++ b/tooling/lsp/src/requests/completion/auto_import.rs @@ -1,13 +1,18 @@ use lsp_types::{Position, Range, TextEdit}; + +use noirc_errors::Span; use noirc_frontend::hir::def_map::ModuleDefId; -use crate::modules::{relative_module_full_path, relative_module_id_path}; +use crate::{ + modules::{relative_module_full_path, relative_module_id_path}, + requests::to_lsp_location, +}; use super::{ kinds::{FunctionCompletionKind, FunctionKind, RequestedItems}, name_matches, sort_text::auto_import_sort_text, - NodeFinder, + NodeFinder, UseSegmentPosition, }; impl<'a> NodeFinder<'a> { @@ -77,26 +82,114 @@ impl<'a> NodeFinder<'a> { label_details.detail = Some(format!("(use {})", full_path)); completion_item.label_details = Some(label_details); - let line = self.auto_import_line as u32; - let character = (self.nesting * 4) as u32; - let indent = " ".repeat(self.nesting * 4); - let mut newlines = "\n"; - - // If the line we are inserting into is not an empty line, insert an extra line to make some room - if let Some(line_text) = self.lines.get(line as usize) { - if !line_text.trim().is_empty() { - newlines = "\n\n"; + // See if there's a single place where one of the parent paths is located + let (use_segment_position, name) = + self.find_use_segment_position(full_path.clone()); + match use_segment_position { + UseSegmentPosition::NoneOrMultiple => { + // The parent path either isn't in any use statement, or it exists in multiple + // use statements. In either case we'll add a new use statement. + completion_item.additional_text_edits = + Some(self.new_use_completion_item_additional_text_edits(full_path)); + } + UseSegmentPosition::Last { span } => { + // We have + // + // use foo::bar; + // ^^^ -> span + // + // and we want to transform it to: + // + // use foo::bar::{self, baz}; + // ^^^^^^^^^^^^^ + // + // So we need one text edit: + // 1. insert "::{self, baz}" right after the span + if let Some(lsp_location) = to_lsp_location(self.files, self.file, span) + { + let range = lsp_location.range; + completion_item.additional_text_edits = Some(vec![TextEdit { + new_text: format!("::{{self, {}}}", name), + range: Range { start: range.end, end: range.end }, + }]); + } else { + completion_item.additional_text_edits = Some( + self.new_use_completion_item_additional_text_edits(full_path), + ); + } + } + UseSegmentPosition::BeforeSegment { segment_span_until_end } => { + // Go past the end + let segment_span_until_end = Span::from( + segment_span_until_end.start()..segment_span_until_end.end() + 1, + ); + + // We have + // + // use foo::bar::{one, two}; + // ^^^^^^^^^^^^^^^ -> segment_span_until_end + // + // and we want to transform it to: + // + // use foo::{bar::{one, two}, baz}; + // ^ ^^^^^^ + // + // So we need two text edits: + // 1. insert "{" right before the segment span + // 2. insert ", baz}" right after the segment span + if let Some(lsp_location) = + to_lsp_location(self.files, self.file, segment_span_until_end) + { + let range = lsp_location.range; + completion_item.additional_text_edits = Some(vec![ + TextEdit { + new_text: "{".to_string(), + range: Range { start: range.start, end: range.start }, + }, + TextEdit { + new_text: format!(", {}}}", name), + range: Range { start: range.end, end: range.end }, + }, + ]); + } else { + completion_item.additional_text_edits = Some( + self.new_use_completion_item_additional_text_edits(full_path), + ); + } + } + UseSegmentPosition::BeforeList { first_entry_span, list_is_empty } => { + // We have + // + // use foo::bar::{one, two}; + // ^^^ -> first_entry_span + // + // and we want to transform it to: + // + // use foo::bar::{baz, one, two}; + // ^^^^ + // + // So we need one text edit: + // 1. insert "baz, " right before the first entry span + if let Some(lsp_location) = + to_lsp_location(self.files, self.file, first_entry_span) + { + let range = lsp_location.range; + completion_item.additional_text_edits = Some(vec![TextEdit { + new_text: if list_is_empty { + name + } else { + format!("{}, ", name) + }, + range: Range { start: range.start, end: range.start }, + }]); + } else { + completion_item.additional_text_edits = Some( + self.new_use_completion_item_additional_text_edits(full_path), + ); + } } } - completion_item.additional_text_edits = Some(vec![TextEdit { - range: Range { - start: Position { line, character }, - end: Position { line, character }, - }, - new_text: format!("use {};{}{}", full_path, newlines, indent), - }]); - completion_item.sort_text = Some(auto_import_sort_text()); self.completion_items.push(completion_item); @@ -104,4 +197,50 @@ impl<'a> NodeFinder<'a> { } } } + + fn new_use_completion_item_additional_text_edits(&self, full_path: String) -> Vec { + let line = self.auto_import_line as u32; + let character = (self.nesting * 4) as u32; + let indent = " ".repeat(self.nesting * 4); + let mut newlines = "\n"; + + // If the line we are inserting into is not an empty line, insert an extra line to make some room + if let Some(line_text) = self.lines.get(line as usize) { + if !line_text.trim().is_empty() { + newlines = "\n\n"; + } + } + + vec![TextEdit { + range: Range { start: Position { line, character }, end: Position { line, character } }, + new_text: format!("use {};{}{}", full_path, newlines, indent), + }] + } + + /// Given a full path like `foo::bar::baz`, returns the first non-"NoneOrMultiple" segment position + /// trying each successive parent, together with the name after the parent. + /// + /// For example, first we'll check if `foo::bar` has a single position. If not, we'll try with `foo`. + fn find_use_segment_position(&self, full_path: String) -> (UseSegmentPosition, String) { + // Build a parent path to know in which full segment we need to add this import + let mut segments: Vec<_> = full_path.split("::").collect(); + let mut name = segments.pop().unwrap().to_string(); + let mut parent_path = segments.join("::"); + + loop { + let use_segment_position = + self.use_segment_positions.get(&parent_path).cloned().unwrap_or_default(); + + if let UseSegmentPosition::NoneOrMultiple = use_segment_position { + if let Some(next_name) = segments.pop() { + name = format!("{next_name}::{name}"); + parent_path = segments.join("::"); + } else { + return (UseSegmentPosition::NoneOrMultiple, String::new()); + } + } else { + return (use_segment_position, name); + } + } + } } diff --git a/tooling/lsp/src/requests/completion/tests.rs b/tooling/lsp/src/requests/completion/tests.rs index 668255eb34d..322dbf932b5 100644 --- a/tooling/lsp/src/requests/completion/tests.rs +++ b/tooling/lsp/src/requests/completion/tests.rs @@ -15,12 +15,13 @@ mod completion_tests { on_completion_request, }, test_utils, + tests::apply_text_edits, }; use lsp_types::{ CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionParams, - CompletionResponse, DidOpenTextDocumentParams, PartialResultParams, Position, Range, - TextDocumentIdentifier, TextDocumentItem, TextDocumentPositionParams, TextEdit, + CompletionResponse, DidOpenTextDocumentParams, PartialResultParams, Position, + TextDocumentIdentifier, TextDocumentItem, TextDocumentPositionParams, WorkDoneProgressParams, }; use tokio::test; @@ -1392,29 +1393,50 @@ mod completion_tests { #[test] async fn test_auto_imports() { - let src = r#" - mod foo { - mod bar { - pub fn hello_world() {} + let src = r#"mod foo { + mod bar { + pub fn hello_world() {} - struct Foo { - } + struct Foo { + } - impl Foo { - // This is here to make sure it's not offered for completion - fn hello_world() {} - } - } - } + impl Foo { + // This is here to make sure it's not offered for completion + fn hello_world() {} + } + } +} - fn main() { - hel>|< - } +fn main() { + hel>|< +} "#; - let items = get_completions(src).await; + + let expected = r#"use foo::bar::hello_world; + +mod foo { + mod bar { + pub fn hello_world() {} + + struct Foo { + } + + impl Foo { + // This is here to make sure it's not offered for completion + fn hello_world() {} + } + } +} + +fn main() { + hel +} + "#; + + let mut items = get_completions(src).await; assert_eq!(items.len(), 1); - let item = &items[0]; + let item = items.remove(0); assert_eq!(item.label, "hello_world()"); assert_eq!( item.label_details, @@ -1424,38 +1446,43 @@ mod completion_tests { }) ); - assert_eq!( - item.additional_text_edits, - Some(vec![TextEdit { - range: Range { - start: Position { line: 0, character: 0 }, - end: Position { line: 0, character: 0 }, - }, - new_text: "use foo::bar::hello_world;\n".to_string(), - }]) - ); - + let changed = + apply_text_edits(&src.replace(">|<", ""), &mut item.additional_text_edits.unwrap()); + assert_eq!(changed, expected); assert_eq!(item.sort_text, Some(auto_import_sort_text())); } #[test] async fn test_auto_imports_when_in_nested_module_and_item_is_further_nested() { - let src = r#" - #[something] - mod foo { - mod bar { - pub fn hello_world() {} - } + let src = r#"#[something] +mod foo { + mod bar { + pub fn hello_world() {} + } - fn foo() { - hel>|< - } - } + fn foo() { + hel>|< + } +} "#; - let items = get_completions(src).await; + + let expected = r#"#[something] +mod foo { + use bar::hello_world; + + mod bar { + pub fn hello_world() {} + } + + fn foo() { + hel + } +} + "#; + let mut items = get_completions(src).await; assert_eq!(items.len(), 1); - let item = &items[0]; + let item = items.remove(0); assert_eq!(item.label, "hello_world()"); assert_eq!( item.label_details, @@ -1465,37 +1492,42 @@ mod completion_tests { }) ); - assert_eq!( - item.additional_text_edits, - Some(vec![TextEdit { - range: Range { - start: Position { line: 3, character: 4 }, - end: Position { line: 3, character: 4 }, - }, - new_text: "use bar::hello_world;\n\n ".to_string(), - }]) - ); + let changed = + apply_text_edits(&src.replace(">|<", ""), &mut item.additional_text_edits.unwrap()); + assert_eq!(changed, expected); } #[test] async fn test_auto_imports_when_in_nested_module_and_item_is_not_further_nested() { - let src = r#" - mod foo { - mod bar { - pub fn hello_world() {} - } + let src = r#"mod foo { + mod bar { + pub fn hello_world() {} + } - mod baz { - fn foo() { - hel>|< - } - } - } - "#; - let items = get_completions(src).await; + mod baz { + fn foo() { + hel>|< + } + } +}"#; + + let expected = r#"mod foo { + mod bar { + pub fn hello_world() {} + } + + mod baz { + use super::bar::hello_world; + + fn foo() { + hel + } + } +}"#; + let mut items = get_completions(src).await; assert_eq!(items.len(), 1); - let item = &items[0]; + let item = items.remove(0); assert_eq!(item.label, "hello_world()"); assert_eq!( item.label_details, @@ -1505,47 +1537,53 @@ mod completion_tests { }) ); - assert_eq!( - item.additional_text_edits, - Some(vec![TextEdit { - range: Range { - start: Position { line: 7, character: 8 }, - end: Position { line: 7, character: 8 }, - }, - new_text: "use super::bar::hello_world;\n\n ".to_string(), - }]) - ); + let changed = + apply_text_edits(&src.replace(">|<", ""), &mut item.additional_text_edits.unwrap()); + assert_eq!(changed, expected); } #[test] async fn test_auto_import_inserts_after_last_use() { - let src = r#" - mod foo { - mod bar { - pub fn hello_world() {} - } - } + let src = r#"mod foo { + mod bar { + pub fn hello_world() {} + } +} - use foo::bar; +mod baz { + fn qux() {} +} - fn main() { - hel>|< - } - "#; - let items = get_completions(src).await; +use baz::qux; + +fn main() { + hel>|< +}"#; + + let expected = r#"mod foo { + mod bar { + pub fn hello_world() {} + } +} + +mod baz { + fn qux() {} +} + +use baz::qux; +use foo::bar::hello_world; + +fn main() { + hel +}"#; + let mut items = get_completions(src).await; assert_eq!(items.len(), 1); - let item = &items[0]; - assert_eq!( - item.additional_text_edits, - Some(vec![TextEdit { - range: Range { - start: Position { line: 8, character: 0 }, - end: Position { line: 8, character: 0 }, - }, - new_text: "use foo::bar::hello_world;\n".to_string(), - }]) - ); + let item = items.remove(0); + + let changed = + apply_text_edits(&src.replace(">|<", ""), &mut item.additional_text_edits.unwrap()); + assert_eq!(changed, expected); } #[test] @@ -1589,22 +1627,36 @@ mod completion_tests { #[test] async fn test_auto_import_suggests_modules_too() { - let src = r#" - mod foo { - pub mod barbaz { - fn hello_world() {} - } - } + let src = r#"mod foo { + pub mod barbaz { + fn hello_world() {} + } + } - fn main() { - barb>|< - } - "#; - let items = get_completions(src).await; + fn main() { + barb>|< + } +}"#; + + let expected = r#"use foo::barbaz; + +mod foo { + pub mod barbaz { + fn hello_world() {} + } + } + + fn main() { + barb + } +}"#; + + let mut items = get_completions(src).await; assert_eq!(items.len(), 1); - let item = &items[0]; + let item = items.remove(0); assert_eq!(item.label, "barbaz"); + assert_eq!( item.label_details, Some(CompletionItemLabelDetails { @@ -1612,6 +1664,286 @@ mod completion_tests { description: None }) ); + + let changed = + apply_text_edits(&src.replace(">|<", ""), &mut item.additional_text_edits.unwrap()); + assert_eq!(changed, expected); + } + + #[test] + async fn test_auto_imports_expands_existing_use_before_one_segment_not_in_list() { + let src = r#"use foo::bar::one_hello_world; + +mod foo { + mod bar { + pub fn one_hello_world() {} + pub fn two_hello_world() {} + } +} + +fn main() { + two_hello_>|< +}"#; + + let expected = r#"use foo::bar::{one_hello_world, two_hello_world}; + +mod foo { + mod bar { + pub fn one_hello_world() {} + pub fn two_hello_world() {} + } +} + +fn main() { + two_hello_ +}"#; + + let mut items = get_completions(src).await; + assert_eq!(items.len(), 1); + + let item = items.remove(0); + let changed = + apply_text_edits(&src.replace(">|<", ""), &mut item.additional_text_edits.unwrap()); + assert_eq!(changed, expected); + assert_eq!(item.sort_text, Some(auto_import_sort_text())); + } + + #[test] + async fn test_auto_imports_expands_existing_use_before_two_segments() { + let src = r#"use foo::bar::one_hello_world; + +mod foo { + mod bar { + pub fn one_hello_world() {} + } + pub fn two_hello_world() {} +} + +fn main() { + two_hello_>|< +}"#; + + let expected = r#"use foo::{bar::one_hello_world, two_hello_world}; + +mod foo { + mod bar { + pub fn one_hello_world() {} + } + pub fn two_hello_world() {} +} + +fn main() { + two_hello_ +}"#; + + let mut items = get_completions(src).await; + assert_eq!(items.len(), 1); + + let item = items.remove(0); + let changed = + apply_text_edits(&src.replace(">|<", ""), &mut item.additional_text_edits.unwrap()); + assert_eq!(changed, expected); + assert_eq!(item.sort_text, Some(auto_import_sort_text())); + } + + #[test] + async fn test_auto_imports_expands_existing_use_before_one_segment_inside_list() { + let src = r#"use foo::{bar::one_hello_world, baz}; + +mod foo { + mod bar { + pub fn one_hello_world() {} + pub fn two_hello_world() {} + } + mod baz {} +} + +fn main() { + two_hello_>|< +}"#; + + let expected = r#"use foo::{bar::{one_hello_world, two_hello_world}, baz}; + +mod foo { + mod bar { + pub fn one_hello_world() {} + pub fn two_hello_world() {} + } + mod baz {} +} + +fn main() { + two_hello_ +}"#; + + let mut items = get_completions(src).await; + assert_eq!(items.len(), 1); + + let item = items.remove(0); + let changed = + apply_text_edits(&src.replace(">|<", ""), &mut item.additional_text_edits.unwrap()); + assert_eq!(changed, expected); + assert_eq!(item.sort_text, Some(auto_import_sort_text())); + } + + #[test] + async fn test_auto_imports_expands_existing_use_before_one_segment_checks_parents() { + let src = r#"use foo::bar::baz; + +mod foo { + mod bar { + mod baz { + pub fn one_hello_world() {} + } + mod qux { + pub fn two_hello_world() {} + } + } +} + +fn main() { + two_hello_>|< +}"#; + + let expected = r#"use foo::bar::{baz, qux::two_hello_world}; + +mod foo { + mod bar { + mod baz { + pub fn one_hello_world() {} + } + mod qux { + pub fn two_hello_world() {} + } + } +} + +fn main() { + two_hello_ +}"#; + + let mut items = get_completions(src).await; + assert_eq!(items.len(), 1); + + let item = items.remove(0); + let changed = + apply_text_edits(&src.replace(">|<", ""), &mut item.additional_text_edits.unwrap()); + assert_eq!(changed, expected); + assert_eq!(item.sort_text, Some(auto_import_sort_text())); + } + + #[test] + async fn test_auto_imports_expands_existing_use_last_segment() { + let src = r#"use foo::bar; + +mod foo { + mod bar { + pub fn one_hello_world() {} + pub fn two_hello_world() {} + } +} + +fn main() { + two_hello_>|< +}"#; + + let expected = r#"use foo::bar::{self, two_hello_world}; + +mod foo { + mod bar { + pub fn one_hello_world() {} + pub fn two_hello_world() {} + } +} + +fn main() { + two_hello_ +}"#; + + let mut items = get_completions(src).await; + assert_eq!(items.len(), 1); + + let item = items.remove(0); + let changed = + apply_text_edits(&src.replace(">|<", ""), &mut item.additional_text_edits.unwrap()); + assert_eq!(changed, expected); + assert_eq!(item.sort_text, Some(auto_import_sort_text())); + } + + #[test] + async fn test_auto_imports_expands_existing_use_before_list() { + let src = r#"use foo::bar::{one_hello_world, three_hello_world}; + +mod foo { + mod bar { + pub fn one_hello_world() {} + pub fn two_hello_world() {} + pub fn three_hello_world() {} + } +} + +fn main() { + two_hello_>|< +}"#; + + let expected = r#"use foo::bar::{two_hello_world, one_hello_world, three_hello_world}; + +mod foo { + mod bar { + pub fn one_hello_world() {} + pub fn two_hello_world() {} + pub fn three_hello_world() {} + } +} + +fn main() { + two_hello_ +}"#; + + let mut items = get_completions(src).await; + assert_eq!(items.len(), 1); + + let item = items.remove(0); + let changed = + apply_text_edits(&src.replace(">|<", ""), &mut item.additional_text_edits.unwrap()); + assert_eq!(changed, expected); + assert_eq!(item.sort_text, Some(auto_import_sort_text())); + } + + #[test] + async fn test_auto_imports_expands_existing_use_before_empty_list() { + let src = r#"use foo::bar::{}; + +mod foo { + mod bar { + pub fn two_hello_world() {} + } +} + +fn main() { + two_hello_>|< +}"#; + + let expected = r#"use foo::bar::{two_hello_world}; + +mod foo { + mod bar { + pub fn two_hello_world() {} + } +} + +fn main() { + two_hello_ +}"#; + + let mut items = get_completions(src).await; + assert_eq!(items.len(), 1); + + let item = items.remove(0); + let changed = + apply_text_edits(&src.replace(">|<", ""), &mut item.additional_text_edits.unwrap()); + assert_eq!(changed, expected); + assert_eq!(item.sort_text, Some(auto_import_sort_text())); } #[test] diff --git a/tooling/lsp/src/tests.rs b/tooling/lsp/src/tests.rs new file mode 100644 index 00000000000..2788f848262 --- /dev/null +++ b/tooling/lsp/src/tests.rs @@ -0,0 +1,32 @@ +#![cfg(test)] + +use lsp_types::TextEdit; + +pub(crate) fn apply_text_edit(src: &str, text_edit: &TextEdit) -> String { + let mut lines: Vec<_> = src.lines().collect(); + assert_eq!(text_edit.range.start.line, text_edit.range.end.line); + + let mut line = lines[text_edit.range.start.line as usize].to_string(); + line.replace_range( + text_edit.range.start.character as usize..text_edit.range.end.character as usize, + &text_edit.new_text, + ); + lines[text_edit.range.start.line as usize] = &line; + lines.join("\n") +} + +pub(crate) fn apply_text_edits(src: &str, text_edits: &mut [TextEdit]) -> String { + let mut text = src.to_string(); + + // Text edits must be applied from last to first, otherwise if we apply a text edit + // that comes before another one, that other one becomes invalid (it will edit the wrong + // piece of code). + text_edits.sort_by_key(|edit| (edit.range.start.line, edit.range.start.character)); + text_edits.reverse(); + + for text_edit in text_edits { + text = apply_text_edit(&text, text_edit); + } + + text +} From 26879eea41bde893391511624638bb0332440f91 Mon Sep 17 00:00:00 2001 From: James Zaki Date: Fri, 25 Oct 2024 19:22:57 +0100 Subject: [PATCH 11/30] chore: minor tweaks to comptime doc (#6357) --- docs/docs/noir/concepts/comptime.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/docs/noir/concepts/comptime.md b/docs/docs/noir/concepts/comptime.md index cea3a896c17..37457d47b46 100644 --- a/docs/docs/noir/concepts/comptime.md +++ b/docs/docs/noir/concepts/comptime.md @@ -5,7 +5,7 @@ keywords: [Noir, comptime, compile-time, metaprogramming, macros, quote, unquote sidebar_position: 15 --- -# Overview +## Overview Metaprogramming in Noir is comprised of three parts: 1. `comptime` code @@ -21,7 +21,7 @@ for greater analysis and modification of programs. --- -# Comptime +## Comptime `comptime` is a new keyword in Noir which marks an item as executing or existing at compile-time. It can be used in several ways: @@ -32,11 +32,11 @@ for greater analysis and modification of programs. - `comptime let` to define a variable whose value is evaluated at compile-time. - `comptime for` to run a for loop at compile-time. Syntax sugar for `comptime { for .. }`. -## Scoping +### Scoping Note that while in a `comptime` context, any runtime variables _local to the current function_ are never visible. -## Evaluating +### Evaluating Evaluation rules of `comptime` follows the normal unconstrained evaluation rules for other Noir code. There are a few things to note though: @@ -62,7 +62,7 @@ For example, using globals to generate unique ids should be fine but relying on function itself was called at compile-time previously, it will already be resolved and cannot be modified. To prevent accidentally calling functions you wish to modify at compile-time, it may be helpful to sort your `comptime` annotation functions into a different crate along with any dependencies they require. -## Lowering +### Lowering When a `comptime` value is used in runtime code it must be lowered into a runtime value. This means replacing the expression with the literal that it evaluated to. For example, the code: @@ -102,7 +102,7 @@ comptime fn get_type() -> Type { ... } --- -# (Quasi) Quote +## (Quasi) Quote Macros in Noir are `comptime` functions which return code as a value which is inserted into the call site when it is lowered there. A code value in this case is of type `Quoted` and can be created by a `quote { ... }` expression. @@ -121,7 +121,7 @@ Calling such a function at compile-time without `!` will just return the `Quoted For those familiar with quoting from other languages (primarily lisps), Noir's `quote` is actually a _quasiquote_. This means we can escape the quoting by using the unquote operator to splice values in the middle of quoted code. -# Unquote +## Unquote The unquote operator `$` is usable within a `quote` expression. It takes a variable as an argument, evaluates the variable, and splices the resulting value into the quoted token stream at that point. For example, @@ -160,7 +160,7 @@ comptime { --- -# Annotations +## Annotations Annotations provide a way to run a `comptime` function on an item in the program. When you use an annotation, the function with the same name will be called with that item as an argument: @@ -188,7 +188,7 @@ For example, this is the mechanism used to insert additional trait implementatio #include_code derive-field-count-example noir_stdlib/src/meta/mod.nr rust -## Calling annotations with additional arguments +### Calling annotations with additional arguments Arguments may optionally be given to annotations. When this is done, these additional arguments are passed to the annotation function after the item argument. @@ -201,13 +201,13 @@ We can also take any number of arguments by adding the `varargs` annotation: --- -# Comptime API +## Comptime API Although `comptime`, `quote`, and unquoting provide a flexible base for writing macros, Noir's true metaprogramming ability comes from being able to interact with the compiler through a compile-time API. This API can be accessed through built-in functions in `std::meta` as well as on methods of several `comptime` types. -The following is an incomplete list of some `comptime` types along with some useful methods on them. +The following is an incomplete list of some `comptime` types along with some useful methods on them. You can see more in the standard library [Metaprogramming section](../standard_library/meta). - `Quoted`: A token stream - `Type`: The type of a Noir type @@ -238,7 +238,7 @@ The following is an incomplete list of some `comptime` types along with some use There are many more functions available by exploring the `std::meta` module and its submodules. Using these methods is the key to writing powerful metaprogramming libraries. -## `#[use_callers_scope]` +### `#[use_callers_scope]` Since certain functions such as `Quoted::as_type`, `Expression::as_type`, or `Quoted::as_trait_constraint` will attempt to resolve their contents in a particular scope - it can be useful to change the scope they resolve in. By default @@ -251,7 +251,7 @@ your attribute function and a helper function it calls use it, then they can bot --- -# Example: Derive +## Example: Derive Using all of the above, we can write a `derive` macro that behaves similarly to Rust's but is not built into the language. From the user's perspective it will look like this: From 308717b6c44db4b206ad371cd6322478ce68746b Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Fri, 25 Oct 2024 18:01:36 -0300 Subject: [PATCH 12/30] feat: let the LSP import code action insert into existing use statements (#6358) --- tooling/lsp/src/lib.rs | 1 + tooling/lsp/src/requests/code_action.rs | 7 +- .../requests/code_action/import_or_qualify.rs | 62 +++- tooling/lsp/src/requests/code_action/tests.rs | 5 +- tooling/lsp/src/requests/completion.rs | 163 +-------- .../src/requests/completion/auto_import.rs | 176 +-------- tooling/lsp/src/requests/completion/tests.rs | 24 +- tooling/lsp/src/requests/mod.rs | 2 +- tooling/lsp/src/tests.rs | 5 +- tooling/lsp/src/use_segment_positions.rs | 336 ++++++++++++++++++ 10 files changed, 424 insertions(+), 357 deletions(-) create mode 100644 tooling/lsp/src/use_segment_positions.rs diff --git a/tooling/lsp/src/lib.rs b/tooling/lsp/src/lib.rs index 7254e3a5f77..a85b9d043b9 100644 --- a/tooling/lsp/src/lib.rs +++ b/tooling/lsp/src/lib.rs @@ -70,6 +70,7 @@ mod solver; mod tests; mod trait_impl_method_stub_generator; mod types; +mod use_segment_positions; mod utils; mod visibility; diff --git a/tooling/lsp/src/requests/code_action.rs b/tooling/lsp/src/requests/code_action.rs index 9299dc76368..f3e9130e17d 100644 --- a/tooling/lsp/src/requests/code_action.rs +++ b/tooling/lsp/src/requests/code_action.rs @@ -22,7 +22,7 @@ use noirc_frontend::{ ParsedModule, }; -use crate::{utils, LspState}; +use crate::{use_segment_positions::UseSegmentPositions, utils, LspState}; use super::{process_request, to_lsp_location}; @@ -82,6 +82,7 @@ struct CodeActionFinder<'a> { nesting: usize, /// The line where an auto_import must be inserted auto_import_line: usize, + use_segment_positions: UseSegmentPositions, /// Text edits for the "Remove all unused imports" code action unused_imports_text_edits: Vec, code_actions: Vec, @@ -121,6 +122,7 @@ impl<'a> CodeActionFinder<'a> { interner, nesting: 0, auto_import_line: 0, + use_segment_positions: UseSegmentPositions::default(), unused_imports_text_edits: vec![], code_actions: vec![], } @@ -184,10 +186,11 @@ impl<'a> CodeActionFinder<'a> { impl<'a> Visitor for CodeActionFinder<'a> { fn visit_item(&mut self, item: &Item) -> bool { - if let ItemKind::Import(..) = &item.kind { + if let ItemKind::Import(use_tree, _) = &item.kind { if let Some(lsp_location) = to_lsp_location(self.files, self.file, item.span) { self.auto_import_line = (lsp_location.range.end.line + 1) as usize; } + self.use_segment_positions.add(use_tree); } self.includes_span(item.span) diff --git a/tooling/lsp/src/requests/code_action/import_or_qualify.rs b/tooling/lsp/src/requests/code_action/import_or_qualify.rs index bf1fe906be3..a3053f8f304 100644 --- a/tooling/lsp/src/requests/code_action/import_or_qualify.rs +++ b/tooling/lsp/src/requests/code_action/import_or_qualify.rs @@ -1,4 +1,4 @@ -use lsp_types::{Position, Range, TextEdit}; +use lsp_types::TextEdit; use noirc_errors::Location; use noirc_frontend::{ ast::{Ident, Path}, @@ -8,6 +8,9 @@ use noirc_frontend::{ use crate::{ byte_span_to_range, modules::{relative_module_full_path, relative_module_id_path}, + use_segment_positions::{ + use_completion_item_additional_text_edits, UseCompletionItemAdditionTextEditsRequest, + }, }; use super::CodeActionFinder; @@ -82,25 +85,21 @@ impl<'a> CodeActionFinder<'a> { } fn push_import_code_action(&mut self, full_path: &str) { - let line = self.auto_import_line as u32; - let character = (self.nesting * 4) as u32; - let indent = " ".repeat(self.nesting * 4); - let mut newlines = "\n"; - - // If the line we are inserting into is not an empty line, insert an extra line to make some room - if let Some(line_text) = self.lines.get(line as usize) { - if !line_text.trim().is_empty() { - newlines = "\n\n"; - } - } - let title = format!("Import {}", full_path); - let text_edit = TextEdit { - range: Range { start: Position { line, character }, end: Position { line, character } }, - new_text: format!("use {};{}{}", full_path, newlines, indent), - }; - let code_action = self.new_quick_fix(title, text_edit); + let text_edits = use_completion_item_additional_text_edits( + UseCompletionItemAdditionTextEditsRequest { + full_path, + files: self.files, + file: self.file, + lines: &self.lines, + nesting: self.nesting, + auto_import_line: self.auto_import_line, + }, + &self.use_segment_positions, + ); + + let code_action = self.new_quick_fix_multiple_edits(title, text_edits); self.code_actions.push(code_action); } @@ -238,4 +237,31 @@ fn main() { assert_code_action(title, src, expected).await; } + + #[test] + async fn test_import_code_action_for_struct_inserts_into_existing_use() { + let title = "Import foo::bar::SomeTypeInBar"; + + let src = r#"use foo::bar::SomeOtherType; + +mod foo { + mod bar { + pub struct SomeTypeInBar {} + } +} + +fn foo(x: SomeType>||<", ""), &text_edits[0]); + let result = apply_text_edits(&src.replace(">|<", ""), text_edits); if result != expected { println!("Expected:\n```\n{}\n```\n\nGot:\n```\n{}\n```", expected, result); assert_eq!(result, expected); diff --git a/tooling/lsp/src/requests/completion.rs b/tooling/lsp/src/requests/completion.rs index d0e0efd97bc..b69b261c9c6 100644 --- a/tooling/lsp/src/requests/completion.rs +++ b/tooling/lsp/src/requests/completion.rs @@ -38,7 +38,7 @@ use sort_text::underscore_sort_text; use crate::{ requests::to_lsp_location, trait_impl_method_stub_generator::TraitImplMethodStubGenerator, - utils, visibility::is_visible, LspState, + use_segment_positions::UseSegmentPositions, utils, visibility::is_visible, LspState, }; use super::process_request; @@ -88,42 +88,6 @@ pub(crate) fn on_completion_request( future::ready(result) } -/// The position of a segment in a `use` statement. -/// We use this to determine how an auto-import should be inserted. -#[derive(Debug, Default, Copy, Clone)] -enum UseSegmentPosition { - /// The segment either doesn't exist in the source code or there are multiple segments. - /// In this case auto-import will add a new use statement. - #[default] - NoneOrMultiple, - /// The segment is the last one in the `use` statement (or nested use statement): - /// - /// use foo::bar; - /// ^^^ - /// - /// Auto-import will transform it to this: - /// - /// use foo::bar::{self, baz}; - Last { span: Span }, - /// The segment happens before another simple (ident) segment: - /// - /// use foo::bar::qux; - /// ^^^ - /// - /// Auto-import will transform it to this: - /// - /// use foo::bar::{qux, baz}; - BeforeSegment { segment_span_until_end: Span }, - /// The segment happens before a list: - /// - /// use foo::bar::{qux, another}; - /// - /// Auto-import will transform it to this: - /// - /// use foo::bar::{qux, another, baz}; - BeforeList { first_entry_span: Span, list_is_empty: bool }, -} - struct NodeFinder<'a> { files: &'a FileMap, file: FileId, @@ -151,11 +115,7 @@ struct NodeFinder<'a> { nesting: usize, /// The line where an auto_import must be inserted auto_import_line: usize, - /// Remember where each segment in a `use` statement is located. - /// The key is the full segment, so for `use foo::bar::baz` we'll have three - /// segments: `foo`, `foo::bar` and `foo::bar::baz`, where the span is just - /// for the last identifier (`foo`, `bar` and `baz` in the previous example). - use_segment_positions: HashMap, + use_segment_positions: UseSegmentPositions, self_type: Option, in_comptime: bool, } @@ -200,7 +160,7 @@ impl<'a> NodeFinder<'a> { suggested_module_def_ids: HashSet::new(), nesting: 0, auto_import_line: 0, - use_segment_positions: HashMap::new(), + use_segment_positions: UseSegmentPositions::default(), self_type: None, in_comptime: false, } @@ -1078,121 +1038,6 @@ impl<'a> NodeFinder<'a> { } /// Determine where each segment in a `use` statement is located. - fn gather_use_tree_segments(&mut self, use_tree: &UseTree, mut prefix: String) { - let kind_string = match use_tree.prefix.kind { - PathKind::Crate => Some("crate".to_string()), - PathKind::Super => Some("super".to_string()), - PathKind::Dep | PathKind::Plain => None, - }; - if let Some(kind_string) = kind_string { - if let Some(segment) = use_tree.prefix.segments.first() { - self.insert_use_segment_position( - kind_string, - UseSegmentPosition::BeforeSegment { - segment_span_until_end: Span::from( - segment.ident.span().start()..use_tree.span.end() - 1, - ), - }, - ); - } else { - self.insert_use_segment_position_before_use_tree_kind(use_tree, kind_string); - } - } - - let prefix_segments_len = use_tree.prefix.segments.len(); - for (index, segment) in use_tree.prefix.segments.iter().enumerate() { - let ident = &segment.ident; - if !prefix.is_empty() { - prefix.push_str("::"); - }; - prefix.push_str(&ident.0.contents); - - if index < prefix_segments_len - 1 { - self.insert_use_segment_position( - prefix.clone(), - UseSegmentPosition::BeforeSegment { - segment_span_until_end: Span::from( - use_tree.prefix.segments[index + 1].ident.span().start() - ..use_tree.span.end() - 1, - ), - }, - ); - } else { - self.insert_use_segment_position_before_use_tree_kind(use_tree, prefix.clone()); - } - } - - match &use_tree.kind { - UseTreeKind::Path(ident, alias) => { - if !prefix.is_empty() { - prefix.push_str("::"); - } - prefix.push_str(&ident.0.contents); - - if alias.is_none() { - self.insert_use_segment_position( - prefix, - UseSegmentPosition::Last { span: ident.span() }, - ); - } else { - self.insert_use_segment_position(prefix, UseSegmentPosition::NoneOrMultiple); - } - } - UseTreeKind::List(use_trees) => { - for use_tree in use_trees { - self.gather_use_tree_segments(use_tree, prefix.clone()); - } - } - } - } - - fn insert_use_segment_position_before_use_tree_kind( - &mut self, - use_tree: &UseTree, - prefix: String, - ) { - match &use_tree.kind { - UseTreeKind::Path(ident, _alias) => { - self.insert_use_segment_position( - prefix, - UseSegmentPosition::BeforeSegment { - segment_span_until_end: Span::from( - ident.span().start()..use_tree.span.end() - 1, - ), - }, - ); - } - UseTreeKind::List(use_trees) => { - if let Some(first_use_tree) = use_trees.first() { - self.insert_use_segment_position( - prefix, - UseSegmentPosition::BeforeList { - first_entry_span: first_use_tree.prefix.span(), - list_is_empty: false, - }, - ); - } else { - self.insert_use_segment_position( - prefix, - UseSegmentPosition::BeforeList { - first_entry_span: Span::from( - use_tree.span.end() - 1..use_tree.span.end() - 1, - ), - list_is_empty: true, - }, - ); - } - } - } - } - - fn insert_use_segment_position(&mut self, segment: String, position: UseSegmentPosition) { - if self.use_segment_positions.get(&segment).is_none() { - self.use_segment_positions.insert(segment, position); - } else { - self.use_segment_positions.insert(segment, UseSegmentPosition::NoneOrMultiple); - } - } fn includes_span(&self, span: Span) -> bool { span.start() as usize <= self.byte_index && self.byte_index <= span.end() as usize @@ -1205,7 +1050,7 @@ impl<'a> Visitor for NodeFinder<'a> { if let Some(lsp_location) = to_lsp_location(self.files, self.file, item.span) { self.auto_import_line = (lsp_location.range.end.line + 1) as usize; } - self.gather_use_tree_segments(use_tree, String::new()); + self.use_segment_positions.add(use_tree); } self.includes_span(item.span) diff --git a/tooling/lsp/src/requests/completion/auto_import.rs b/tooling/lsp/src/requests/completion/auto_import.rs index 7a9f51cca74..9ed633289c1 100644 --- a/tooling/lsp/src/requests/completion/auto_import.rs +++ b/tooling/lsp/src/requests/completion/auto_import.rs @@ -1,18 +1,17 @@ -use lsp_types::{Position, Range, TextEdit}; - -use noirc_errors::Span; use noirc_frontend::hir::def_map::ModuleDefId; use crate::{ modules::{relative_module_full_path, relative_module_id_path}, - requests::to_lsp_location, + use_segment_positions::{ + use_completion_item_additional_text_edits, UseCompletionItemAdditionTextEditsRequest, + }, }; use super::{ kinds::{FunctionCompletionKind, FunctionKind, RequestedItems}, name_matches, sort_text::auto_import_sort_text, - NodeFinder, UseSegmentPosition, + NodeFinder, }; impl<'a> NodeFinder<'a> { @@ -81,115 +80,18 @@ impl<'a> NodeFinder<'a> { let mut label_details = completion_item.label_details.unwrap(); label_details.detail = Some(format!("(use {})", full_path)); completion_item.label_details = Some(label_details); - - // See if there's a single place where one of the parent paths is located - let (use_segment_position, name) = - self.find_use_segment_position(full_path.clone()); - match use_segment_position { - UseSegmentPosition::NoneOrMultiple => { - // The parent path either isn't in any use statement, or it exists in multiple - // use statements. In either case we'll add a new use statement. - completion_item.additional_text_edits = - Some(self.new_use_completion_item_additional_text_edits(full_path)); - } - UseSegmentPosition::Last { span } => { - // We have - // - // use foo::bar; - // ^^^ -> span - // - // and we want to transform it to: - // - // use foo::bar::{self, baz}; - // ^^^^^^^^^^^^^ - // - // So we need one text edit: - // 1. insert "::{self, baz}" right after the span - if let Some(lsp_location) = to_lsp_location(self.files, self.file, span) - { - let range = lsp_location.range; - completion_item.additional_text_edits = Some(vec![TextEdit { - new_text: format!("::{{self, {}}}", name), - range: Range { start: range.end, end: range.end }, - }]); - } else { - completion_item.additional_text_edits = Some( - self.new_use_completion_item_additional_text_edits(full_path), - ); - } - } - UseSegmentPosition::BeforeSegment { segment_span_until_end } => { - // Go past the end - let segment_span_until_end = Span::from( - segment_span_until_end.start()..segment_span_until_end.end() + 1, - ); - - // We have - // - // use foo::bar::{one, two}; - // ^^^^^^^^^^^^^^^ -> segment_span_until_end - // - // and we want to transform it to: - // - // use foo::{bar::{one, two}, baz}; - // ^ ^^^^^^ - // - // So we need two text edits: - // 1. insert "{" right before the segment span - // 2. insert ", baz}" right after the segment span - if let Some(lsp_location) = - to_lsp_location(self.files, self.file, segment_span_until_end) - { - let range = lsp_location.range; - completion_item.additional_text_edits = Some(vec![ - TextEdit { - new_text: "{".to_string(), - range: Range { start: range.start, end: range.start }, - }, - TextEdit { - new_text: format!(", {}}}", name), - range: Range { start: range.end, end: range.end }, - }, - ]); - } else { - completion_item.additional_text_edits = Some( - self.new_use_completion_item_additional_text_edits(full_path), - ); - } - } - UseSegmentPosition::BeforeList { first_entry_span, list_is_empty } => { - // We have - // - // use foo::bar::{one, two}; - // ^^^ -> first_entry_span - // - // and we want to transform it to: - // - // use foo::bar::{baz, one, two}; - // ^^^^ - // - // So we need one text edit: - // 1. insert "baz, " right before the first entry span - if let Some(lsp_location) = - to_lsp_location(self.files, self.file, first_entry_span) - { - let range = lsp_location.range; - completion_item.additional_text_edits = Some(vec![TextEdit { - new_text: if list_is_empty { - name - } else { - format!("{}, ", name) - }, - range: Range { start: range.start, end: range.start }, - }]); - } else { - completion_item.additional_text_edits = Some( - self.new_use_completion_item_additional_text_edits(full_path), - ); - } - } - } - + completion_item.additional_text_edits = + Some(use_completion_item_additional_text_edits( + UseCompletionItemAdditionTextEditsRequest { + full_path: &full_path, + files: self.files, + file: self.file, + lines: &self.lines, + nesting: self.nesting, + auto_import_line: self.auto_import_line, + }, + &self.use_segment_positions, + )); completion_item.sort_text = Some(auto_import_sort_text()); self.completion_items.push(completion_item); @@ -197,50 +99,4 @@ impl<'a> NodeFinder<'a> { } } } - - fn new_use_completion_item_additional_text_edits(&self, full_path: String) -> Vec { - let line = self.auto_import_line as u32; - let character = (self.nesting * 4) as u32; - let indent = " ".repeat(self.nesting * 4); - let mut newlines = "\n"; - - // If the line we are inserting into is not an empty line, insert an extra line to make some room - if let Some(line_text) = self.lines.get(line as usize) { - if !line_text.trim().is_empty() { - newlines = "\n\n"; - } - } - - vec![TextEdit { - range: Range { start: Position { line, character }, end: Position { line, character } }, - new_text: format!("use {};{}{}", full_path, newlines, indent), - }] - } - - /// Given a full path like `foo::bar::baz`, returns the first non-"NoneOrMultiple" segment position - /// trying each successive parent, together with the name after the parent. - /// - /// For example, first we'll check if `foo::bar` has a single position. If not, we'll try with `foo`. - fn find_use_segment_position(&self, full_path: String) -> (UseSegmentPosition, String) { - // Build a parent path to know in which full segment we need to add this import - let mut segments: Vec<_> = full_path.split("::").collect(); - let mut name = segments.pop().unwrap().to_string(); - let mut parent_path = segments.join("::"); - - loop { - let use_segment_position = - self.use_segment_positions.get(&parent_path).cloned().unwrap_or_default(); - - if let UseSegmentPosition::NoneOrMultiple = use_segment_position { - if let Some(next_name) = segments.pop() { - name = format!("{next_name}::{name}"); - parent_path = segments.join("::"); - } else { - return (UseSegmentPosition::NoneOrMultiple, String::new()); - } - } else { - return (use_segment_position, name); - } - } - } } diff --git a/tooling/lsp/src/requests/completion/tests.rs b/tooling/lsp/src/requests/completion/tests.rs index 322dbf932b5..be3a75f72c8 100644 --- a/tooling/lsp/src/requests/completion/tests.rs +++ b/tooling/lsp/src/requests/completion/tests.rs @@ -1447,7 +1447,7 @@ fn main() { ); let changed = - apply_text_edits(&src.replace(">|<", ""), &mut item.additional_text_edits.unwrap()); + apply_text_edits(&src.replace(">|<", ""), &item.additional_text_edits.unwrap()); assert_eq!(changed, expected); assert_eq!(item.sort_text, Some(auto_import_sort_text())); } @@ -1493,7 +1493,7 @@ mod foo { ); let changed = - apply_text_edits(&src.replace(">|<", ""), &mut item.additional_text_edits.unwrap()); + apply_text_edits(&src.replace(">|<", ""), &item.additional_text_edits.unwrap()); assert_eq!(changed, expected); } @@ -1538,7 +1538,7 @@ mod foo { ); let changed = - apply_text_edits(&src.replace(">|<", ""), &mut item.additional_text_edits.unwrap()); + apply_text_edits(&src.replace(">|<", ""), &item.additional_text_edits.unwrap()); assert_eq!(changed, expected); } @@ -1582,7 +1582,7 @@ fn main() { let item = items.remove(0); let changed = - apply_text_edits(&src.replace(">|<", ""), &mut item.additional_text_edits.unwrap()); + apply_text_edits(&src.replace(">|<", ""), &item.additional_text_edits.unwrap()); assert_eq!(changed, expected); } @@ -1666,7 +1666,7 @@ mod foo { ); let changed = - apply_text_edits(&src.replace(">|<", ""), &mut item.additional_text_edits.unwrap()); + apply_text_edits(&src.replace(">|<", ""), &item.additional_text_edits.unwrap()); assert_eq!(changed, expected); } @@ -1703,7 +1703,7 @@ fn main() { let item = items.remove(0); let changed = - apply_text_edits(&src.replace(">|<", ""), &mut item.additional_text_edits.unwrap()); + apply_text_edits(&src.replace(">|<", ""), &item.additional_text_edits.unwrap()); assert_eq!(changed, expected); assert_eq!(item.sort_text, Some(auto_import_sort_text())); } @@ -1741,7 +1741,7 @@ fn main() { let item = items.remove(0); let changed = - apply_text_edits(&src.replace(">|<", ""), &mut item.additional_text_edits.unwrap()); + apply_text_edits(&src.replace(">|<", ""), &item.additional_text_edits.unwrap()); assert_eq!(changed, expected); assert_eq!(item.sort_text, Some(auto_import_sort_text())); } @@ -1781,7 +1781,7 @@ fn main() { let item = items.remove(0); let changed = - apply_text_edits(&src.replace(">|<", ""), &mut item.additional_text_edits.unwrap()); + apply_text_edits(&src.replace(">|<", ""), &item.additional_text_edits.unwrap()); assert_eq!(changed, expected); assert_eq!(item.sort_text, Some(auto_import_sort_text())); } @@ -1827,7 +1827,7 @@ fn main() { let item = items.remove(0); let changed = - apply_text_edits(&src.replace(">|<", ""), &mut item.additional_text_edits.unwrap()); + apply_text_edits(&src.replace(">|<", ""), &item.additional_text_edits.unwrap()); assert_eq!(changed, expected); assert_eq!(item.sort_text, Some(auto_import_sort_text())); } @@ -1865,7 +1865,7 @@ fn main() { let item = items.remove(0); let changed = - apply_text_edits(&src.replace(">|<", ""), &mut item.additional_text_edits.unwrap()); + apply_text_edits(&src.replace(">|<", ""), &item.additional_text_edits.unwrap()); assert_eq!(changed, expected); assert_eq!(item.sort_text, Some(auto_import_sort_text())); } @@ -1905,7 +1905,7 @@ fn main() { let item = items.remove(0); let changed = - apply_text_edits(&src.replace(">|<", ""), &mut item.additional_text_edits.unwrap()); + apply_text_edits(&src.replace(">|<", ""), &item.additional_text_edits.unwrap()); assert_eq!(changed, expected); assert_eq!(item.sort_text, Some(auto_import_sort_text())); } @@ -1941,7 +1941,7 @@ fn main() { let item = items.remove(0); let changed = - apply_text_edits(&src.replace(">|<", ""), &mut item.additional_text_edits.unwrap()); + apply_text_edits(&src.replace(">|<", ""), &item.additional_text_edits.unwrap()); assert_eq!(changed, expected); assert_eq!(item.sort_text, Some(auto_import_sort_text())); } diff --git a/tooling/lsp/src/requests/mod.rs b/tooling/lsp/src/requests/mod.rs index 597d8355468..0ee66f1f618 100644 --- a/tooling/lsp/src/requests/mod.rs +++ b/tooling/lsp/src/requests/mod.rs @@ -378,7 +378,7 @@ fn character_to_line_offset(line: &str, character: u32) -> Result } } -fn to_lsp_location<'a, F>( +pub(crate) fn to_lsp_location<'a, F>( files: &'a F, file_id: F::FileId, definition_span: noirc_errors::Span, diff --git a/tooling/lsp/src/tests.rs b/tooling/lsp/src/tests.rs index 2788f848262..7f2d48cd23f 100644 --- a/tooling/lsp/src/tests.rs +++ b/tooling/lsp/src/tests.rs @@ -15,17 +15,18 @@ pub(crate) fn apply_text_edit(src: &str, text_edit: &TextEdit) -> String { lines.join("\n") } -pub(crate) fn apply_text_edits(src: &str, text_edits: &mut [TextEdit]) -> String { +pub(crate) fn apply_text_edits(src: &str, text_edits: &[TextEdit]) -> String { let mut text = src.to_string(); // Text edits must be applied from last to first, otherwise if we apply a text edit // that comes before another one, that other one becomes invalid (it will edit the wrong // piece of code). + let mut text_edits = text_edits.to_vec(); text_edits.sort_by_key(|edit| (edit.range.start.line, edit.range.start.character)); text_edits.reverse(); for text_edit in text_edits { - text = apply_text_edit(&text, text_edit); + text = apply_text_edit(&text, &text_edit); } text diff --git a/tooling/lsp/src/use_segment_positions.rs b/tooling/lsp/src/use_segment_positions.rs new file mode 100644 index 00000000000..f9a3f429029 --- /dev/null +++ b/tooling/lsp/src/use_segment_positions.rs @@ -0,0 +1,336 @@ +use std::collections::HashMap; + +use fm::{FileId, FileMap}; +use lsp_types::{Position, Range, TextEdit}; +use noirc_errors::Span; +use noirc_frontend::ast::{PathKind, UseTree, UseTreeKind}; + +use crate::requests::to_lsp_location; + +/// The position of a segment in a `use` statement. +/// We use this to determine how an auto-import should be inserted. +#[derive(Debug, Default, Copy, Clone)] +pub(crate) enum UseSegmentPosition { + /// The segment either doesn't exist in the source code or there are multiple segments. + /// In this case auto-import will add a new use statement. + #[default] + NoneOrMultiple, + /// The segment is the last one in the `use` statement (or nested use statement): + /// + /// use foo::bar; + /// ^^^ + /// + /// Auto-import will transform it to this: + /// + /// use foo::bar::{self, baz}; + Last { span: Span }, + /// The segment happens before another simple (ident) segment: + /// + /// use foo::bar::qux; + /// ^^^ + /// + /// Auto-import will transform it to this: + /// + /// use foo::bar::{qux, baz}; + BeforeSegment { segment_span_until_end: Span }, + /// The segment happens before a list: + /// + /// use foo::bar::{qux, another}; + /// + /// Auto-import will transform it to this: + /// + /// use foo::bar::{qux, another, baz}; + BeforeList { first_entry_span: Span, list_is_empty: bool }, +} + +/// Remembers where each segment in a `use` statement is located. +/// The key is the full segment, so for `use foo::bar::baz` we'll have three +/// segments: `foo`, `foo::bar` and `foo::bar::baz`, where the span is just +/// for the last identifier (`foo`, `bar` and `baz` in the previous example). +#[derive(Default)] +pub(crate) struct UseSegmentPositions { + use_segment_positions: HashMap, +} + +impl UseSegmentPositions { + pub(crate) fn add(&mut self, use_tree: &UseTree) { + self.gather_use_tree_segments(use_tree, String::new()); + } + + /// Given a full path like `foo::bar::baz`, returns the first non-"NoneOrMultiple" segment position + /// trying each successive parent, together with the name after the parent. + /// + /// For example, first we'll check if `foo::bar` has a single position. If not, we'll try with `foo`. + pub(crate) fn get(&self, full_path: &str) -> (UseSegmentPosition, String) { + // Build a parent path to know in which full segment we need to add this import + let mut segments: Vec<_> = full_path.split("::").collect(); + let mut name = segments.pop().unwrap().to_string(); + let mut parent_path = segments.join("::"); + + loop { + let use_segment_position = + self.use_segment_positions.get(&parent_path).cloned().unwrap_or_default(); + + if let UseSegmentPosition::NoneOrMultiple = use_segment_position { + if let Some(next_name) = segments.pop() { + name = format!("{next_name}::{name}"); + parent_path = segments.join("::"); + } else { + return (UseSegmentPosition::NoneOrMultiple, String::new()); + } + } else { + return (use_segment_position, name); + } + } + } + + fn gather_use_tree_segments(&mut self, use_tree: &UseTree, mut prefix: String) { + let kind_string = match use_tree.prefix.kind { + PathKind::Crate => Some("crate".to_string()), + PathKind::Super => Some("super".to_string()), + PathKind::Dep | PathKind::Plain => None, + }; + if let Some(kind_string) = kind_string { + if let Some(segment) = use_tree.prefix.segments.first() { + self.insert_use_segment_position( + kind_string, + UseSegmentPosition::BeforeSegment { + segment_span_until_end: Span::from( + segment.ident.span().start()..use_tree.span.end() - 1, + ), + }, + ); + } else { + self.insert_use_segment_position_before_use_tree_kind(use_tree, kind_string); + } + } + + let prefix_segments_len = use_tree.prefix.segments.len(); + for (index, segment) in use_tree.prefix.segments.iter().enumerate() { + let ident = &segment.ident; + if !prefix.is_empty() { + prefix.push_str("::"); + }; + prefix.push_str(&ident.0.contents); + + if index < prefix_segments_len - 1 { + self.insert_use_segment_position( + prefix.clone(), + UseSegmentPosition::BeforeSegment { + segment_span_until_end: Span::from( + use_tree.prefix.segments[index + 1].ident.span().start() + ..use_tree.span.end() - 1, + ), + }, + ); + } else { + self.insert_use_segment_position_before_use_tree_kind(use_tree, prefix.clone()); + } + } + + match &use_tree.kind { + UseTreeKind::Path(ident, alias) => { + if !prefix.is_empty() { + prefix.push_str("::"); + } + prefix.push_str(&ident.0.contents); + + if alias.is_none() { + self.insert_use_segment_position( + prefix, + UseSegmentPosition::Last { span: ident.span() }, + ); + } else { + self.insert_use_segment_position(prefix, UseSegmentPosition::NoneOrMultiple); + } + } + UseTreeKind::List(use_trees) => { + for use_tree in use_trees { + self.gather_use_tree_segments(use_tree, prefix.clone()); + } + } + } + } + + fn insert_use_segment_position_before_use_tree_kind( + &mut self, + use_tree: &UseTree, + prefix: String, + ) { + match &use_tree.kind { + UseTreeKind::Path(ident, _alias) => { + self.insert_use_segment_position( + prefix, + UseSegmentPosition::BeforeSegment { + segment_span_until_end: Span::from( + ident.span().start()..use_tree.span.end() - 1, + ), + }, + ); + } + UseTreeKind::List(use_trees) => { + if let Some(first_use_tree) = use_trees.first() { + self.insert_use_segment_position( + prefix, + UseSegmentPosition::BeforeList { + first_entry_span: first_use_tree.prefix.span(), + list_is_empty: false, + }, + ); + } else { + self.insert_use_segment_position( + prefix, + UseSegmentPosition::BeforeList { + first_entry_span: Span::from( + use_tree.span.end() - 1..use_tree.span.end() - 1, + ), + list_is_empty: true, + }, + ); + } + } + } + } + + fn insert_use_segment_position(&mut self, segment: String, position: UseSegmentPosition) { + if self.use_segment_positions.get(&segment).is_none() { + self.use_segment_positions.insert(segment, position); + } else { + self.use_segment_positions.insert(segment, UseSegmentPosition::NoneOrMultiple); + } + } +} + +pub(crate) struct UseCompletionItemAdditionTextEditsRequest<'a> { + /// The full path of the use statement to insert + pub(crate) full_path: &'a str, + pub(crate) files: &'a FileMap, + pub(crate) file: FileId, + /// All of the current source lines + pub(crate) lines: &'a Vec<&'a str>, + /// How many nested `mod` we are in deep + pub(crate) nesting: usize, + /// The line where an auto_import must be inserted + pub(crate) auto_import_line: usize, +} + +/// Returns the text edits needed to add an auto-import for a given full path. +pub(crate) fn use_completion_item_additional_text_edits( + request: UseCompletionItemAdditionTextEditsRequest, + positions: &UseSegmentPositions, +) -> Vec { + let (use_segment_position, name) = positions.get(request.full_path); + match use_segment_position { + UseSegmentPosition::NoneOrMultiple => { + // The parent path either isn't in any use statement, or it exists in multiple + // use statements. In either case we'll add a new use statement. + + new_use_completion_item_additional_text_edits(request) + } + UseSegmentPosition::Last { span } => { + // We have + // + // use foo::bar; + // ^^^ -> span + // + // and we want to transform it to: + // + // use foo::bar::{self, baz}; + // ^^^^^^^^^^^^^ + // + // So we need one text edit: + // 1. insert "::{self, baz}" right after the span + if let Some(lsp_location) = to_lsp_location(request.files, request.file, span) { + let range = lsp_location.range; + vec![TextEdit { + new_text: format!("::{{self, {}}}", name), + range: Range { start: range.end, end: range.end }, + }] + } else { + new_use_completion_item_additional_text_edits(request) + } + } + UseSegmentPosition::BeforeSegment { segment_span_until_end } => { + // Go past the end + let segment_span_until_end = + Span::from(segment_span_until_end.start()..segment_span_until_end.end() + 1); + + // We have + // + // use foo::bar::{one, two}; + // ^^^^^^^^^^^^^^^ -> segment_span_until_end + // + // and we want to transform it to: + // + // use foo::{bar::{one, two}, baz}; + // ^ ^^^^^^ + // + // So we need two text edits: + // 1. insert "{" right before the segment span + // 2. insert ", baz}" right after the segment span + if let Some(lsp_location) = + to_lsp_location(request.files, request.file, segment_span_until_end) + { + let range = lsp_location.range; + vec![ + TextEdit { + new_text: "{".to_string(), + range: Range { start: range.start, end: range.start }, + }, + TextEdit { + new_text: format!(", {}}}", name), + range: Range { start: range.end, end: range.end }, + }, + ] + } else { + new_use_completion_item_additional_text_edits(request) + } + } + UseSegmentPosition::BeforeList { first_entry_span, list_is_empty } => { + // We have + // + // use foo::bar::{one, two}; + // ^^^ -> first_entry_span + // + // and we want to transform it to: + // + // use foo::bar::{baz, one, two}; + // ^^^^ + // + // So we need one text edit: + // 1. insert "baz, " right before the first entry span + if let Some(lsp_location) = + to_lsp_location(request.files, request.file, first_entry_span) + { + let range = lsp_location.range; + vec![TextEdit { + new_text: if list_is_empty { name } else { format!("{}, ", name) }, + range: Range { start: range.start, end: range.start }, + }] + } else { + new_use_completion_item_additional_text_edits(request) + } + } + } +} + +fn new_use_completion_item_additional_text_edits( + request: UseCompletionItemAdditionTextEditsRequest, +) -> Vec { + let line = request.auto_import_line as u32; + let character = (request.nesting * 4) as u32; + let indent = " ".repeat(request.nesting * 4); + let mut newlines = "\n"; + + // If the line we are inserting into is not an empty line, insert an extra line to make some room + if let Some(line_text) = request.lines.get(line as usize) { + if !line_text.trim().is_empty() { + newlines = "\n\n"; + } + } + + vec![TextEdit { + range: Range { start: Position { line, character }, end: Position { line, character } }, + new_text: format!("use {};{}{}", request.full_path, newlines, indent), + }] +} From 2f376100d3ee7ab519d6ea30153395bb3e7af7b1 Mon Sep 17 00:00:00 2001 From: jfecher Date: Fri, 25 Oct 2024 16:02:37 -0500 Subject: [PATCH 13/30] fix: Fix panic in comptime code (#6361) --- compiler/noirc_frontend/src/hir/comptime/interpreter.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/noirc_frontend/src/hir/comptime/interpreter.rs b/compiler/noirc_frontend/src/hir/comptime/interpreter.rs index ffb759e74a2..7205242ead9 100644 --- a/compiler/noirc_frontend/src/hir/comptime/interpreter.rs +++ b/compiler/noirc_frontend/src/hir/comptime/interpreter.rs @@ -554,8 +554,8 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { match &definition.kind { DefinitionKind::Function(function_id) => { let typ = self.elaborator.interner.id_type(id).follow_bindings(); - let bindings = - Rc::new(self.elaborator.interner.get_instantiation_bindings(id).clone()); + let bindings = self.elaborator.interner.try_get_instantiation_bindings(id); + let bindings = Rc::new(bindings.map_or(TypeBindings::default(), Clone::clone)); Ok(Value::Function(*function_id, typ, bindings)) } DefinitionKind::Local(_) => self.lookup(&ident), From da729791b7ffcfcd2f58ba1f8bf2c274c04f303e Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Mon, 28 Oct 2024 08:14:47 -0300 Subject: [PATCH 14/30] fix: slightly better formatting of empty blocks with comments (#6367) --- tooling/nargo_fmt/src/chunks.rs | 15 +++++++++- .../src/formatter/comments_and_whitespace.rs | 5 +++- tooling/nargo_fmt/src/formatter/expression.rs | 15 +++++----- tooling/nargo_fmt/src/formatter/module.rs | 28 ++++++++++--------- 4 files changed, 41 insertions(+), 22 deletions(-) diff --git a/tooling/nargo_fmt/src/chunks.rs b/tooling/nargo_fmt/src/chunks.rs index 673cf020aed..0e55dfad3b7 100644 --- a/tooling/nargo_fmt/src/chunks.rs +++ b/tooling/nargo_fmt/src/chunks.rs @@ -995,7 +995,12 @@ impl<'a> Formatter<'a> { /// Appends the string to the current buffer line by line, with some pre-checks. fn write_chunk_lines(&mut self, string: &str) { - for (index, line) in string.lines().enumerate() { + let lines: Vec<_> = string.lines().collect(); + + let mut index = 0; + while index < lines.len() { + let line = &lines[index]; + // Don't indent the first line (it should already be indented). // Also don't indent if the current line already has a space as the last char // (it means it's already indented) @@ -1015,6 +1020,14 @@ impl<'a> Formatter<'a> { } else { self.write(line); } + + index += 1; + + // If a newline comes next too, write multiple lines to preserve original formatting + while index < lines.len() && lines[index].is_empty() { + self.write_multiple_lines_without_skipping_whitespace_and_comments(); + index += 1; + } } } diff --git a/tooling/nargo_fmt/src/formatter/comments_and_whitespace.rs b/tooling/nargo_fmt/src/formatter/comments_and_whitespace.rs index 4aba0481e24..547d33348b8 100644 --- a/tooling/nargo_fmt/src/formatter/comments_and_whitespace.rs +++ b/tooling/nargo_fmt/src/formatter/comments_and_whitespace.rs @@ -643,7 +643,10 @@ mod foo; /* hello */ } "; - let expected = "struct Foo { /* hello */ }\n"; + let expected = "struct Foo { + /* hello */ +} +"; assert_format(src, expected); } diff --git a/tooling/nargo_fmt/src/formatter/expression.rs b/tooling/nargo_fmt/src/formatter/expression.rs index ebfa3ae78fb..cd46b09f190 100644 --- a/tooling/nargo_fmt/src/formatter/expression.rs +++ b/tooling/nargo_fmt/src/formatter/expression.rs @@ -1142,6 +1142,9 @@ impl<'a, 'b> ChunkFormatter<'a, 'b> { pub(super) fn empty_block_contents_chunk(&mut self) -> Option { let mut group = ChunkGroup::new(); group.increase_indentation(); + + let newlines_count = self.following_newlines_count(); + let mut chunk = self.chunk(|formatter| { formatter.skip_comments_and_whitespace_writing_multiple_lines_if_found(); }); @@ -1151,15 +1154,13 @@ impl<'a, 'b> ChunkFormatter<'a, 'b> { // so there's nothing to write. None } else { - if chunk.string.trim_start().starts_with("//") { - group.text(chunk); - group.decrease_indentation(); - group.line(); - } else { + // If we have a trailing comment, preserve it in the same line + if newlines_count == 0 && !chunk.string.trim_start().starts_with("//") { chunk.string = format!(" {} ", chunk.string.trim()); - group.text(chunk); - group.decrease_indentation(); } + group.text(chunk); + group.decrease_indentation(); + group.line(); Some(group) } } diff --git a/tooling/nargo_fmt/src/formatter/module.rs b/tooling/nargo_fmt/src/formatter/module.rs index 00ac9fe2f47..3df9b7e0c90 100644 --- a/tooling/nargo_fmt/src/formatter/module.rs +++ b/tooling/nargo_fmt/src/formatter/module.rs @@ -31,27 +31,17 @@ impl<'a> Formatter<'a> { self.write_space(); self.write_identifier(submodule.name); self.write_space(); + self.write_left_brace(); if parsed_module_is_empty(&submodule.contents) { - self.write_left_brace(); - self.increase_indentation(); - - let comments_count_before = self.written_comments_count; - self.skip_comments_and_whitespace_writing_multiple_lines_if_found(); - self.decrease_indentation(); - if self.written_comments_count > comments_count_before { - self.write_line(); - self.write_indentation(); - } - self.write_right_brace(); + self.format_empty_block_contents(); } else { - self.write_left_brace(); self.increase_indentation(); self.write_line(); self.format_parsed_module(submodule.contents, self.ignore_next); self.decrease_indentation(); self.write_indentation(); - self.write_right_brace(); } + self.write_right_brace(); } } @@ -102,6 +92,18 @@ mod foo;\n"; assert_format(src, expected); } + #[test] + fn format_empty_submodule_2() { + let src = "mod foo { mod bar { + + } }"; + let expected = "mod foo { + mod bar {} +} +"; + assert_format(src, expected); + } + #[test] fn format_empty_subcontract() { let src = "contract foo { }"; From 83d29f259debe41d0b5cdfb6e63d31733ae4e0c7 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Mon, 28 Oct 2024 08:15:51 -0300 Subject: [PATCH 15/30] fix: remove assumed parent traits (#6365) --- compiler/noirc_frontend/src/node_interner.rs | 25 ++++++++++++++++++++ compiler/noirc_frontend/src/tests/traits.rs | 21 ++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/compiler/noirc_frontend/src/node_interner.rs b/compiler/noirc_frontend/src/node_interner.rs index 2183cfba0ef..5fe88ed4e23 100644 --- a/compiler/noirc_frontend/src/node_interner.rs +++ b/compiler/noirc_frontend/src/node_interner.rs @@ -1875,8 +1875,33 @@ impl NodeInterner { /// Removes all TraitImplKind::Assumed from the list of known impls for the given trait pub fn remove_assumed_trait_implementations_for_trait(&mut self, trait_id: TraitId) { + self.remove_assumed_trait_implementations_for_trait_and_parents(trait_id, trait_id); + } + + fn remove_assumed_trait_implementations_for_trait_and_parents( + &mut self, + trait_id: TraitId, + starting_trait_id: TraitId, + ) { let entries = self.trait_implementation_map.entry(trait_id).or_default(); entries.retain(|(_, kind)| matches!(kind, TraitImplKind::Normal(_))); + + // Also remove assumed implementations for the parent traits, if any + if let Some(trait_bounds) = + self.try_get_trait(trait_id).map(|the_trait| the_trait.trait_bounds.clone()) + { + for parent_trait_bound in trait_bounds { + // Avoid looping forever in case there are cycles + if parent_trait_bound.trait_id == starting_trait_id { + continue; + } + + self.remove_assumed_trait_implementations_for_trait_and_parents( + parent_trait_bound.trait_id, + starting_trait_id, + ); + } + } } /// Tags the given identifier with the selected trait_impl so that monomorphization diff --git a/compiler/noirc_frontend/src/tests/traits.rs b/compiler/noirc_frontend/src/tests/traits.rs index 88138ecde4d..dd6430a94cc 100644 --- a/compiler/noirc_frontend/src/tests/traits.rs +++ b/compiler/noirc_frontend/src/tests/traits.rs @@ -261,3 +261,24 @@ fn errors_if_impl_trait_constraint_is_not_satisfied() { assert_eq!(typ, "SomeGreeter"); assert_eq!(impl_trait, "Foo"); } + +#[test] +fn removes_assumed_parent_traits_after_function_ends() { + let src = r#" + trait Foo {} + trait Bar: Foo {} + + pub fn foo() + where + T: Bar, + {} + + pub fn bar() + where + T: Foo, + {} + + fn main() {} + "#; + assert_no_errors(src); +} From 51eb2954e8dfb3da298431a82f36fa72ebbee8eb Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Mon, 28 Oct 2024 08:57:57 -0300 Subject: [PATCH 16/30] fix: LSP auto-import would import public item inside private module (#6366) --- .../src/hir/def_collector/dc_crate.rs | 7 +- .../src/hir/def_collector/dc_mod.rs | 1 + compiler/noirc_frontend/src/node_interner.rs | 1 + tooling/lsp/src/modules.rs | 17 +--- .../requests/code_action/import_or_qualify.rs | 33 +++++--- tooling/lsp/src/requests/completion.rs | 17 +++- .../src/requests/completion/auto_import.rs | 13 ++- tooling/lsp/src/requests/completion/tests.rs | 79 +++++++++++-------- tooling/lsp/src/visibility.rs | 53 ++++++++++++- 9 files changed, 154 insertions(+), 67 deletions(-) diff --git a/compiler/noirc_frontend/src/hir/def_collector/dc_crate.rs b/compiler/noirc_frontend/src/hir/def_collector/dc_crate.rs index 16fd43ba2a2..658812be324 100644 --- a/compiler/noirc_frontend/src/hir/def_collector/dc_crate.rs +++ b/compiler/noirc_frontend/src/hir/def_collector/dc_crate.rs @@ -303,7 +303,12 @@ impl DefCollector { def_map.extern_prelude.insert(dep.as_name(), module_id); let location = dep_def_map[dep_def_root].location; - let attributes = ModuleAttributes { name: dep.as_name(), location, parent: None }; + let attributes = ModuleAttributes { + name: dep.as_name(), + location, + parent: None, + visibility: ItemVisibility::Public, + }; context.def_interner.add_module_attributes(module_id, attributes); } diff --git a/compiler/noirc_frontend/src/hir/def_collector/dc_mod.rs b/compiler/noirc_frontend/src/hir/def_collector/dc_mod.rs index b9ce8f361f7..825a8414fe0 100644 --- a/compiler/noirc_frontend/src/hir/def_collector/dc_mod.rs +++ b/compiler/noirc_frontend/src/hir/def_collector/dc_mod.rs @@ -885,6 +885,7 @@ fn push_child_module( name: mod_name.0.contents.clone(), location: mod_location, parent: Some(parent), + visibility, }, ); diff --git a/compiler/noirc_frontend/src/node_interner.rs b/compiler/noirc_frontend/src/node_interner.rs index 5fe88ed4e23..68510f2ffbd 100644 --- a/compiler/noirc_frontend/src/node_interner.rs +++ b/compiler/noirc_frontend/src/node_interner.rs @@ -56,6 +56,7 @@ pub struct ModuleAttributes { pub name: String, pub location: Location, pub parent: Option, + pub visibility: ItemVisibility, } type StructAttributes = Vec; diff --git a/tooling/lsp/src/modules.rs b/tooling/lsp/src/modules.rs index 9f9a826d6ca..cadf71b2eec 100644 --- a/tooling/lsp/src/modules.rs +++ b/tooling/lsp/src/modules.rs @@ -1,14 +1,9 @@ -use std::collections::BTreeMap; - use noirc_frontend::{ - ast::ItemVisibility, graph::{CrateId, Dependency}, - hir::def_map::{CrateDefMap, ModuleDefId, ModuleId}, + hir::def_map::{ModuleDefId, ModuleId}, node_interner::{NodeInterner, ReferenceId}, }; -use crate::visibility::is_visible; - pub(crate) fn get_parent_module( interner: &NodeInterner, module_def_id: ModuleDefId, @@ -33,18 +28,12 @@ pub(crate) fn module_def_id_to_reference_id(module_def_id: ModuleDefId) -> Refer /// - Otherwise, that item's parent module's path is returned pub(crate) fn relative_module_full_path( module_def_id: ModuleDefId, - visibility: ItemVisibility, current_module_id: ModuleId, current_module_parent_id: Option, interner: &NodeInterner, - def_maps: &BTreeMap, ) -> Option { let full_path; if let ModuleDefId::ModuleId(module_id) = module_def_id { - if !is_visible(module_id, current_module_id, visibility, def_maps) { - return None; - } - full_path = relative_module_id_path( module_id, ¤t_module_id, @@ -56,10 +45,6 @@ pub(crate) fn relative_module_full_path( return None; }; - if !is_visible(parent_module, current_module_id, visibility, def_maps) { - return None; - } - full_path = relative_module_id_path( parent_module, ¤t_module_id, diff --git a/tooling/lsp/src/requests/code_action/import_or_qualify.rs b/tooling/lsp/src/requests/code_action/import_or_qualify.rs index a3053f8f304..2e051890544 100644 --- a/tooling/lsp/src/requests/code_action/import_or_qualify.rs +++ b/tooling/lsp/src/requests/code_action/import_or_qualify.rs @@ -11,6 +11,7 @@ use crate::{ use_segment_positions::{ use_completion_item_additional_text_edits, UseCompletionItemAdditionTextEditsRequest, }, + visibility::module_def_id_is_visible, }; use super::CodeActionFinder; @@ -41,6 +42,16 @@ impl<'a> CodeActionFinder<'a> { } for (module_def_id, visibility, defining_module) in entries { + if !module_def_id_is_visible( + *module_def_id, + self.module_id, + *visibility, + self.interner, + self.def_maps, + ) { + continue; + } + let module_full_path = if let Some(defining_module) = defining_module { relative_module_id_path( *defining_module, @@ -51,11 +62,9 @@ impl<'a> CodeActionFinder<'a> { } else { let Some(module_full_path) = relative_module_full_path( *module_def_id, - *visibility, self.module_id, current_module_parent_id, self.interner, - self.def_maps, ) else { continue; }; @@ -132,7 +141,7 @@ mod tests { let src = r#" mod foo { - mod bar { + pub mod bar { pub struct SomeTypeInBar {} } } @@ -142,7 +151,7 @@ mod tests { let expected = r#" mod foo { - mod bar { + pub mod bar { pub struct SomeTypeInBar {} } } @@ -158,7 +167,7 @@ mod tests { let title = "Import foo::bar::SomeTypeInBar"; let src = r#"mod foo { - mod bar { + pub mod bar { pub struct SomeTypeInBar {} } } @@ -168,7 +177,7 @@ fn foo(x: SomeType>|| NodeFinder<'a> { if name_matches(name, prefix) { let per_ns = module_data.find_name(ident); if let Some((module_def_id, visibility, _)) = per_ns.types { - if is_visible(module_id, self.module_id, visibility, self.def_maps) { + if item_in_module_is_visible( + module_id, + self.module_id, + visibility, + self.def_maps, + ) { let completion_items = self.module_def_id_completion_items( module_def_id, name.clone(), @@ -813,7 +819,12 @@ impl<'a> NodeFinder<'a> { } if let Some((module_def_id, visibility, _)) = per_ns.values { - if is_visible(module_id, self.module_id, visibility, self.def_maps) { + if item_in_module_is_visible( + module_id, + self.module_id, + visibility, + self.def_maps, + ) { let completion_items = self.module_def_id_completion_items( module_def_id, name.clone(), diff --git a/tooling/lsp/src/requests/completion/auto_import.rs b/tooling/lsp/src/requests/completion/auto_import.rs index 9ed633289c1..3b12d941c98 100644 --- a/tooling/lsp/src/requests/completion/auto_import.rs +++ b/tooling/lsp/src/requests/completion/auto_import.rs @@ -5,6 +5,7 @@ use crate::{ use_segment_positions::{ use_completion_item_additional_text_edits, UseCompletionItemAdditionTextEditsRequest, }, + visibility::module_def_id_is_visible, }; use super::{ @@ -33,6 +34,16 @@ impl<'a> NodeFinder<'a> { continue; } + if !module_def_id_is_visible( + *module_def_id, + self.module_id, + *visibility, + self.interner, + self.def_maps, + ) { + continue; + } + let completion_items = self.module_def_id_completion_items( *module_def_id, name.clone(), @@ -58,11 +69,9 @@ impl<'a> NodeFinder<'a> { } else { let Some(module_full_path) = relative_module_full_path( *module_def_id, - *visibility, self.module_id, current_module_parent_id, self.interner, - self.def_maps, ) else { continue; }; diff --git a/tooling/lsp/src/requests/completion/tests.rs b/tooling/lsp/src/requests/completion/tests.rs index be3a75f72c8..b399088d05b 100644 --- a/tooling/lsp/src/requests/completion/tests.rs +++ b/tooling/lsp/src/requests/completion/tests.rs @@ -1394,7 +1394,7 @@ mod completion_tests { #[test] async fn test_auto_imports() { let src = r#"mod foo { - mod bar { + pub mod bar { pub fn hello_world() {} struct Foo { @@ -1415,7 +1415,7 @@ fn main() { let expected = r#"use foo::bar::hello_world; mod foo { - mod bar { + pub mod bar { pub fn hello_world() {} struct Foo { @@ -1456,7 +1456,7 @@ fn main() { async fn test_auto_imports_when_in_nested_module_and_item_is_further_nested() { let src = r#"#[something] mod foo { - mod bar { + pub mod bar { pub fn hello_world() {} } @@ -1470,7 +1470,7 @@ mod foo { mod foo { use bar::hello_world; - mod bar { + pub mod bar { pub fn hello_world() {} } @@ -1500,11 +1500,11 @@ mod foo { #[test] async fn test_auto_imports_when_in_nested_module_and_item_is_not_further_nested() { let src = r#"mod foo { - mod bar { + pub mod bar { pub fn hello_world() {} } - mod baz { + pub mod baz { fn foo() { hel>|< } @@ -1512,11 +1512,11 @@ mod foo { }"#; let expected = r#"mod foo { - mod bar { + pub mod bar { pub fn hello_world() {} } - mod baz { + pub mod baz { use super::bar::hello_world; fn foo() { @@ -1545,7 +1545,7 @@ mod foo { #[test] async fn test_auto_import_inserts_after_last_use() { let src = r#"mod foo { - mod bar { + pub mod bar { pub fn hello_world() {} } } @@ -1561,7 +1561,7 @@ fn main() { }"#; let expected = r#"mod foo { - mod bar { + pub mod bar { pub fn hello_world() {} } } @@ -1625,6 +1625,23 @@ fn main() { assert!(items.is_empty()); } + #[test] + async fn test_does_not_auto_import_public_function_in_private_module() { + let src = r#" + mod foo { + mod bar { + pub fn hello_world() {} + } + } + + fn main() { + hel>|< + } + "#; + let items = get_completions(src).await; + assert!(items.is_empty()); + } + #[test] async fn test_auto_import_suggests_modules_too() { let src = r#"mod foo { @@ -1675,7 +1692,7 @@ mod foo { let src = r#"use foo::bar::one_hello_world; mod foo { - mod bar { + pub mod bar { pub fn one_hello_world() {} pub fn two_hello_world() {} } @@ -1688,7 +1705,7 @@ fn main() { let expected = r#"use foo::bar::{one_hello_world, two_hello_world}; mod foo { - mod bar { + pub mod bar { pub fn one_hello_world() {} pub fn two_hello_world() {} } @@ -1713,7 +1730,7 @@ fn main() { let src = r#"use foo::bar::one_hello_world; mod foo { - mod bar { + pub mod bar { pub fn one_hello_world() {} } pub fn two_hello_world() {} @@ -1726,7 +1743,7 @@ fn main() { let expected = r#"use foo::{bar::one_hello_world, two_hello_world}; mod foo { - mod bar { + pub mod bar { pub fn one_hello_world() {} } pub fn two_hello_world() {} @@ -1751,11 +1768,11 @@ fn main() { let src = r#"use foo::{bar::one_hello_world, baz}; mod foo { - mod bar { + pub mod bar { pub fn one_hello_world() {} pub fn two_hello_world() {} } - mod baz {} + pub mod baz {} } fn main() { @@ -1765,11 +1782,11 @@ fn main() { let expected = r#"use foo::{bar::{one_hello_world, two_hello_world}, baz}; mod foo { - mod bar { + pub mod bar { pub fn one_hello_world() {} pub fn two_hello_world() {} } - mod baz {} + pub mod baz {} } fn main() { @@ -1791,11 +1808,11 @@ fn main() { let src = r#"use foo::bar::baz; mod foo { - mod bar { - mod baz { + pub mod bar { + pub mod baz { pub fn one_hello_world() {} } - mod qux { + pub mod qux { pub fn two_hello_world() {} } } @@ -1808,11 +1825,11 @@ fn main() { let expected = r#"use foo::bar::{baz, qux::two_hello_world}; mod foo { - mod bar { - mod baz { + pub mod bar { + pub mod baz { pub fn one_hello_world() {} } - mod qux { + pub mod qux { pub fn two_hello_world() {} } } @@ -1837,7 +1854,7 @@ fn main() { let src = r#"use foo::bar; mod foo { - mod bar { + pub mod bar { pub fn one_hello_world() {} pub fn two_hello_world() {} } @@ -1850,7 +1867,7 @@ fn main() { let expected = r#"use foo::bar::{self, two_hello_world}; mod foo { - mod bar { + pub mod bar { pub fn one_hello_world() {} pub fn two_hello_world() {} } @@ -1875,7 +1892,7 @@ fn main() { let src = r#"use foo::bar::{one_hello_world, three_hello_world}; mod foo { - mod bar { + pub mod bar { pub fn one_hello_world() {} pub fn two_hello_world() {} pub fn three_hello_world() {} @@ -1889,7 +1906,7 @@ fn main() { let expected = r#"use foo::bar::{two_hello_world, one_hello_world, three_hello_world}; mod foo { - mod bar { + pub mod bar { pub fn one_hello_world() {} pub fn two_hello_world() {} pub fn three_hello_world() {} @@ -1915,7 +1932,7 @@ fn main() { let src = r#"use foo::bar::{}; mod foo { - mod bar { + pub mod bar { pub fn two_hello_world() {} } } @@ -1927,7 +1944,7 @@ fn main() { let expected = r#"use foo::bar::{two_hello_world}; mod foo { - mod bar { + pub mod bar { pub fn two_hello_world() {} } } @@ -2233,7 +2250,7 @@ fn main() { async fn test_auto_import_suggests_pub_use_for_function() { let src = r#" mod bar { - mod baz { + pub mod baz { pub fn coco() {} } diff --git a/tooling/lsp/src/visibility.rs b/tooling/lsp/src/visibility.rs index 207302f327e..f5b807055d5 100644 --- a/tooling/lsp/src/visibility.rs +++ b/tooling/lsp/src/visibility.rs @@ -4,12 +4,23 @@ use noirc_frontend::{ ast::ItemVisibility, graph::CrateId, hir::{ - def_map::{CrateDefMap, ModuleId}, + def_map::{CrateDefMap, ModuleDefId, ModuleId}, resolution::visibility::can_reference_module_id, }, + node_interner::NodeInterner, }; -pub(super) fn is_visible( +use crate::modules::get_parent_module; + +/// Returns true if an item with the given visibility in the target module +/// is visible from the current module. For example: +/// +/// mod foo { +/// ^^^ <-- target module +/// pub(crate) fn bar() {} +/// ^^^^^^^^^^ <- visibility +/// } +pub(super) fn item_in_module_is_visible( target_module_id: ModuleId, current_module_id: ModuleId, visibility: ItemVisibility, @@ -23,3 +34,41 @@ pub(super) fn is_visible( visibility, ) } + +/// Returns true if the given ModuleDefId is visible from the current module, given its visibility. +/// This will in turn check if the ModuleDefId parent modules are visible from the current module. +pub(super) fn module_def_id_is_visible( + module_def_id: ModuleDefId, + current_module_id: ModuleId, + mut visibility: ItemVisibility, + interner: &NodeInterner, + def_maps: &BTreeMap, +) -> bool { + // First find out which module we need to check. + // If a module is trying to be referenced, it's that module. Otherwise it's the module that contains the item. + let mut target_module_id = if let ModuleDefId::ModuleId(module_id) = module_def_id { + Some(module_id) + } else { + get_parent_module(interner, module_def_id) + }; + + // Then check if it's visible, and upwards + while let Some(module_id) = target_module_id { + if !item_in_module_is_visible(module_id, current_module_id, visibility, def_maps) { + return false; + } + + let module_data = &def_maps[&module_id.krate].modules()[module_id.local_id.0]; + let parent_local_id = module_data.parent; + target_module_id = + parent_local_id.map(|local_id| ModuleId { krate: module_id.krate, local_id }); + + // This is a bit strange, but the visibility is always that of the item inside another module, + // so the visibility we update here is for the next loop check. + visibility = interner + .try_module_attributes(&module_id) + .map_or(ItemVisibility::Public, |attributes| attributes.visibility); + } + + true +} From e909dcbb06c7b0043ffc79d5b8af99835b0096e5 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Mon, 28 Oct 2024 12:32:18 -0300 Subject: [PATCH 17/30] feat: let LSP suggest traits in trait bounds (#6370) --- compiler/noirc_frontend/src/ast/visitor.rs | 54 +++++++++++++++++-- tooling/lsp/src/requests/completion.rs | 20 +++++-- .../requests/completion/completion_items.rs | 8 +++ tooling/lsp/src/requests/completion/kinds.rs | 4 +- tooling/lsp/src/requests/completion/tests.rs | 38 +++++++++++++ 5 files changed, 115 insertions(+), 9 deletions(-) diff --git a/compiler/noirc_frontend/src/ast/visitor.rs b/compiler/noirc_frontend/src/ast/visitor.rs index 632c5656137..16ee0fc4c02 100644 --- a/compiler/noirc_frontend/src/ast/visitor.rs +++ b/compiler/noirc_frontend/src/ast/visitor.rs @@ -22,8 +22,8 @@ use crate::{ use super::{ ForBounds, FunctionReturnType, GenericTypeArgs, IntegerBitSize, ItemVisibility, Pattern, - Signedness, TraitImplItemKind, TypePath, UnresolvedGenerics, UnresolvedTraitConstraint, - UnresolvedType, UnresolvedTypeData, UnresolvedTypeExpression, + Signedness, TraitBound, TraitImplItemKind, TypePath, UnresolvedGenerics, + UnresolvedTraitConstraint, UnresolvedType, UnresolvedTypeData, UnresolvedTypeExpression, }; #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -438,6 +438,14 @@ pub trait Visitor { true } + fn visit_trait_bound(&mut self, _: &TraitBound) -> bool { + true + } + + fn visit_unresolved_trait_constraint(&mut self, _: &UnresolvedTraitConstraint) -> bool { + true + } + fn visit_pattern(&mut self, _: &Pattern) -> bool { true } @@ -555,6 +563,12 @@ impl NoirFunction { param.typ.accept(visitor); } + self.def.return_type.accept(visitor); + + for constraint in &self.def.where_clause { + constraint.accept(visitor); + } + self.def.body.accept(None, visitor); } } @@ -645,6 +659,14 @@ impl NoirTrait { attribute.accept(AttributeTarget::Trait, visitor); } + for bound in &self.bounds { + bound.accept(visitor); + } + + for constraint in &self.where_clause { + constraint.accept(visitor); + } + for item in &self.items { item.item.accept(visitor); } @@ -686,7 +708,7 @@ impl TraitItem { return_type.accept(visitor); for unresolved_trait_constraint in where_clause { - unresolved_trait_constraint.typ.accept(visitor); + unresolved_trait_constraint.accept(visitor); } if let Some(body) = body { @@ -1346,6 +1368,32 @@ impl FunctionReturnType { } } +impl TraitBound { + pub fn accept(&self, visitor: &mut impl Visitor) { + if visitor.visit_trait_bound(self) { + self.accept_children(visitor); + } + } + + pub fn accept_children(&self, visitor: &mut impl Visitor) { + self.trait_path.accept(visitor); + self.trait_generics.accept(visitor); + } +} + +impl UnresolvedTraitConstraint { + pub fn accept(&self, visitor: &mut impl Visitor) { + if visitor.visit_unresolved_trait_constraint(self) { + self.accept_children(visitor); + } + } + + pub fn accept_children(&self, visitor: &mut impl Visitor) { + self.typ.accept(visitor); + self.trait_bound.accept(visitor); + } +} + impl Pattern { pub fn accept(&self, visitor: &mut impl Visitor) { if visitor.visit_pattern(self) { diff --git a/tooling/lsp/src/requests/completion.rs b/tooling/lsp/src/requests/completion.rs index 145edd85811..2dd42c740cd 100644 --- a/tooling/lsp/src/requests/completion.rs +++ b/tooling/lsp/src/requests/completion.rs @@ -19,9 +19,9 @@ use noirc_frontend::{ Expression, ExpressionKind, ForLoopStatement, GenericTypeArgs, Ident, IfExpression, IntegerBitSize, ItemVisibility, LValue, Lambda, LetStatement, MemberAccessExpression, MethodCallExpression, NoirFunction, NoirStruct, NoirTraitImpl, Path, PathKind, Pattern, - Signedness, Statement, TraitImplItemKind, TypeImpl, TypePath, UnresolvedGeneric, - UnresolvedGenerics, UnresolvedType, UnresolvedTypeData, UnresolvedTypeExpression, UseTree, - UseTreeKind, Visitor, + Signedness, Statement, TraitBound, TraitImplItemKind, TypeImpl, TypePath, + UnresolvedGeneric, UnresolvedGenerics, UnresolvedType, UnresolvedTypeData, + UnresolvedTypeExpression, UseTree, UseTreeKind, Visitor, }, graph::{CrateId, Dependency}, hir::{ @@ -384,7 +384,7 @@ impl<'a> NodeFinder<'a> { self.builtin_types_completion(&prefix); self.type_parameters_completion(&prefix); } - RequestedItems::OnlyAttributeFunctions(..) => (), + RequestedItems::OnlyTraits | RequestedItems::OnlyAttributeFunctions(..) => (), } self.complete_auto_imports(&prefix, requested_items, function_completion_kind); } @@ -1124,6 +1124,10 @@ impl<'a> Visitor for NodeFinder<'a> { noir_function.def.return_type.accept(self); + for constraint in &noir_function.def.where_clause { + constraint.accept(self); + } + self.local_variables.clear(); for param in &noir_function.def.parameters { self.collect_local_variables(¶m.pattern); @@ -1231,7 +1235,7 @@ impl<'a> Visitor for NodeFinder<'a> { return_type.accept(self); for unresolved_trait_constraint in where_clause { - unresolved_trait_constraint.typ.accept(self); + unresolved_trait_constraint.accept(self); } if let Some(body) = body { @@ -1702,6 +1706,12 @@ impl<'a> Visitor for NodeFinder<'a> { last_was_dollar = false; } } + + fn visit_trait_bound(&mut self, trait_bound: &TraitBound) -> bool { + self.find_in_path(&trait_bound.trait_path, RequestedItems::OnlyTraits); + trait_bound.trait_generics.accept(self); + false + } } fn get_field_type(typ: &Type, name: &str) -> Option { diff --git a/tooling/lsp/src/requests/completion/completion_items.rs b/tooling/lsp/src/requests/completion/completion_items.rs index f281f5e3abf..49e4474738e 100644 --- a/tooling/lsp/src/requests/completion/completion_items.rs +++ b/tooling/lsp/src/requests/completion/completion_items.rs @@ -35,6 +35,14 @@ impl<'a> NodeFinder<'a> { | ModuleDefId::TypeAliasId(_) | ModuleDefId::TraitId(_) => (), }, + RequestedItems::OnlyTraits => match module_def_id { + ModuleDefId::FunctionId(_) | ModuleDefId::GlobalId(_) | ModuleDefId::TypeId(_) => { + return Vec::new() + } + ModuleDefId::ModuleId(_) + | ModuleDefId::TypeAliasId(_) + | ModuleDefId::TraitId(_) => (), + }, RequestedItems::OnlyAttributeFunctions(..) => { if !matches!(module_def_id, ModuleDefId::FunctionId(..)) { return Vec::new(); diff --git a/tooling/lsp/src/requests/completion/kinds.rs b/tooling/lsp/src/requests/completion/kinds.rs index 6fa74ffdb1a..ba6faada6f4 100644 --- a/tooling/lsp/src/requests/completion/kinds.rs +++ b/tooling/lsp/src/requests/completion/kinds.rs @@ -25,8 +25,10 @@ pub(super) enum FunctionKind<'a> { pub(super) enum RequestedItems { // Suggest any items (types, functions, etc.). AnyItems, - // Only suggest types. + // Only suggest types (and modules, because they can contain types). OnlyTypes, + // Only suggest traits (and modules, because they can contain traits). + OnlyTraits, // Only attribute functions OnlyAttributeFunctions(AttributeTarget), } diff --git a/tooling/lsp/src/requests/completion/tests.rs b/tooling/lsp/src/requests/completion/tests.rs index b399088d05b..a34e7a99861 100644 --- a/tooling/lsp/src/requests/completion/tests.rs +++ b/tooling/lsp/src/requests/completion/tests.rs @@ -2719,4 +2719,42 @@ fn main() { assert_completion(src, Vec::new()).await; } + + #[test] + async fn test_suggests_trait_in_trait_parent_bounds() { + let src = r#" + trait Foobar {} + struct Foobarbaz {} + + trait Bar: Foob>|< {} + "#; + assert_completion( + src, + vec![simple_completion_item( + "Foobar", + CompletionItemKind::INTERFACE, + Some("Foobar".to_string()), + )], + ) + .await; + } + + #[test] + async fn test_suggests_trait_in_function_where_clause() { + let src = r#" + trait Foobar {} + struct Foobarbaz {} + + fn foo() where T: Foob>|< {} + "#; + assert_completion( + src, + vec![simple_completion_item( + "Foobar", + CompletionItemKind::INTERFACE, + Some("Foobar".to_string()), + )], + ) + .await; + } } From a4fc6e861492ab5ff12ebc5fdbb248f983eab0a2 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Mon, 28 Oct 2024 12:32:38 -0300 Subject: [PATCH 18/30] fix: (LSP) check visibility of module that re-exports item, if any (#6371) --- .../requests/code_action/import_or_qualify.rs | 1 + .../src/requests/completion/auto_import.rs | 1 + tooling/lsp/src/requests/completion/tests.rs | 23 +++++++++++++++++++ tooling/lsp/src/visibility.rs | 14 +++++++---- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/tooling/lsp/src/requests/code_action/import_or_qualify.rs b/tooling/lsp/src/requests/code_action/import_or_qualify.rs index 2e051890544..ffc83b05a5b 100644 --- a/tooling/lsp/src/requests/code_action/import_or_qualify.rs +++ b/tooling/lsp/src/requests/code_action/import_or_qualify.rs @@ -46,6 +46,7 @@ impl<'a> CodeActionFinder<'a> { *module_def_id, self.module_id, *visibility, + *defining_module, self.interner, self.def_maps, ) { diff --git a/tooling/lsp/src/requests/completion/auto_import.rs b/tooling/lsp/src/requests/completion/auto_import.rs index 3b12d941c98..0e80b284f32 100644 --- a/tooling/lsp/src/requests/completion/auto_import.rs +++ b/tooling/lsp/src/requests/completion/auto_import.rs @@ -38,6 +38,7 @@ impl<'a> NodeFinder<'a> { *module_def_id, self.module_id, *visibility, + *defining_module, self.interner, self.def_maps, ) { diff --git a/tooling/lsp/src/requests/completion/tests.rs b/tooling/lsp/src/requests/completion/tests.rs index a34e7a99861..745dacfc152 100644 --- a/tooling/lsp/src/requests/completion/tests.rs +++ b/tooling/lsp/src/requests/completion/tests.rs @@ -1642,6 +1642,29 @@ fn main() { assert!(items.is_empty()); } + #[test] + async fn checks_visibility_of_module_that_exports_item_if_any() { + let src = r#" + mod foo { + mod bar { + pub fn hello_world() {} + } + + pub use bar::hello_world; + } + + fn main() { + hello_w>|< + } + "#; + let mut items = get_completions(src).await; + assert_eq!(items.len(), 1); + + let item = items.remove(0); + assert_eq!(item.label, "hello_world()"); + assert_eq!(item.label_details.unwrap().detail.unwrap(), "(use foo::hello_world)"); + } + #[test] async fn test_auto_import_suggests_modules_too() { let src = r#"mod foo { diff --git a/tooling/lsp/src/visibility.rs b/tooling/lsp/src/visibility.rs index f5b807055d5..6a21525f069 100644 --- a/tooling/lsp/src/visibility.rs +++ b/tooling/lsp/src/visibility.rs @@ -37,10 +37,13 @@ pub(super) fn item_in_module_is_visible( /// Returns true if the given ModuleDefId is visible from the current module, given its visibility. /// This will in turn check if the ModuleDefId parent modules are visible from the current module. +/// If `defining_module` is Some, it will be considered as the parent of the item to check +/// (this is the case when the item is re-exported with `pub use` or similar). pub(super) fn module_def_id_is_visible( module_def_id: ModuleDefId, current_module_id: ModuleId, mut visibility: ItemVisibility, + mut defining_module: Option, interner: &NodeInterner, def_maps: &BTreeMap, ) -> bool { @@ -49,7 +52,7 @@ pub(super) fn module_def_id_is_visible( let mut target_module_id = if let ModuleDefId::ModuleId(module_id) = module_def_id { Some(module_id) } else { - get_parent_module(interner, module_def_id) + std::mem::take(&mut defining_module).or_else(|| get_parent_module(interner, module_def_id)) }; // Then check if it's visible, and upwards @@ -58,10 +61,11 @@ pub(super) fn module_def_id_is_visible( return false; } - let module_data = &def_maps[&module_id.krate].modules()[module_id.local_id.0]; - let parent_local_id = module_data.parent; - target_module_id = - parent_local_id.map(|local_id| ModuleId { krate: module_id.krate, local_id }); + target_module_id = std::mem::take(&mut defining_module).or_else(|| { + let module_data = &def_maps[&module_id.krate].modules()[module_id.local_id.0]; + let parent_local_id = module_data.parent; + parent_local_id.map(|local_id| ModuleId { krate: module_id.krate, local_id }) + }); // This is a bit strange, but the visibility is always that of the item inside another module, // so the visibility we update here is for the next loop check. From 60c770f5f2594eea31ac75c852980edefa40d9eb Mon Sep 17 00:00:00 2001 From: Tom French <15848336+TomAFrench@users.noreply.github.com> Date: Mon, 28 Oct 2024 19:00:56 +0000 Subject: [PATCH 19/30] feat: do not increment reference counts on arrays through references (#6375) --- compiler/noirc_evaluator/src/ssa/function_builder/mod.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/compiler/noirc_evaluator/src/ssa/function_builder/mod.rs b/compiler/noirc_evaluator/src/ssa/function_builder/mod.rs index f810b65d105..aebb47ccf8e 100644 --- a/compiler/noirc_evaluator/src/ssa/function_builder/mod.rs +++ b/compiler/noirc_evaluator/src/ssa/function_builder/mod.rs @@ -442,13 +442,7 @@ impl FunctionBuilder { match self.type_of_value(value) { Type::Numeric(_) => (), Type::Function => (), - Type::Reference(element) => { - if element.contains_an_array() { - let reference = value; - let value = self.insert_load(reference, element.as_ref().clone()); - self.update_array_reference_count(value, increment); - } - } + Type::Reference(_) => (), Type::Array(..) | Type::Slice(..) => { // If there are nested arrays or slices, we wait until ArrayGet // is issued to increment the count of that array. From 15c729a7f29564092411658be613145b18ddd226 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Tue, 29 Oct 2024 10:06:37 -0300 Subject: [PATCH 20/30] fix: allow globals in format strings (#6382) --- .../src/elaborator/expressions.rs | 32 ++++++++++++------- .../fmtstr_with_global/Nargo.toml | 7 ++++ .../fmtstr_with_global/src/main.nr | 5 +++ 3 files changed, 32 insertions(+), 12 deletions(-) create mode 100644 test_programs/execution_success/fmtstr_with_global/Nargo.toml create mode 100644 test_programs/execution_success/fmtstr_with_global/src/main.nr diff --git a/compiler/noirc_frontend/src/elaborator/expressions.rs b/compiler/noirc_frontend/src/elaborator/expressions.rs index 31a518ca97f..c0a91cca4d7 100644 --- a/compiler/noirc_frontend/src/elaborator/expressions.rs +++ b/compiler/noirc_frontend/src/elaborator/expressions.rs @@ -8,7 +8,7 @@ use crate::{ ast::{ ArrayLiteral, BlockExpression, CallExpression, CastExpression, ConstructorExpression, Expression, ExpressionKind, Ident, IfExpression, IndexExpression, InfixExpression, - ItemVisibility, Lambda, Literal, MemberAccessExpression, MethodCallExpression, + ItemVisibility, Lambda, Literal, MemberAccessExpression, MethodCallExpression, Path, PrefixExpression, StatementKind, UnaryOp, UnresolvedTypeData, UnresolvedTypeExpression, }, hir::{ @@ -21,7 +21,7 @@ use crate::{ hir_def::{ expr::{ HirArrayLiteral, HirBinaryOp, HirBlockExpression, HirCallExpression, HirCastExpression, - HirConstructorExpression, HirExpression, HirIfExpression, HirIndexExpression, + HirConstructorExpression, HirExpression, HirIdent, HirIfExpression, HirIndexExpression, HirInfixExpression, HirLambda, HirLiteral, HirMemberAccess, HirMethodCallExpression, HirPrefixExpression, }, @@ -247,27 +247,35 @@ impl<'context> Elaborator<'context> { let scope_tree = self.scopes.current_scope_tree(); let variable = scope_tree.find(ident_name); - if let Some((old_value, _)) = variable { + + let hir_ident = if let Some((old_value, _)) = variable { old_value.num_times_used += 1; - let ident = HirExpression::Ident(old_value.ident.clone(), None); - let expr_id = self.interner.push_expr(ident); - self.interner.push_expr_location(expr_id, call_expr_span, self.file); - let ident = old_value.ident.clone(); - let typ = self.type_check_variable(ident, expr_id, None); - self.interner.push_expr_type(expr_id, typ.clone()); - capture_types.push(typ); - fmt_str_idents.push(expr_id); + old_value.ident.clone() + } else if let Ok(definition_id) = + self.lookup_global(Path::from_single(ident_name.to_string(), call_expr_span)) + { + HirIdent::non_trait_method(definition_id, Location::new(call_expr_span, self.file)) } else if ident_name.parse::().is_ok() { self.push_err(ResolverError::NumericConstantInFormatString { name: ident_name.to_owned(), span: call_expr_span, }); + continue; } else { self.push_err(ResolverError::VariableNotDeclared { name: ident_name.to_owned(), span: call_expr_span, }); - } + continue; + }; + + let hir_expr = HirExpression::Ident(hir_ident.clone(), None); + let expr_id = self.interner.push_expr(hir_expr); + self.interner.push_expr_location(expr_id, call_expr_span, self.file); + let typ = self.type_check_variable(hir_ident, expr_id, None); + self.interner.push_expr_type(expr_id, typ.clone()); + capture_types.push(typ); + fmt_str_idents.push(expr_id); } let len = Type::Constant(str.len().into(), Kind::u32()); diff --git a/test_programs/execution_success/fmtstr_with_global/Nargo.toml b/test_programs/execution_success/fmtstr_with_global/Nargo.toml new file mode 100644 index 00000000000..889683f7410 --- /dev/null +++ b/test_programs/execution_success/fmtstr_with_global/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "fmtstr_with_global" +type = "bin" +authors = [""] +compiler_version = ">=0.32.0" + +[dependencies] diff --git a/test_programs/execution_success/fmtstr_with_global/src/main.nr b/test_programs/execution_success/fmtstr_with_global/src/main.nr new file mode 100644 index 00000000000..8b9c9635015 --- /dev/null +++ b/test_programs/execution_success/fmtstr_with_global/src/main.nr @@ -0,0 +1,5 @@ +global FOO = 1; + +fn main() { + println(f"foo = {FOO}"); +} From b42accf59c9294131ce2773ac3ebdb20f548ece5 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Tue, 29 Oct 2024 10:18:47 -0300 Subject: [PATCH 21/30] fix: (formatter) correctly format quote delimiters (#6377) --- tooling/nargo_fmt/src/formatter/expression.rs | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/tooling/nargo_fmt/src/formatter/expression.rs b/tooling/nargo_fmt/src/formatter/expression.rs index cd46b09f190..239b52b7d04 100644 --- a/tooling/nargo_fmt/src/formatter/expression.rs +++ b/tooling/nargo_fmt/src/formatter/expression.rs @@ -310,6 +310,24 @@ impl<'a, 'b> ChunkFormatter<'a, 'b> { } pub(super) fn format_quote(&mut self) -> ChunkGroup { + // A quote's prefix isn't captured in the token, so let's figure it out which one + // is it by looking at the source code. + let mut quote_source_code = + &self.source[self.token_span.start() as usize..self.token_span.end() as usize]; + + // Remove "quote" and any whitespace following it + quote_source_code = quote_source_code.strip_prefix("quote").unwrap(); + quote_source_code = quote_source_code.trim_start(); + + // The first char is the delimiter + let delimiter_start = quote_source_code.chars().next().unwrap(); + let delimiter_end = match delimiter_start { + '(' => ')', + '{' => '}', + '[' => ']', + _ => panic!("Unexpected delimiter: {}", delimiter_start), + }; + // We use the current token rather than the Tokens we got from `Token::Quote` because // the current token has whitespace and comments in it, while the one we got from // the parser doesn't. @@ -319,11 +337,13 @@ impl<'a, 'b> ChunkFormatter<'a, 'b> { let mut group = ChunkGroup::new(); group.verbatim(self.chunk(|formatter| { - formatter.write("quote {"); + formatter.write("quote"); + formatter.write_space(); + formatter.write(&delimiter_start.to_string()); for token in tokens.0 { formatter.write_source_span(token.to_span()); } - formatter.write("}"); + formatter.write(&delimiter_end.to_string()); })); group } @@ -2045,6 +2065,13 @@ global y = 1; assert_format(src, expected); } + #[test] + fn format_quote_with_bracket_delimiter() { + let src = "global x = quote [ 1 2 3 $four $(five) ];"; + let expected = "global x = quote [ 1 2 3 $four $(five) ];\n"; + assert_format(src, expected); + } + #[test] fn format_lambda_no_parameters() { let src = "global x = | | 1 ;"; From 0232b573c418ab74715b7cc1d3e858d993bc1c07 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Tue, 29 Oct 2024 11:12:06 -0300 Subject: [PATCH 22/30] feat: suggest removing `!` from macro call that doesn't return Quoted (#6384) --- .../src/hir/type_check/errors.rs | 14 ++- tooling/lsp/src/requests/code_action.rs | 34 ++++++- .../code_action/remove_bang_from_call.rs | 97 +++++++++++++++++++ 3 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 tooling/lsp/src/requests/code_action/remove_bang_from_call.rs diff --git a/compiler/noirc_frontend/src/hir/type_check/errors.rs b/compiler/noirc_frontend/src/hir/type_check/errors.rs index 99de6bca434..3b4ab148ef7 100644 --- a/compiler/noirc_frontend/src/hir/type_check/errors.rs +++ b/compiler/noirc_frontend/src/hir/type_check/errors.rs @@ -434,11 +434,15 @@ impl<'a> From<&'a TypeCheckError> for Diagnostic { let msg = format!("Expected {expected_count} generic{expected_plural} from this function, but {actual_count} {actual_plural} provided"); Diagnostic::simple_error(msg, "".into(), *span) }, - TypeCheckError::MacroReturningNonExpr { typ, span } => Diagnostic::simple_error( - format!("Expected macro call to return a `Quoted` but found a(n) `{typ}`"), - "Macro calls must return quoted values, otherwise there is no code to insert".into(), - *span, - ), + TypeCheckError::MacroReturningNonExpr { typ, span } => { + let mut error = Diagnostic::simple_error( + format!("Expected macro call to return a `Quoted` but found a(n) `{typ}`"), + "Macro calls must return quoted values, otherwise there is no code to insert.".into(), + *span, + ); + error.add_secondary("Hint: remove the `!` from the end of the function name.".to_string(), *span); + error + }, TypeCheckError::UnsupportedTurbofishUsage { span } => { let msg = "turbofish (`::<_>`) usage at this position isn't supported yet"; Diagnostic::simple_error(msg.to_string(), "".to_string(), *span) diff --git a/tooling/lsp/src/requests/code_action.rs b/tooling/lsp/src/requests/code_action.rs index f3e9130e17d..5c2831be7e9 100644 --- a/tooling/lsp/src/requests/code_action.rs +++ b/tooling/lsp/src/requests/code_action.rs @@ -12,7 +12,10 @@ use lsp_types::{ }; use noirc_errors::Span; use noirc_frontend::{ - ast::{ConstructorExpression, ItemVisibility, NoirTraitImpl, Path, UseTree, Visitor}, + ast::{ + CallExpression, ConstructorExpression, ItemVisibility, MethodCallExpression, NoirTraitImpl, + Path, UseTree, Visitor, + }, graph::CrateId, hir::def_map::{CrateDefMap, LocalModuleId, ModuleId}, node_interner::NodeInterner, @@ -29,6 +32,7 @@ use super::{process_request, to_lsp_location}; mod fill_struct_fields; mod implement_missing_members; mod import_or_qualify; +mod remove_bang_from_call; mod remove_unused_import; mod tests; @@ -250,4 +254,32 @@ impl<'a> Visitor for CodeActionFinder<'a> { true } + + fn visit_call_expression(&mut self, call: &CallExpression, span: Span) -> bool { + if !self.includes_span(span) { + return false; + } + + if call.is_macro_call { + self.remove_bang_from_call(call.func.span); + } + + true + } + + fn visit_method_call_expression( + &mut self, + method_call: &MethodCallExpression, + span: Span, + ) -> bool { + if !self.includes_span(span) { + return false; + } + + if method_call.is_macro_call { + self.remove_bang_from_call(method_call.method_name.span()); + } + + true + } } diff --git a/tooling/lsp/src/requests/code_action/remove_bang_from_call.rs b/tooling/lsp/src/requests/code_action/remove_bang_from_call.rs new file mode 100644 index 00000000000..90f4fef0efd --- /dev/null +++ b/tooling/lsp/src/requests/code_action/remove_bang_from_call.rs @@ -0,0 +1,97 @@ +use lsp_types::TextEdit; +use noirc_errors::{Location, Span}; +use noirc_frontend::{node_interner::ReferenceId, QuotedType, Type}; + +use crate::byte_span_to_range; + +use super::CodeActionFinder; + +impl<'a> CodeActionFinder<'a> { + pub(super) fn remove_bang_from_call(&mut self, span: Span) { + // If we can't find the referenced function, there's nothing we can do + let Some(ReferenceId::Function(func_id)) = + self.interner.find_referenced(Location::new(span, self.file)) + else { + return; + }; + + // If the return type is Quoted, all is good + let func_meta = self.interner.function_meta(&func_id); + if let Type::Quoted(QuotedType::Quoted) = func_meta.return_type() { + return; + } + + // The `!` comes right after the name + let byte_span = span.end() as usize..span.end() as usize + 1; + let Some(range) = byte_span_to_range(self.files, self.file, byte_span) else { + return; + }; + + let title = "Remove `!` from call".to_string(); + let text_edit = TextEdit { range, new_text: "".to_string() }; + + let code_action = self.new_quick_fix(title, text_edit); + self.code_actions.push(code_action); + } +} + +#[cfg(test)] +mod tests { + use tokio::test; + + use crate::requests::code_action::tests::assert_code_action; + + #[test] + async fn test_removes_bang_from_call() { + let title = "Remove `!` from call"; + + let src = r#" + fn foo() {} + + fn main() { + fo>|| Date: Tue, 29 Oct 2024 15:14:17 +0100 Subject: [PATCH 23/30] feat: Add capacities to brillig vectors and use them in slice ops (#6332) --- .../brillig/brillig_gen/brillig_black_box.rs | 17 +- .../src/brillig/brillig_gen/brillig_block.rs | 44 +-- .../brillig/brillig_gen/brillig_slice_ops.rs | 152 ++++++----- .../brillig_ir/codegen_control_flow.rs | 35 +++ .../src/brillig/brillig_ir/codegen_memory.rs | 256 +++++++++++++++++- .../src/brillig/brillig_ir/entry_point.rs | 2 +- .../src/brillig/brillig_ir/procedures/mod.rs | 16 +- .../procedures/prepare_vector_insert.rs | 71 +++-- .../procedures/prepare_vector_push.rs | 137 ++++++++-- .../brillig_ir/procedures/vector_copy.rs | 4 +- .../{vector_pop.rs => vector_pop_back.rs} | 86 +++--- .../brillig_ir/procedures/vector_pop_front.rs | 130 +++++++++ .../brillig_ir/procedures/vector_remove.rs | 39 ++- tooling/debugger/src/repl.rs | 8 + 14 files changed, 768 insertions(+), 229 deletions(-) rename compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/{vector_pop.rs => vector_pop_back.rs} (60%) create mode 100644 compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/vector_pop_front.rs diff --git a/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_black_box.rs b/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_black_box.rs index 55f89dcb30f..3685c9540f3 100644 --- a/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_black_box.rs +++ b/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_black_box.rs @@ -8,7 +8,7 @@ use acvm::{ use crate::brillig::brillig_ir::{ brillig_variable::BrilligVariable, debug_show::DebugToString, registers::RegisterAllocator, - BrilligBinaryOp, BrilligContext, + BrilligContext, }; /// Transforms SSA's black box function calls into the corresponding brillig instructions @@ -395,19 +395,8 @@ pub(crate) fn convert_black_box_call BrilligBlock<'block> { match output_register { // Returned vectors need to emit some bytecode to format the result as a BrilligVector ValueOrArray::HeapVector(heap_vector) => { - // Update the stack pointer so that we do not overwrite - // dynamic memory returned from other external calls - // Single values and allocation of fixed sized arrays has already been handled - // inside of `allocate_external_call_result` - let total_size = self.brillig_context.allocate_register(); - self.brillig_context.codegen_usize_op( - heap_vector.size, - total_size, - BrilligBinaryOp::Add, - 2, // RC and Length + self.brillig_context.initialize_externally_returned_vector( + output_variable.extract_vector(), + *heap_vector, ); - - self.brillig_context - .increase_free_memory_pointer_instruction(total_size); - let brillig_vector = output_variable.extract_vector(); - let size_pointer = self.brillig_context.allocate_register(); - - self.brillig_context.codegen_usize_op( - brillig_vector.pointer, - size_pointer, - BrilligBinaryOp::Add, - 1_usize, // Slices are [RC, Size, ...items] - ); - self.brillig_context - .store_instruction(size_pointer, heap_vector.size); - self.brillig_context.deallocate_register(size_pointer); - self.brillig_context.deallocate_register(total_size); - // Update the dynamic slice length maintained in SSA if let ValueOrArray::MemoryAddress(len_index) = output_values[i - 1] { @@ -515,8 +491,11 @@ impl<'block> BrilligBlock<'block> { element_size, ); - self.brillig_context - .codegen_initialize_vector(destination_vector, source_size_register); + self.brillig_context.codegen_initialize_vector( + destination_vector, + source_size_register, + None, + ); // Items let vector_items_pointer = @@ -1551,7 +1530,7 @@ impl<'block> BrilligBlock<'block> { let size = self .brillig_context .make_usize_constant_instruction(array.len().into()); - self.brillig_context.codegen_initialize_vector(vector, size); + self.brillig_context.codegen_initialize_vector(vector, size, None); self.brillig_context.deallocate_single_addr(size); } _ => unreachable!( @@ -1797,11 +1776,6 @@ impl<'block> BrilligBlock<'block> { // The stack pointer will then be updated by the caller of this method // once the external call is resolved and the array size is known self.brillig_context.load_free_memory_pointer_instruction(vector.pointer); - self.brillig_context.indirect_const_instruction( - vector.pointer, - BRILLIG_MEMORY_ADDRESSING_BIT_SIZE, - 1_usize.into(), - ); variable } diff --git a/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_slice_ops.rs b/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_slice_ops.rs index 76e35395dd6..26c7151bf07 100644 --- a/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_slice_ops.rs +++ b/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_slice_ops.rs @@ -79,17 +79,15 @@ impl<'block> BrilligBlock<'block> { source_vector: BrilligVector, removed_items: &[BrilligVariable], ) { - let read_pointer = self.brillig_context.allocate_register(); - self.brillig_context.call_vector_pop_procedure( + let read_pointer = self.brillig_context.codegen_make_vector_items_pointer(source_vector); + self.read_variables(read_pointer, removed_items); + self.brillig_context.deallocate_register(read_pointer); + + self.brillig_context.call_vector_pop_front_procedure( source_vector, target_vector, - read_pointer, removed_items.len(), - false, ); - - self.read_variables(read_pointer, removed_items); - self.brillig_context.deallocate_register(read_pointer); } pub(crate) fn slice_pop_back_operation( @@ -99,12 +97,11 @@ impl<'block> BrilligBlock<'block> { removed_items: &[BrilligVariable], ) { let read_pointer = self.brillig_context.allocate_register(); - self.brillig_context.call_vector_pop_procedure( + self.brillig_context.call_vector_pop_back_procedure( source_vector, target_vector, read_pointer, removed_items.len(), - true, ); self.read_variables(read_pointer, removed_items); @@ -213,7 +210,7 @@ mod tests { push_back: bool, array: Vec, item_to_push: FieldElement, - mut expected_return: Vec, + expected_return: Vec, ) { let arguments = vec![ BrilligParameter::Slice( @@ -223,11 +220,15 @@ mod tests { BrilligParameter::SingleAddr(BRILLIG_MEMORY_ADDRESSING_BIT_SIZE), ]; let result_length = array.len() + 1; + assert_eq!(result_length, expected_return.len()); + let result_length_with_metadata = result_length + 2; // Leading length and capacity + + // Entry points don't support returning slices, so we implicitly cast the vector to an array + // With the metadata at the start. let returns = vec![BrilligParameter::Array( vec![BrilligParameter::SingleAddr(BRILLIG_MEMORY_ADDRESSING_BIT_SIZE)], - result_length + 1, // Leading length since the we return a vector + result_length_with_metadata, )]; - expected_return.insert(0, FieldElement::from(result_length)); let (_, mut function_context, mut context) = create_test_environment(); @@ -262,14 +263,17 @@ mod tests { let bytecode = create_entry_point_bytecode(context, arguments, returns).byte_code; let (vm, return_data_offset, return_data_size) = create_and_run_vm(array.into_iter().chain(vec![item_to_push]).collect(), &bytecode); - assert_eq!(return_data_size, expected_return.len()); - assert_eq!( - vm.get_memory()[return_data_offset..(return_data_offset + expected_return.len())] - .iter() - .map(|mem_val| mem_val.to_field()) - .collect::>(), - expected_return - ); + assert_eq!(return_data_size, result_length_with_metadata); + let mut returned_vector: Vec = vm.get_memory() + [return_data_offset..(return_data_offset + result_length_with_metadata)] + .iter() + .map(|mem_val| mem_val.to_field()) + .collect(); + let returned_size = returned_vector.remove(0); + assert_eq!(returned_size, result_length.into()); + let _returned_capacity = returned_vector.remove(0); + + assert_eq!(returned_vector, expected_return); } test_case_push( @@ -321,7 +325,7 @@ mod tests { fn test_case_pop( pop_back: bool, array: Vec, - mut expected_return_array: Vec, + expected_return_array: Vec, expected_return_item: FieldElement, ) { let arguments = vec![BrilligParameter::Slice( @@ -329,15 +333,18 @@ mod tests { array.len(), )]; let result_length = array.len() - 1; + assert_eq!(result_length, expected_return_array.len()); + let result_length_with_metadata = result_length + 2; // Leading length and capacity + // Entry points don't support returning slices, so we implicitly cast the vector to an array + // With the metadata at the start. let returns = vec![ + BrilligParameter::SingleAddr(BRILLIG_MEMORY_ADDRESSING_BIT_SIZE), BrilligParameter::Array( vec![BrilligParameter::SingleAddr(BRILLIG_MEMORY_ADDRESSING_BIT_SIZE)], - result_length + 1, + result_length_with_metadata, ), - BrilligParameter::SingleAddr(BRILLIG_MEMORY_ADDRESSING_BIT_SIZE), ]; - expected_return_array.insert(0, FieldElement::from(result_length)); let (_, mut function_context, mut context) = create_test_environment(); @@ -367,22 +374,28 @@ mod tests { ); } - context.codegen_return(&[target_vector.pointer, removed_item.address]); + context.codegen_return(&[removed_item.address, target_vector.pointer]); let bytecode = create_entry_point_bytecode(context, arguments, returns).byte_code; - let expected_return: Vec<_> = - expected_return_array.into_iter().chain(vec![expected_return_item]).collect(); + let (vm, return_data_offset, return_data_size) = create_and_run_vm(array.clone(), &bytecode); - assert_eq!(return_data_size, expected_return.len()); - - assert_eq!( - vm.get_memory()[return_data_offset..(return_data_offset + expected_return.len())] - .iter() - .map(|mem_val| mem_val.to_field()) - .collect::>(), - expected_return - ); + // vector + removed item + assert_eq!(return_data_size, result_length_with_metadata + 1); + + let mut return_data: Vec = vm.get_memory() + [return_data_offset..(return_data_offset + return_data_size)] + .iter() + .map(|mem_val| mem_val.to_field()) + .collect(); + let returned_item = return_data.remove(0); + assert_eq!(returned_item, expected_return_item); + + let returned_size = return_data.remove(0); + assert_eq!(returned_size, result_length.into()); + let _returned_capacity = return_data.remove(0); + + assert_eq!(return_data, expected_return_array); } test_case_pop( @@ -414,7 +427,7 @@ mod tests { array: Vec, item: FieldElement, index: FieldElement, - mut expected_return: Vec, + expected_return: Vec, ) { let arguments = vec![ BrilligParameter::Slice( @@ -425,11 +438,15 @@ mod tests { BrilligParameter::SingleAddr(BRILLIG_MEMORY_ADDRESSING_BIT_SIZE), ]; let result_length = array.len() + 1; + assert_eq!(result_length, expected_return.len()); + let result_length_with_metadata = result_length + 2; // Leading length and capacity + + // Entry points don't support returning slices, so we implicitly cast the vector to an array + // With the metadata at the start. let returns = vec![BrilligParameter::Array( vec![BrilligParameter::SingleAddr(BRILLIG_MEMORY_ADDRESSING_BIT_SIZE)], - result_length + 1, + result_length_with_metadata, )]; - expected_return.insert(0, FieldElement::from(result_length)); let (_, mut function_context, mut context) = create_test_environment(); @@ -461,15 +478,18 @@ mod tests { let bytecode = create_entry_point_bytecode(context, arguments, returns).byte_code; let (vm, return_data_offset, return_data_size) = create_and_run_vm(calldata, &bytecode); - assert_eq!(return_data_size, expected_return.len()); - - assert_eq!( - vm.get_memory()[return_data_offset..(return_data_offset + expected_return.len())] - .iter() - .map(|mem_val| mem_val.to_field()) - .collect::>(), - expected_return - ); + assert_eq!(return_data_size, result_length_with_metadata); + + let mut returned_vector: Vec = vm.get_memory() + [return_data_offset..(return_data_offset + result_length_with_metadata)] + .iter() + .map(|mem_val| mem_val.to_field()) + .collect(); + let returned_size = returned_vector.remove(0); + assert_eq!(returned_size, result_length.into()); + let _returned_capacity = returned_vector.remove(0); + + assert_eq!(returned_vector, expected_return); } test_case_insert( @@ -546,7 +566,7 @@ mod tests { fn test_case_remove( array: Vec, index: FieldElement, - mut expected_array: Vec, + expected_array: Vec, expected_removed_item: FieldElement, ) { let arguments = vec![ @@ -557,15 +577,16 @@ mod tests { BrilligParameter::SingleAddr(BRILLIG_MEMORY_ADDRESSING_BIT_SIZE), ]; let result_length = array.len() - 1; + assert_eq!(result_length, expected_array.len()); + let result_length_with_metadata = result_length + 2; // Leading length and capacity let returns = vec![ + BrilligParameter::SingleAddr(BRILLIG_MEMORY_ADDRESSING_BIT_SIZE), BrilligParameter::Array( vec![BrilligParameter::SingleAddr(BRILLIG_MEMORY_ADDRESSING_BIT_SIZE)], - result_length + 1, + result_length_with_metadata, ), - BrilligParameter::SingleAddr(BRILLIG_MEMORY_ADDRESSING_BIT_SIZE), ]; - expected_array.insert(0, FieldElement::from(result_length)); let (_, mut function_context, mut context) = create_test_environment(); @@ -592,24 +613,29 @@ mod tests { &[BrilligVariable::SingleAddr(removed_item)], ); - context.codegen_return(&[target_vector.pointer, removed_item.address]); + context.codegen_return(&[removed_item.address, target_vector.pointer]); let calldata: Vec<_> = array.into_iter().chain(vec![index]).collect(); let bytecode = create_entry_point_bytecode(context, arguments, returns).byte_code; let (vm, return_data_offset, return_data_size) = create_and_run_vm(calldata, &bytecode); - let expected_return: Vec<_> = - expected_array.into_iter().chain(vec![expected_removed_item]).collect(); - assert_eq!(return_data_size, expected_return.len()); + // vector + removed item + assert_eq!(return_data_size, result_length_with_metadata + 1); - assert_eq!( - vm.get_memory()[return_data_offset..(return_data_offset + expected_return.len())] - .iter() - .map(|mem_val| mem_val.to_field()) - .collect::>(), - expected_return - ); + let mut return_data: Vec = vm.get_memory() + [return_data_offset..(return_data_offset + return_data_size)] + .iter() + .map(|mem_val| mem_val.to_field()) + .collect(); + let returned_item = return_data.remove(0); + assert_eq!(returned_item, expected_removed_item); + + let returned_size = return_data.remove(0); + assert_eq!(returned_size, result_length.into()); + let _returned_capacity = return_data.remove(0); + + assert_eq!(return_data, expected_array); } test_case_remove( diff --git a/compiler/noirc_evaluator/src/brillig/brillig_ir/codegen_control_flow.rs b/compiler/noirc_evaluator/src/brillig/brillig_ir/codegen_control_flow.rs index c305d8c78f3..b74154c16be 100644 --- a/compiler/noirc_evaluator/src/brillig/brillig_ir/codegen_control_flow.rs +++ b/compiler/noirc_evaluator/src/brillig/brillig_ir/codegen_control_flow.rs @@ -12,6 +12,41 @@ use super::{ }; impl BrilligContext { + pub(crate) fn codegen_generic_iteration( + &mut self, + make_iterator: impl FnOnce(&mut BrilligContext) -> T, + update_iterator: impl FnOnce(&mut BrilligContext, &T), + make_finish_condition: impl FnOnce(&mut BrilligContext, &T) -> SingleAddrVariable, + on_iteration: impl FnOnce(&mut BrilligContext, &T), + clean_iterator: impl FnOnce(&mut BrilligContext, T), + ) { + let iterator = make_iterator(self); + + let (loop_section, loop_label) = self.reserve_next_section_label(); + self.enter_section(loop_section); + + // Loop body + let should_end = make_finish_condition(self, &iterator); + + let (exit_loop_section, exit_loop_label) = self.reserve_next_section_label(); + + self.jump_if_instruction(should_end.address, exit_loop_label); + + // Call the on iteration function + on_iteration(self, &iterator); + + // Update iterator + update_iterator(self, &iterator); + self.jump_instruction(loop_label); + + // Exit the loop + self.enter_section(exit_loop_section); + + // Deallocate our temporary registers + self.deallocate_single_addr(should_end); + clean_iterator(self, iterator); + } + /// This codegen will issue a loop for (let iterator_register = loop_start; i < loop_bound; i += step) /// The body of the loop should be issued by the caller in the on_iteration closure. pub(crate) fn codegen_for_loop( diff --git a/compiler/noirc_evaluator/src/brillig/brillig_ir/codegen_memory.rs b/compiler/noirc_evaluator/src/brillig/brillig_ir/codegen_memory.rs index 0199d9537a6..a34034bb550 100644 --- a/compiler/noirc_evaluator/src/brillig/brillig_ir/codegen_memory.rs +++ b/compiler/noirc_evaluator/src/brillig/brillig_ir/codegen_memory.rs @@ -93,16 +93,125 @@ impl BrilligContext< ); } else { let value_register = self.allocate_register(); + let end_source_pointer = self.allocate_register(); + self.memory_op_instruction( + source_pointer, + num_elements_variable.address, + end_source_pointer, + BrilligBinaryOp::Add, + ); - self.codegen_loop(num_elements_variable.address, |ctx, iterator| { - ctx.codegen_load_with_offset(source_pointer, iterator, value_register); - ctx.codegen_store_with_offset(destination_pointer, iterator, value_register); - }); - + self.codegen_generic_iteration( + |brillig_context| { + let source_iterator = brillig_context.allocate_register(); + let target_iterator = brillig_context.allocate_register(); + + brillig_context.mov_instruction(source_iterator, source_pointer); + brillig_context.mov_instruction(target_iterator, destination_pointer); + + (source_iterator, target_iterator) + }, + |brillig_context, &(source_iterator, target_iterator)| { + brillig_context.codegen_usize_op_in_place( + source_iterator, + BrilligBinaryOp::Add, + 1, + ); + brillig_context.codegen_usize_op_in_place( + target_iterator, + BrilligBinaryOp::Add, + 1, + ); + }, + |brillig_context, &(source_iterator, _)| { + // We have finished when the source/target pointer is less than the source/target start + let finish_condition = + SingleAddrVariable::new(brillig_context.allocate_register(), 1); + brillig_context.memory_op_instruction( + source_iterator, + end_source_pointer, + finish_condition.address, + BrilligBinaryOp::Equals, + ); + finish_condition + }, + |brillig_context, &(source_iterator, target_iterator)| { + brillig_context.load_instruction(value_register, source_iterator); + brillig_context.store_instruction(target_iterator, value_register); + }, + |brillig_context, (source_iterator, target_iterator)| { + brillig_context.deallocate_register(source_iterator); + brillig_context.deallocate_register(target_iterator); + }, + ); self.deallocate_register(value_register); + self.deallocate_register(end_source_pointer); } } + /// Copies num_elements_variable from the source pointer to the target pointer, starting from the end + pub(crate) fn codegen_mem_copy_from_the_end( + &mut self, + source_start: MemoryAddress, + target_start: MemoryAddress, + num_elements_variable: SingleAddrVariable, + ) { + self.codegen_generic_iteration( + |brillig_context| { + // Create the pointer to the last item for both source and target + let num_items_minus_one = brillig_context.allocate_register(); + brillig_context.codegen_usize_op( + num_elements_variable.address, + num_items_minus_one, + BrilligBinaryOp::Sub, + 1, + ); + let target_pointer = brillig_context.allocate_register(); + brillig_context.memory_op_instruction( + target_start, + num_items_minus_one, + target_pointer, + BrilligBinaryOp::Add, + ); + let source_pointer = brillig_context.allocate_register(); + brillig_context.memory_op_instruction( + source_start, + num_items_minus_one, + source_pointer, + BrilligBinaryOp::Add, + ); + brillig_context.deallocate_register(num_items_minus_one); + (source_pointer, target_pointer) + }, + |brillig_context, &(source_pointer, target_pointer)| { + brillig_context.codegen_usize_op_in_place(source_pointer, BrilligBinaryOp::Sub, 1); + brillig_context.codegen_usize_op_in_place(target_pointer, BrilligBinaryOp::Sub, 1); + }, + |brillig_context, &(source_pointer, _)| { + // We have finished when the source/target pointer is less than the source/target start + let finish_condition = + SingleAddrVariable::new(brillig_context.allocate_register(), 1); + brillig_context.memory_op_instruction( + source_pointer, + source_start, + finish_condition.address, + BrilligBinaryOp::LessThan, + ); + finish_condition + }, + |brillig_context, &(source_pointer, target_pointer)| { + let value_register = brillig_context.allocate_register(); + brillig_context.load_instruction(value_register, source_pointer); + brillig_context.store_instruction(target_pointer, value_register); + brillig_context.deallocate_register(value_register); + }, + |brillig_context, (source_pointer, target_pointer)| { + brillig_context.deallocate_register(source_pointer); + brillig_context.deallocate_register(target_pointer); + }, + ); + } + /// This instruction will reverse the order of the `size` elements pointed by `pointer`. pub(crate) fn codegen_array_reverse( &mut self, @@ -170,7 +279,7 @@ impl BrilligContext< self.codegen_usize_op(vector.pointer, current_pointer, BrilligBinaryOp::Add, 1); self.load_instruction(heap_vector.size, current_pointer); // Now prepare the pointer to the items - self.codegen_usize_op(current_pointer, heap_vector.pointer, BrilligBinaryOp::Add, 1); + self.codegen_usize_op(current_pointer, heap_vector.pointer, BrilligBinaryOp::Add, 2); self.deallocate_register(current_pointer); heap_vector @@ -201,16 +310,73 @@ impl BrilligContext< result } + pub(crate) fn codegen_update_vector_length( + &mut self, + vector: BrilligVector, + new_length: SingleAddrVariable, + ) { + let write_pointer = self.allocate_register(); + self.codegen_usize_op(vector.pointer, write_pointer, BrilligBinaryOp::Add, 1); + self.store_instruction(write_pointer, new_length.address); + self.deallocate_register(write_pointer); + } + + /// Returns a variable holding the capacity of a given vector + pub(crate) fn codegen_make_vector_capacity( + &mut self, + vector: BrilligVector, + ) -> SingleAddrVariable { + let result = SingleAddrVariable::new_usize(self.allocate_register()); + self.codegen_usize_op(vector.pointer, result.address, BrilligBinaryOp::Add, 2); + self.load_instruction(result.address, result.address); + result + } + + /// Writes a pointer to the items of a given vector + pub(crate) fn codegen_vector_items_pointer( + &mut self, + vector: BrilligVector, + result: MemoryAddress, + ) { + self.codegen_usize_op(vector.pointer, result, BrilligBinaryOp::Add, 3); + } + /// Returns a pointer to the items of a given vector pub(crate) fn codegen_make_vector_items_pointer( &mut self, vector: BrilligVector, ) -> MemoryAddress { let result = self.allocate_register(); - self.codegen_usize_op(vector.pointer, result, BrilligBinaryOp::Add, 2); + self.codegen_vector_items_pointer(vector, result); result } + /// Reads the metadata of a vector and stores it in the given variables + pub(crate) fn codegen_read_vector_metadata( + &mut self, + vector: BrilligVector, + rc: SingleAddrVariable, + size: SingleAddrVariable, + capacity: SingleAddrVariable, + items_pointer: SingleAddrVariable, + ) { + assert!(rc.bit_size == BRILLIG_MEMORY_ADDRESSING_BIT_SIZE); + assert!(size.bit_size == BRILLIG_MEMORY_ADDRESSING_BIT_SIZE); + assert!(capacity.bit_size == BRILLIG_MEMORY_ADDRESSING_BIT_SIZE); + assert!(items_pointer.bit_size == BRILLIG_MEMORY_ADDRESSING_BIT_SIZE); + + self.load_instruction(rc.address, vector.pointer); + + let read_pointer = self.allocate_register(); + self.codegen_usize_op(vector.pointer, read_pointer, BrilligBinaryOp::Add, 1); + self.load_instruction(size.address, read_pointer); + self.codegen_usize_op_in_place(read_pointer, BrilligBinaryOp::Add, 1); + self.load_instruction(capacity.address, read_pointer); + self.codegen_usize_op(read_pointer, items_pointer.address, BrilligBinaryOp::Add, 1); + + self.deallocate_register(read_pointer); + } + /// Returns a variable holding the length of a given array pub(crate) fn codegen_make_array_length(&mut self, array: BrilligArray) -> SingleAddrVariable { let result = SingleAddrVariable::new_usize(self.allocate_register()); @@ -262,28 +428,88 @@ impl BrilligContext< ); } - /// Initializes a vector, allocating memory to store its representation and initializing the reference counter and size. + pub(crate) fn codegen_initialize_vector_metadata( + &mut self, + vector: BrilligVector, + size: SingleAddrVariable, + capacity: Option, + ) { + // Write RC + self.indirect_const_instruction( + vector.pointer, + BRILLIG_MEMORY_ADDRESSING_BIT_SIZE, + 1_usize.into(), + ); + + // Write size + let write_pointer = self.allocate_register(); + self.codegen_usize_op(vector.pointer, write_pointer, BrilligBinaryOp::Add, 1); + self.store_instruction(write_pointer, size.address); + + // Write capacity + self.codegen_usize_op_in_place(write_pointer, BrilligBinaryOp::Add, 1); + self.store_instruction(write_pointer, capacity.unwrap_or(size).address); + + self.deallocate_register(write_pointer); + } + + /// Initializes a vector, allocating memory to store its representation and initializing the reference counter, size and capacity pub(crate) fn codegen_initialize_vector( &mut self, vector: BrilligVector, size: SingleAddrVariable, + capacity: Option, // Defaults to size if None ) { let allocation_size = self.allocate_register(); - self.codegen_usize_op(size.address, allocation_size, BrilligBinaryOp::Add, 2); + // Allocation size = capacity + 3 (rc, size, capacity) + self.codegen_usize_op( + capacity.unwrap_or(size).address, + allocation_size, + BrilligBinaryOp::Add, + 3, + ); self.codegen_allocate_mem(vector.pointer, allocation_size); self.deallocate_register(allocation_size); - // Write RC + self.codegen_initialize_vector_metadata(vector, size, capacity); + } + + /// We don't know the length of a vector returned externally before the call + /// so we pass the free memory pointer and then use this function to allocate + /// after the fact when we know the length. + pub(crate) fn initialize_externally_returned_vector( + &mut self, + vector: BrilligVector, + resulting_heap_vector: HeapVector, + ) { + let total_size = self.allocate_register(); + self.codegen_usize_op( + resulting_heap_vector.size, + total_size, + BrilligBinaryOp::Add, + 3, // Rc, length and capacity + ); + + self.increase_free_memory_pointer_instruction(total_size); + let write_pointer = self.allocate_register(); + + // Vectors are [RC, Size, Capacity, ...items] + // Initialize RC self.indirect_const_instruction( vector.pointer, BRILLIG_MEMORY_ADDRESSING_BIT_SIZE, 1_usize.into(), ); - // Write size - let len_write_pointer = self.allocate_register(); - self.codegen_usize_op(vector.pointer, len_write_pointer, BrilligBinaryOp::Add, 1); - self.store_instruction(len_write_pointer, size.address); - self.deallocate_register(len_write_pointer); + // Initialize size + self.codegen_usize_op(vector.pointer, write_pointer, BrilligBinaryOp::Add, 1_usize); + self.store_instruction(write_pointer, resulting_heap_vector.size); + + // Initialize capacity + self.codegen_usize_op_in_place(write_pointer, BrilligBinaryOp::Add, 1_usize); + self.store_instruction(write_pointer, resulting_heap_vector.size); + + self.deallocate_register(write_pointer); + self.deallocate_register(total_size); } } diff --git a/compiler/noirc_evaluator/src/brillig/brillig_ir/entry_point.rs b/compiler/noirc_evaluator/src/brillig/brillig_ir/entry_point.rs index 75d91716c23..b78dcb09d9a 100644 --- a/compiler/noirc_evaluator/src/brillig/brillig_ir/entry_point.rs +++ b/compiler/noirc_evaluator/src/brillig/brillig_ir/entry_point.rs @@ -196,7 +196,7 @@ impl BrilligContext { let deflattened_items_pointer = if is_vector { let vector = BrilligVector { pointer: deflattened_array_pointer }; - self.codegen_initialize_vector(vector, deflattened_size_variable); + self.codegen_initialize_vector(vector, deflattened_size_variable, None); self.codegen_make_vector_items_pointer(vector) } else { diff --git a/compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/mod.rs b/compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/mod.rs index 0ee6fe49435..1c3d1f4d0ad 100644 --- a/compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/mod.rs +++ b/compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/mod.rs @@ -5,7 +5,8 @@ mod mem_copy; mod prepare_vector_insert; mod prepare_vector_push; mod vector_copy; -mod vector_pop; +mod vector_pop_back; +mod vector_pop_front; mod vector_remove; use array_copy::compile_array_copy_procedure; @@ -15,7 +16,8 @@ use mem_copy::compile_mem_copy_procedure; use prepare_vector_insert::compile_prepare_vector_insert_procedure; use prepare_vector_push::compile_prepare_vector_push_procedure; use vector_copy::compile_vector_copy_procedure; -use vector_pop::compile_vector_pop_procedure; +use vector_pop_back::compile_vector_pop_back_procedure; +use vector_pop_front::compile_vector_pop_front_procedure; use vector_remove::compile_vector_remove_procedure; use crate::brillig::brillig_ir::AcirField; @@ -36,7 +38,8 @@ pub(crate) enum ProcedureId { VectorCopy, MemCopy, PrepareVectorPush(bool), - VectorPop(bool), + VectorPopFront, + VectorPopBack, PrepareVectorInsert, VectorRemove, CheckMaxStackDepth, @@ -56,8 +59,11 @@ pub(crate) fn compile_procedure( ProcedureId::PrepareVectorPush(push_back) => { compile_prepare_vector_push_procedure(&mut brillig_context, push_back); } - ProcedureId::VectorPop(pop_back) => { - compile_vector_pop_procedure(&mut brillig_context, pop_back); + ProcedureId::VectorPopFront => { + compile_vector_pop_front_procedure(&mut brillig_context); + } + ProcedureId::VectorPopBack => { + compile_vector_pop_back_procedure(&mut brillig_context); } ProcedureId::PrepareVectorInsert => { compile_prepare_vector_insert_procedure(&mut brillig_context); diff --git a/compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/prepare_vector_insert.rs b/compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/prepare_vector_insert.rs index 8dbbf80782c..1c1a738509c 100644 --- a/compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/prepare_vector_insert.rs +++ b/compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/prepare_vector_insert.rs @@ -2,7 +2,7 @@ use std::vec; use acvm::{acir::brillig::MemoryAddress, AcirField}; -use super::ProcedureId; +use super::{prepare_vector_push::reallocate_vector_for_insertion, ProcedureId}; use crate::brillig::brillig_ir::{ brillig_variable::{BrilligVector, SingleAddrVariable}, debug_show::DebugToString, @@ -58,9 +58,19 @@ pub(super) fn compile_prepare_vector_insert_procedure( + brillig_context: &mut BrilligContext, + source_vector: BrilligVector, + source_rc: SingleAddrVariable, + source_capacity: SingleAddrVariable, + target_vector: BrilligVector, + target_size: SingleAddrVariable, +) { + let does_capacity_fit = SingleAddrVariable::new(brillig_context.allocate_register(), 1); + brillig_context.memory_op_instruction( + target_size.address, + source_capacity.address, + does_capacity_fit.address, + BrilligBinaryOp::LessThanEquals, + ); + + let is_rc_one = SingleAddrVariable::new(brillig_context.allocate_register(), 1); + brillig_context.codegen_usize_op( + source_rc.address, + is_rc_one.address, + BrilligBinaryOp::Equals, + 1, + ); + + // Reallocate target vector for insertion + brillig_context.codegen_branch( + does_capacity_fit.address, + |brillig_context, does_capacity_fit| { + if does_capacity_fit { + brillig_context.codegen_branch(is_rc_one.address, |brillig_context, is_rc_one| { + if is_rc_one { + // We can insert in place, so we can just move the source pointer to the destination pointer and update the length + brillig_context + .mov_instruction(target_vector.pointer, source_vector.pointer); + brillig_context.codegen_update_vector_length(target_vector, target_size); + } else { + brillig_context.codegen_initialize_vector( + target_vector, + target_size, + Some(source_capacity), + ); + } + }); + } else { + let double_size = + SingleAddrVariable::new_usize(brillig_context.allocate_register()); + brillig_context.codegen_usize_op( + target_size.address, + double_size.address, + BrilligBinaryOp::Mul, + 2_usize, + ); + brillig_context.codegen_initialize_vector( + target_vector, + target_size, + Some(double_size), + ); + brillig_context.deallocate_single_addr(double_size); + } + }, + ); + + brillig_context.deallocate_single_addr(is_rc_one); + brillig_context.deallocate_single_addr(does_capacity_fit); +} diff --git a/compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/vector_copy.rs b/compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/vector_copy.rs index 7695e840c0b..6d2f9c4afb4 100644 --- a/compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/vector_copy.rs +++ b/compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/vector_copy.rs @@ -53,8 +53,8 @@ pub(super) fn compile_vector_copy_procedure( let result_vector = BrilligVector { pointer: new_vector_pointer_return }; // Allocate the memory for the new vec - let allocation_size = ctx.codegen_make_vector_length(source_vector); - ctx.codegen_usize_op_in_place(allocation_size.address, BrilligBinaryOp::Add, 2_usize); + let allocation_size = ctx.codegen_make_vector_capacity(source_vector); + ctx.codegen_usize_op_in_place(allocation_size.address, BrilligBinaryOp::Add, 3_usize); // Capacity plus 3 (rc, len, cap) ctx.codegen_allocate_mem(result_vector.pointer, allocation_size.address); ctx.codegen_mem_copy(source_vector.pointer, result_vector.pointer, allocation_size); diff --git a/compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/vector_pop.rs b/compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/vector_pop_back.rs similarity index 60% rename from compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/vector_pop.rs rename to compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/vector_pop_back.rs index 8fcfebb2360..bfc9d512852 100644 --- a/compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/vector_pop.rs +++ b/compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/vector_pop_back.rs @@ -11,14 +11,13 @@ use crate::brillig::brillig_ir::{ }; impl BrilligContext { - /// Pops items from the vector, returning the new vector and the pointer to the popped items in read_pointer. - pub(crate) fn call_vector_pop_procedure( + /// Pops items from the back of a vector, returning the new vector and the pointer to the popped items in read_pointer. + pub(crate) fn call_vector_pop_back_procedure( &mut self, source_vector: BrilligVector, destination_vector: BrilligVector, read_pointer: MemoryAddress, item_pop_count: usize, - back: bool, ) { let source_vector_pointer_arg = MemoryAddress::direct(ScratchSpace::start()); let item_pop_count_arg = MemoryAddress::direct(ScratchSpace::start() + 1); @@ -28,16 +27,15 @@ impl BrilligContext< self.mov_instruction(source_vector_pointer_arg, source_vector.pointer); self.usize_const_instruction(item_pop_count_arg, item_pop_count.into()); - self.add_procedure_call_instruction(ProcedureId::VectorPop(back)); + self.add_procedure_call_instruction(ProcedureId::VectorPopBack); self.mov_instruction(destination_vector.pointer, new_vector_pointer_return); self.mov_instruction(read_pointer, read_pointer_return); } } -pub(super) fn compile_vector_pop_procedure( +pub(super) fn compile_vector_pop_back_procedure( brillig_context: &mut BrilligContext, - pop_back: bool, ) { let source_vector_pointer_arg = MemoryAddress::direct(ScratchSpace::start()); let item_pop_count_arg = MemoryAddress::direct(ScratchSpace::start() + 1); @@ -65,49 +63,47 @@ pub(super) fn compile_vector_pop_procedure( BrilligBinaryOp::Sub, ); - brillig_context.codegen_initialize_vector(target_vector, target_size); + let rc = brillig_context.allocate_register(); + brillig_context.load_instruction(rc, source_vector.pointer); + let is_rc_one = brillig_context.allocate_register(); + brillig_context.codegen_usize_op(rc, is_rc_one, BrilligBinaryOp::Equals, 1_usize); - // Now we offset the source pointer by removed_items.len() let source_vector_items_pointer = brillig_context.codegen_make_vector_items_pointer(source_vector); - let target_vector_items_pointer = - brillig_context.codegen_make_vector_items_pointer(target_vector); - - if pop_back { - // Now we copy the source vector starting at index 0 into the target vector - brillig_context.codegen_mem_copy( - source_vector_items_pointer, - target_vector_items_pointer, - target_size, - ); - brillig_context.memory_op_instruction( - source_vector_items_pointer, - target_size.address, - read_pointer_return, - BrilligBinaryOp::Add, - ); - } else { - let source_copy_pointer = brillig_context.allocate_register(); - brillig_context.memory_op_instruction( - source_vector_items_pointer, - item_pop_count_arg, - source_copy_pointer, - BrilligBinaryOp::Add, - ); - - // Now we copy the source vector starting at index removed_items.len() into the target vector - brillig_context.codegen_mem_copy( - source_copy_pointer, - target_vector_items_pointer, - target_size, - ); - brillig_context.mov_instruction(read_pointer_return, source_vector_items_pointer); - - brillig_context.deallocate_register(source_copy_pointer); - } + + brillig_context.codegen_branch(is_rc_one, |brillig_context, is_rc_one| { + if is_rc_one { + // We can reuse the source vector updating its length + brillig_context.mov_instruction(target_vector.pointer, source_vector.pointer); + brillig_context.codegen_update_vector_length(target_vector, target_size); + } else { + // We need to clone the source vector + brillig_context.codegen_initialize_vector(target_vector, target_size, None); + + let target_vector_items_pointer = + brillig_context.codegen_make_vector_items_pointer(target_vector); + + // Now we copy the source vector starting at index 0 into the target vector but with the reduced length + brillig_context.codegen_mem_copy( + source_vector_items_pointer, + target_vector_items_pointer, + target_size, + ); + brillig_context.deallocate_register(target_vector_items_pointer); + } + }); + + brillig_context.memory_op_instruction( + source_vector_items_pointer, + target_size.address, + read_pointer_return, + BrilligBinaryOp::Add, + ); + + brillig_context.deallocate_register(rc); + brillig_context.deallocate_register(is_rc_one); + brillig_context.deallocate_register(source_vector_items_pointer); brillig_context.deallocate_single_addr(source_size); brillig_context.deallocate_single_addr(target_size); - brillig_context.deallocate_register(source_vector_items_pointer); - brillig_context.deallocate_register(target_vector_items_pointer); } diff --git a/compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/vector_pop_front.rs b/compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/vector_pop_front.rs new file mode 100644 index 00000000000..49123ca2f50 --- /dev/null +++ b/compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/vector_pop_front.rs @@ -0,0 +1,130 @@ +use std::vec; + +use acvm::{acir::brillig::MemoryAddress, AcirField}; + +use super::ProcedureId; +use crate::brillig::brillig_ir::{ + brillig_variable::{BrilligVector, SingleAddrVariable}, + debug_show::DebugToString, + registers::{RegisterAllocator, ScratchSpace}, + BrilligBinaryOp, BrilligContext, +}; + +impl BrilligContext { + /// Pops items from the front of a vector, returning the new vector + pub(crate) fn call_vector_pop_front_procedure( + &mut self, + source_vector: BrilligVector, + destination_vector: BrilligVector, + item_pop_count: usize, + ) { + let source_vector_pointer_arg = MemoryAddress::direct(ScratchSpace::start()); + let item_pop_count_arg = MemoryAddress::direct(ScratchSpace::start() + 1); + let new_vector_pointer_return = MemoryAddress::direct(ScratchSpace::start() + 2); + + self.mov_instruction(source_vector_pointer_arg, source_vector.pointer); + self.usize_const_instruction(item_pop_count_arg, item_pop_count.into()); + + self.add_procedure_call_instruction(ProcedureId::VectorPopFront); + + self.mov_instruction(destination_vector.pointer, new_vector_pointer_return); + } +} + +pub(super) fn compile_vector_pop_front_procedure( + brillig_context: &mut BrilligContext, +) { + let source_vector_pointer_arg = MemoryAddress::direct(ScratchSpace::start()); + let item_pop_count_arg = MemoryAddress::direct(ScratchSpace::start() + 1); + let new_vector_pointer_return = MemoryAddress::direct(ScratchSpace::start() + 2); + + brillig_context.set_allocated_registers(vec![ + source_vector_pointer_arg, + item_pop_count_arg, + new_vector_pointer_return, + ]); + + let source_vector = BrilligVector { pointer: source_vector_pointer_arg }; + let target_vector = BrilligVector { pointer: new_vector_pointer_return }; + + let source_rc = SingleAddrVariable::new_usize(brillig_context.allocate_register()); + let source_size = SingleAddrVariable::new_usize(brillig_context.allocate_register()); + let source_capacity = SingleAddrVariable::new_usize(brillig_context.allocate_register()); + let source_items_pointer = SingleAddrVariable::new_usize(brillig_context.allocate_register()); + brillig_context.codegen_read_vector_metadata( + source_vector, + source_rc, + source_size, + source_capacity, + source_items_pointer, + ); + + // target_size = source_size - item_pop_count + let target_size = SingleAddrVariable::new_usize(brillig_context.allocate_register()); + brillig_context.memory_op_instruction( + source_size.address, + item_pop_count_arg, + target_size.address, + BrilligBinaryOp::Sub, + ); + + let is_rc_one = brillig_context.allocate_register(); + brillig_context.codegen_usize_op( + source_rc.address, + is_rc_one, + BrilligBinaryOp::Equals, + 1_usize, + ); + + brillig_context.codegen_branch(is_rc_one, |brillig_context, is_rc_one| { + if is_rc_one { + // We reuse the source vector, moving the metadata to the right (decreasing capacity) + brillig_context.memory_op_instruction( + source_vector.pointer, + item_pop_count_arg, + target_vector.pointer, + BrilligBinaryOp::Add, + ); + brillig_context.memory_op_instruction( + source_capacity.address, + item_pop_count_arg, + source_capacity.address, + BrilligBinaryOp::Sub, + ); + brillig_context.codegen_initialize_vector_metadata( + target_vector, + target_size, + Some(source_capacity), + ); + } else { + brillig_context.codegen_initialize_vector(target_vector, target_size, None); + + let target_vector_items_pointer = + brillig_context.codegen_make_vector_items_pointer(target_vector); + + let source_copy_pointer = brillig_context.allocate_register(); + brillig_context.memory_op_instruction( + source_items_pointer.address, + item_pop_count_arg, + source_copy_pointer, + BrilligBinaryOp::Add, + ); + // Now we copy the source vector starting at index removed_items.len() into the target vector + brillig_context.codegen_mem_copy( + source_copy_pointer, + target_vector_items_pointer, + target_size, + ); + + brillig_context.deallocate_register(source_copy_pointer); + brillig_context.deallocate_register(target_vector_items_pointer); + } + }); + + brillig_context.deallocate_register(is_rc_one); + brillig_context.deallocate_single_addr(target_size); + brillig_context.deallocate_single_addr(source_rc); + brillig_context.deallocate_single_addr(source_size); + brillig_context.deallocate_single_addr(source_capacity); + brillig_context.deallocate_single_addr(source_items_pointer); +} diff --git a/compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/vector_remove.rs b/compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/vector_remove.rs index b7b54f970fa..7abc43286ee 100644 --- a/compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/vector_remove.rs +++ b/compiler/noirc_evaluator/src/brillig/brillig_ir/procedures/vector_remove.rs @@ -53,7 +53,7 @@ pub(super) fn compile_vector_remove_procedure( let target_vector = BrilligVector { pointer: new_vector_pointer_return }; let index = SingleAddrVariable::new_usize(index_arg); - // First we need to allocate the target vector decrementing the size by removed_items.len() + // Reallocate if necessary let source_size = brillig_context.codegen_make_vector_length(source_vector); let target_size = SingleAddrVariable::new_usize(brillig_context.allocate_register()); @@ -64,19 +64,36 @@ pub(super) fn compile_vector_remove_procedure( BrilligBinaryOp::Sub, ); - brillig_context.codegen_initialize_vector(target_vector, target_size); + let rc = brillig_context.allocate_register(); + brillig_context.load_instruction(rc, source_vector.pointer); + let is_rc_one = brillig_context.allocate_register(); + brillig_context.codegen_usize_op(rc, is_rc_one, BrilligBinaryOp::Equals, 1_usize); - // Copy the elements to the left of the index let source_vector_items_pointer = brillig_context.codegen_make_vector_items_pointer(source_vector); - let target_vector_items_pointer = - brillig_context.codegen_make_vector_items_pointer(target_vector); - brillig_context.codegen_mem_copy( - source_vector_items_pointer, - target_vector_items_pointer, - index, - ); + let target_vector_items_pointer = brillig_context.allocate_register(); + + brillig_context.codegen_branch(is_rc_one, |brillig_context, is_rc_one| { + if is_rc_one { + brillig_context.mov_instruction(target_vector.pointer, source_vector.pointer); + brillig_context.codegen_update_vector_length(target_vector, target_size); + brillig_context + .codegen_vector_items_pointer(target_vector, target_vector_items_pointer); + } else { + brillig_context.codegen_initialize_vector(target_vector, target_size, None); + + // Copy the elements to the left of the index + brillig_context + .codegen_vector_items_pointer(target_vector, target_vector_items_pointer); + + brillig_context.codegen_mem_copy( + source_vector_items_pointer, + target_vector_items_pointer, + index, + ); + } + }); // Compute the source pointer after the removed items let source_pointer_after_index = brillig_context.allocate_register(); @@ -124,6 +141,8 @@ pub(super) fn compile_vector_remove_procedure( SingleAddrVariable::new_usize(item_count), ); + brillig_context.deallocate_register(rc); + brillig_context.deallocate_register(is_rc_one); brillig_context.deallocate_register(source_pointer_after_index); brillig_context.deallocate_register(target_pointer_at_index); brillig_context.deallocate_register(item_count); diff --git a/tooling/debugger/src/repl.rs b/tooling/debugger/src/repl.rs index 012af0e88e8..486e84060f0 100644 --- a/tooling/debugger/src/repl.rs +++ b/tooling/debugger/src/repl.rs @@ -5,6 +5,8 @@ use acvm::acir::circuit::brillig::{BrilligBytecode, BrilligFunctionId}; use acvm::acir::circuit::{Circuit, Opcode, OpcodeLocation}; use acvm::acir::native_types::{Witness, WitnessMap, WitnessStack}; use acvm::brillig_vm::brillig::Opcode as BrilligOpcode; +use acvm::brillig_vm::MemoryValue; +use acvm::AcirField; use acvm::{BlackBoxFunctionSolver, FieldElement}; use nargo::NargoError; use noirc_driver::CompiledProgram; @@ -369,6 +371,12 @@ impl<'a, B: BlackBoxFunctionSolver> ReplDebugger<'a, B> { }; for (index, value) in memory.iter().enumerate() { + // Zero field is the default value, we omit it when printing memory + if let MemoryValue::Field(field) = value { + if field == &FieldElement::zero() { + continue; + } + } println!("{index} = {}", value); } } From e92b519bdfbd2a149a46745ad2ecffdd0e91f3f1 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Tue, 29 Oct 2024 11:27:00 -0300 Subject: [PATCH 24/30] feat: better LSP hover for functions (#6376) --- tooling/lsp/src/requests/hover.rs | 179 ++++++++++++++++-- .../test_programs/workspace/two/src/lib.nr | 30 ++- 2 files changed, 193 insertions(+), 16 deletions(-) diff --git a/tooling/lsp/src/requests/hover.rs b/tooling/lsp/src/requests/hover.rs index 5087955ea77..64974a42133 100644 --- a/tooling/lsp/src/requests/hover.rs +++ b/tooling/lsp/src/requests/hover.rs @@ -4,7 +4,7 @@ use async_lsp::ResponseError; use fm::{FileMap, PathString}; use lsp_types::{Hover, HoverContents, HoverParams, MarkupContent, MarkupKind}; use noirc_frontend::{ - ast::Visibility, + ast::{ItemVisibility, Visibility}, elaborator::types::try_eval_array_length_id, hir::def_map::ModuleId, hir_def::{ @@ -13,9 +13,9 @@ use noirc_frontend::{ traits::Trait, }, node_interner::{ - DefinitionId, DefinitionKind, ExprId, FuncId, GlobalId, ReferenceId, TraitId, TypeAliasId, + DefinitionId, DefinitionKind, ExprId, FuncId, GlobalId, NodeInterner, ReferenceId, + StructId, TraitId, TypeAliasId, }, - node_interner::{NodeInterner, StructId}, Generics, Shared, StructType, Type, TypeAlias, TypeBinding, TypeVariable, }; @@ -300,26 +300,113 @@ fn get_exprs_global_value(interner: &NodeInterner, exprs: &[ExprId]) -> Option String { let func_meta = args.interner.function_meta(&id); + let func_modifiers = args.interner.function_modifiers(&id); + let func_name_definition_id = args.interner.definition(func_meta.name.id); let mut string = String::new(); let formatted_parent_module = format_parent_module(ReferenceId::Function(id), args, &mut string); - let formatted_parent_struct = if let Some(struct_id) = func_meta.struct_id { + + let formatted_parent_type = if let Some(trait_impl_id) = func_meta.trait_impl { + let trait_impl = args.interner.get_trait_implementation(trait_impl_id); + let trait_impl = trait_impl.borrow(); + let trait_ = args.interner.get_trait(trait_impl.trait_id); + + let generics: Vec<_> = + trait_impl + .trait_generics + .iter() + .filter_map(|generic| { + if let Type::NamedGeneric(_, name) = generic { + Some(name) + } else { + None + } + }) + .collect(); + + string.push('\n'); + string.push_str(" impl"); + if !generics.is_empty() { + string.push('<'); + for (index, generic) in generics.into_iter().enumerate() { + if index > 0 { + string.push_str(", "); + } + string.push_str(generic); + } + string.push('>'); + } + + string.push(' '); + string.push_str(&trait_.name.0.contents); + if !trait_impl.trait_generics.is_empty() { + string.push('<'); + for (index, generic) in trait_impl.trait_generics.iter().enumerate() { + if index > 0 { + string.push_str(", "); + } + string.push_str(&generic.to_string()); + } + string.push('>'); + } + + string.push_str(" for "); + string.push_str(&trait_impl.typ.to_string()); + + true + } else if let Some(trait_id) = func_meta.trait_id { + let trait_ = args.interner.get_trait(trait_id); + string.push('\n'); + string.push_str(" trait "); + string.push_str(&trait_.name.0.contents); + format_generics(&trait_.generics, &mut string); + + true + } else if let Some(struct_id) = func_meta.struct_id { let struct_type = args.interner.get_struct(struct_id); let struct_type = struct_type.borrow(); if formatted_parent_module { string.push_str("::"); } string.push_str(&struct_type.name.0.contents); + string.push('\n'); + string.push_str(" "); + string.push_str("impl"); + + let impl_generics: Vec<_> = func_meta + .all_generics + .iter() + .take(func_meta.all_generics.len() - func_meta.direct_generics.len()) + .cloned() + .collect(); + format_generics(&impl_generics, &mut string); + + string.push(' '); + string.push_str(&struct_type.name.0.contents); + format_generic_names(&impl_generics, &mut string); + true } else { false }; - if formatted_parent_module || formatted_parent_struct { + if formatted_parent_module || formatted_parent_type { string.push('\n'); } string.push_str(" "); + + if func_modifiers.visibility != ItemVisibility::Private { + string.push_str(&func_modifiers.visibility.to_string()); + string.push(' '); + } + if func_modifiers.is_unconstrained { + string.push_str("unconstrained "); + } + if func_modifiers.is_comptime { + string.push_str("comptime "); + } + string.push_str("fn "); string.push_str(&func_name_definition_id.name); format_generics(&func_meta.direct_generics, &mut string); @@ -411,21 +498,55 @@ fn format_local(id: DefinitionId, args: &ProcessRequestCallbackArgs) -> String { string } -/// Some doc comments fn format_generics(generics: &Generics, string: &mut String) { + format_generics_impl( + generics, false, // only show names + string, + ); +} + +fn format_generic_names(generics: &Generics, string: &mut String) { + format_generics_impl( + generics, true, // only show names + string, + ); +} + +fn format_generics_impl(generics: &Generics, only_show_names: bool, string: &mut String) { if generics.is_empty() { return; } string.push('<'); for (index, generic) in generics.iter().enumerate() { - string.push_str(&generic.name); - if index != generics.len() - 1 { + if index > 0 { string.push_str(", "); } + + if only_show_names { + string.push_str(&generic.name); + } else { + match generic.kind() { + noirc_frontend::Kind::Any | noirc_frontend::Kind::Normal => { + string.push_str(&generic.name); + } + noirc_frontend::Kind::IntegerOrField | noirc_frontend::Kind::Integer => { + string.push_str("let "); + string.push_str(&generic.name); + string.push_str(": u32"); + } + noirc_frontend::Kind::Numeric(typ) => { + string.push_str("let "); + string.push_str(&generic.name); + string.push_str(": "); + string.push_str(&typ.to_string()); + } + } + } } string.push('>'); } + fn format_pattern(pattern: &HirPattern, interner: &NodeInterner, string: &mut String) { match pattern { HirPattern::Identifier(ident) => { @@ -647,6 +768,11 @@ mod hover_tests { use tokio::test; async fn assert_hover(directory: &str, file: &str, position: Position, expected_text: &str) { + let hover_text = get_hover_text(directory, file, position).await; + assert_eq!(hover_text, expected_text); + } + + async fn get_hover_text(directory: &str, file: &str, position: Position) -> String { let (mut state, noir_text_document) = test_utils::init_lsp_server(directory).await; // noir_text_document is always `src/main.nr` in the workspace directory, so let's go to the workspace dir @@ -673,7 +799,7 @@ mod hover_tests { panic!("Expected hover contents to be Markup"); }; - assert_eq!(markup.value, expected_text); + markup.value } #[test] @@ -759,7 +885,7 @@ mod hover_tests { "two/src/lib.nr", Position { line: 3, character: 4 }, r#" one - fn function_one()"#, + pub fn function_one()"#, ) .await; } @@ -771,7 +897,7 @@ mod hover_tests { "two/src/lib.nr", Position { line: 2, character: 7 }, r#" two - fn function_two()"#, + pub fn function_two()"#, ) .await; } @@ -783,6 +909,7 @@ mod hover_tests { "two/src/lib.nr", Position { line: 20, character: 6 }, r#" one::subone::SubOneStruct + impl SubOneStruct fn foo(self, x: i32, y: i32) -> Field"#, ) .await; @@ -892,7 +1019,7 @@ mod hover_tests { assert_hover( "workspace", "two/src/lib.nr", - Position { line: 43, character: 4 }, + Position { line: 42, character: 4 }, r#" two mod other"#, ) @@ -904,7 +1031,7 @@ mod hover_tests { assert_hover( "workspace", "two/src/lib.nr", - Position { line: 44, character: 11 }, + Position { line: 43, character: 11 }, r#" two mod other"#, ) @@ -955,8 +1082,32 @@ mod hover_tests { "workspace", "two/src/lib.nr", Position { line: 54, character: 2 }, - " two\n fn attr(_: FunctionDefinition) -> Quoted", + " two\n comptime fn attr(_: FunctionDefinition) -> Quoted", ) .await; } + + #[test] + async fn hover_on_generic_struct_function() { + let hover_text = + get_hover_text("workspace", "two/src/lib.nr", Position { line: 70, character: 11 }) + .await; + assert!(hover_text.starts_with( + " two::Foo + impl Foo + fn new() -> Foo" + )); + } + + #[test] + async fn hover_on_trait_impl_function_call() { + let hover_text = + get_hover_text("workspace", "two/src/lib.nr", Position { line: 83, character: 16 }) + .await; + assert!(hover_text.starts_with( + " two + impl Bar for Foo + pub fn bar_stuff(self)" + )); + } } diff --git a/tooling/lsp/test_programs/workspace/two/src/lib.nr b/tooling/lsp/test_programs/workspace/two/src/lib.nr index c6b516d88ab..2dec902f327 100644 --- a/tooling/lsp/test_programs/workspace/two/src/lib.nr +++ b/tooling/lsp/test_programs/workspace/two/src/lib.nr @@ -41,8 +41,8 @@ fn use_impl_method() { } mod other; -use other::another_function; use crate::other::other_function; +use other::another_function; use one::subone::GenericStruct; @@ -57,4 +57,30 @@ pub fn foo() {} comptime fn attr(_: FunctionDefinition) -> Quoted { quote { pub fn hello() {} } -} \ No newline at end of file +} + +struct Foo {} + +impl Foo { + fn new() -> Self { + Foo {} + } +} + +fn new_foo() -> Foo { + Foo::new() +} + +trait Bar { + fn bar_stuff(self); +} + +impl Bar for Foo { + fn bar_stuff(self) {} +} + +fn bar_stuff() { + let foo = Foo::new(); + foo.bar_stuff(); +} + From b985fdf6e635570b8db3af83d9ec14e7cd749062 Mon Sep 17 00:00:00 2001 From: jfecher Date: Tue, 29 Oct 2024 09:39:42 -0500 Subject: [PATCH 25/30] fix: Display every bit in integer tokens (#6360) Co-authored-by: Tom French --- acvm-repo/acir_field/src/field_element.rs | 71 +------------------ compiler/noirc_frontend/src/lexer/token.rs | 4 +- .../noirc_frontend/src/tests/bound_checks.rs | 4 +- .../test/browser/errors.test.ts | 2 +- .../noirc_abi_wasm/test/node/errors.test.ts | 2 +- 5 files changed, 7 insertions(+), 76 deletions(-) diff --git a/acvm-repo/acir_field/src/field_element.rs b/acvm-repo/acir_field/src/field_element.rs index 2323f008dbe..47ceb903111 100644 --- a/acvm-repo/acir_field/src/field_element.rs +++ b/acvm-repo/acir_field/src/field_element.rs @@ -8,7 +8,7 @@ use std::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign}; use crate::AcirField; // XXX: Switch out for a trait and proper implementations -// This implementation is in-efficient, can definitely remove hex usage and Iterator instances for trivial functionality +// This implementation is inefficient, can definitely remove hex usage and Iterator instances for trivial functionality #[derive(Default, Clone, Copy, Eq, PartialOrd, Ord)] pub struct FieldElement(F); @@ -33,46 +33,6 @@ impl std::fmt::Display for FieldElement { write!(f, "-")?; } - // Number of bits needed to represent the smaller representation - let num_bits = smaller_repr.bits(); - - // Check if the number represents a power of 2 - if smaller_repr.count_ones() == 1 { - let mut bit_index = 0; - for i in 0..num_bits { - if smaller_repr.bit(i) { - bit_index = i; - break; - } - } - return match bit_index { - 0 => write!(f, "1"), - 1 => write!(f, "2"), - 2 => write!(f, "4"), - 3 => write!(f, "8"), - _ => write!(f, "2{}", superscript(bit_index)), - }; - } - - // Check if number is a multiple of a power of 2. - // This is used because when computing the quotient - // we usually have numbers in the form 2^t * q + r - // We focus on 2^64, 2^32, 2^16, 2^8, 2^4 because - // they are common. We could extend this to a more - // general factorization strategy, but we pay in terms of CPU time - let mul_sign = "×"; - for power in [64, 32, 16, 8, 4] { - let power_of_two = BigUint::from(2_u128).pow(power); - if &smaller_repr % &power_of_two == BigUint::zero() { - return write!( - f, - "2{}{}{}", - superscript(power as u64), - mul_sign, - smaller_repr / &power_of_two, - ); - } - } write!(f, "{smaller_repr}") } } @@ -409,35 +369,6 @@ impl SubAssign for FieldElement { } } -// For pretty printing powers -fn superscript(n: u64) -> String { - if n == 0 { - "⁰".to_owned() - } else if n == 1 { - "¹".to_owned() - } else if n == 2 { - "²".to_owned() - } else if n == 3 { - "³".to_owned() - } else if n == 4 { - "⁴".to_owned() - } else if n == 5 { - "⁵".to_owned() - } else if n == 6 { - "⁶".to_owned() - } else if n == 7 { - "⁷".to_owned() - } else if n == 8 { - "⁸".to_owned() - } else if n == 9 { - "⁹".to_owned() - } else if n >= 10 { - superscript(n / 10) + &superscript(n % 10) - } else { - panic!("{}", n.to_string() + " can't be converted to superscript."); - } -} - #[cfg(test)] mod tests { use super::{AcirField, FieldElement}; diff --git a/compiler/noirc_frontend/src/lexer/token.rs b/compiler/noirc_frontend/src/lexer/token.rs index 8f05832d26d..daf59445982 100644 --- a/compiler/noirc_frontend/src/lexer/token.rs +++ b/compiler/noirc_frontend/src/lexer/token.rs @@ -1,4 +1,4 @@ -use acvm::{acir::AcirField, FieldElement}; +use acvm::FieldElement; use noirc_errors::{Position, Span, Spanned}; use std::fmt; @@ -367,7 +367,7 @@ impl fmt::Display for Token { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { Token::Ident(ref s) => write!(f, "{s}"), - Token::Int(n) => write!(f, "{}", n.to_u128()), + Token::Int(n) => write!(f, "{}", n), Token::Bool(b) => write!(f, "{b}"), Token::Str(ref b) => write!(f, "{b:?}"), Token::FmtStr(ref b) => write!(f, "f{b:?}"), diff --git a/compiler/noirc_frontend/src/tests/bound_checks.rs b/compiler/noirc_frontend/src/tests/bound_checks.rs index 271f9d7a1a7..05669bda411 100644 --- a/compiler/noirc_frontend/src/tests/bound_checks.rs +++ b/compiler/noirc_frontend/src/tests/bound_checks.rs @@ -14,7 +14,7 @@ fn overflowing_u8() { if let CompilationError::TypeError(error) = &errors[0].0 { assert_eq!( error.to_string(), - "The value `2⁸` cannot fit into `u8` which has range `0..=255`" + "The value `256` cannot fit into `u8` which has range `0..=255`" ); } else { panic!("Expected OverflowingAssignment error, got {:?}", errors[0].0); @@ -52,7 +52,7 @@ fn overflowing_i8() { if let CompilationError::TypeError(error) = &errors[0].0 { assert_eq!( error.to_string(), - "The value `2⁷` cannot fit into `i8` which has range `-128..=127`" + "The value `128` cannot fit into `i8` which has range `-128..=127`" ); } else { panic!("Expected OverflowingAssignment error, got {:?}", errors[0].0); diff --git a/tooling/noirc_abi_wasm/test/browser/errors.test.ts b/tooling/noirc_abi_wasm/test/browser/errors.test.ts index 0f75ff64a3e..cc060cff4d6 100644 --- a/tooling/noirc_abi_wasm/test/browser/errors.test.ts +++ b/tooling/noirc_abi_wasm/test/browser/errors.test.ts @@ -9,7 +9,7 @@ it('errors when an integer input overflows', async () => { const { abi, inputs } = await import('../shared/uint_overflow'); expect(() => abiEncode(abi, inputs)).to.throw( - 'The value passed for parameter `foo` does not match the specified type:\nValue Field(2³⁸) does not fall within range of allowable values for a Integer { sign: Unsigned, width: 32 }', + 'The value passed for parameter `foo` does not match the specified type:\nValue Field(274877906944) does not fall within range of allowable values for a Integer { sign: Unsigned, width: 32 }', ); }); diff --git a/tooling/noirc_abi_wasm/test/node/errors.test.ts b/tooling/noirc_abi_wasm/test/node/errors.test.ts index fba451b4a8c..491fd5a5671 100644 --- a/tooling/noirc_abi_wasm/test/node/errors.test.ts +++ b/tooling/noirc_abi_wasm/test/node/errors.test.ts @@ -5,7 +5,7 @@ it('errors when an integer input overflows', async () => { const { abi, inputs } = await import('../shared/uint_overflow'); expect(() => abiEncode(abi, inputs)).to.throw( - 'The value passed for parameter `foo` does not match the specified type:\nValue Field(2³⁸) does not fall within range of allowable values for a Integer { sign: Unsigned, width: 32 }', + 'The value passed for parameter `foo` does not match the specified type:\nValue Field(274877906944) does not fall within range of allowable values for a Integer { sign: Unsigned, width: 32 }', ); }); From eba151ecf59c61f7ffc6bec00d455dce84e7b927 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Tue, 29 Oct 2024 13:49:08 -0300 Subject: [PATCH 26/30] feat: support specifying generics on a struct when calling an associated function (#6306) --- .../src/elaborator/expressions.rs | 5 +- compiler/noirc_frontend/src/elaborator/mod.rs | 17 +- .../noirc_frontend/src/elaborator/patterns.rs | 121 ++++++--- .../noirc_frontend/src/elaborator/scope.rs | 105 +++++--- .../src/elaborator/statements.rs | 3 +- .../noirc_frontend/src/elaborator/types.rs | 32 +-- .../src/hir/def_collector/dc_crate.rs | 11 +- .../src/hir/def_map/module_def.rs | 76 ------ .../src/hir/def_map/namespace.rs | 8 - .../src/hir/resolution/errors.rs | 2 +- .../src/hir/resolution/import.rs | 255 +++++++++++++----- .../src/hir/resolution/path_resolver.rs | 6 +- compiler/noirc_frontend/src/locations.rs | 36 ++- compiler/noirc_frontend/src/tests.rs | 4 +- .../noirc_frontend/src/tests/turbofish.rs | 109 ++++++-- tooling/lsp/src/attribute_reference_finder.rs | 23 +- 16 files changed, 514 insertions(+), 299 deletions(-) diff --git a/compiler/noirc_frontend/src/elaborator/expressions.rs b/compiler/noirc_frontend/src/elaborator/expressions.rs index c0a91cca4d7..6dfd295db6f 100644 --- a/compiler/noirc_frontend/src/elaborator/expressions.rs +++ b/compiler/noirc_frontend/src/elaborator/expressions.rs @@ -251,7 +251,7 @@ impl<'context> Elaborator<'context> { let hir_ident = if let Some((old_value, _)) = variable { old_value.num_times_used += 1; old_value.ident.clone() - } else if let Ok(definition_id) = + } else if let Ok((definition_id, _)) = self.lookup_global(Path::from_single(ident_name.to_string(), call_expr_span)) { HirIdent::non_trait_method(definition_id, Location::new(call_expr_span, self.file)) @@ -536,9 +536,6 @@ impl<'context> Elaborator<'context> { last_segment.generics = Some(generics.ordered_args); } - let exclude_last_segment = true; - self.check_unsupported_turbofish_usage(&path, exclude_last_segment); - let last_segment = path.last_segment(); let is_self_type = last_segment.ident.is_self_type_name(); diff --git a/compiler/noirc_frontend/src/elaborator/mod.rs b/compiler/noirc_frontend/src/elaborator/mod.rs index 9f56b72f4e9..2a723286d8b 100644 --- a/compiler/noirc_frontend/src/elaborator/mod.rs +++ b/compiler/noirc_frontend/src/elaborator/mod.rs @@ -4,7 +4,8 @@ use std::{ }; use crate::{ - ast::ItemVisibility, hir_def::traits::ResolvedTraitBound, StructField, StructType, TypeBindings, + ast::ItemVisibility, hir::resolution::import::PathResolutionItem, + hir_def::traits::ResolvedTraitBound, StructField, StructType, TypeBindings, }; use crate::{ ast::{ @@ -20,7 +21,7 @@ use crate::{ }, def_collector::{dc_crate::CollectedItems, errors::DefCollectorErrorKind}, def_map::{DefMaps, ModuleData}, - def_map::{LocalModuleId, ModuleDefId, ModuleId, MAIN_FUNCTION}, + def_map::{LocalModuleId, ModuleId, MAIN_FUNCTION}, resolution::errors::ResolverError, resolution::import::PathResolution, scope::ScopeForest as GenericScopeForest, @@ -667,11 +668,11 @@ impl<'context> Elaborator<'context> { pub fn resolve_module_by_path(&mut self, path: Path) -> Option { match self.resolve_path(path.clone()) { - Ok(PathResolution { module_def_id: ModuleDefId::ModuleId(module_id), error }) => { - if error.is_some() { - None - } else { + Ok(PathResolution { item: PathResolutionItem::Module(module_id), errors }) => { + if errors.is_empty() { Some(module_id) + } else { + None } } _ => None, @@ -680,8 +681,8 @@ impl<'context> Elaborator<'context> { fn resolve_trait_by_path(&mut self, path: Path) -> Option { let error = match self.resolve_path(path.clone()) { - Ok(PathResolution { module_def_id: ModuleDefId::TraitId(trait_id), error }) => { - if let Some(error) = error { + Ok(PathResolution { item: PathResolutionItem::Trait(trait_id), errors }) => { + for error in errors { self.push_err(error); } return Some(trait_id); diff --git a/compiler/noirc_frontend/src/elaborator/patterns.rs b/compiler/noirc_frontend/src/elaborator/patterns.rs index d55011f98a1..b45f455633b 100644 --- a/compiler/noirc_frontend/src/elaborator/patterns.rs +++ b/compiler/noirc_frontend/src/elaborator/patterns.rs @@ -9,7 +9,7 @@ use crate::{ }, hir::{ def_collector::dc_crate::CompilationError, - resolution::errors::ResolverError, + resolution::{errors::ResolverError, import::PathResolutionItem}, type_check::{Source, TypeCheckError}, }, hir_def::{ @@ -178,9 +178,6 @@ impl<'context> Elaborator<'context> { mutable: Option, new_definitions: &mut Vec, ) -> HirPattern { - let exclude_last_segment = true; - self.check_unsupported_turbofish_usage(&name, exclude_last_segment); - let last_segment = name.last_segment(); let name_span = last_segment.ident.span(); let is_self_type = last_segment.ident.is_self_type_name(); @@ -195,7 +192,7 @@ impl<'context> Elaborator<'context> { }; let (struct_type, generics) = match self.lookup_type_or_error(name) { - Some(Type::Struct(struct_type, generics)) => (struct_type, generics), + Some(Type::Struct(struct_type, struct_generics)) => (struct_type, struct_generics), None => return error_identifier(self), Some(typ) => { let typ = typ.to_string(); @@ -468,54 +465,102 @@ impl<'context> Elaborator<'context> { } pub(super) fn elaborate_variable(&mut self, variable: Path) -> (ExprId, Type) { - let exclude_last_segment = true; - self.check_unsupported_turbofish_usage(&variable, exclude_last_segment); - let unresolved_turbofish = variable.segments.last().unwrap().generics.clone(); let span = variable.span; - let expr = self.resolve_variable(variable); + let (expr, item) = self.resolve_variable(variable); let definition_id = expr.id; + let type_generics = item.map(|item| self.resolve_item_turbofish(item)).unwrap_or_default(); + let definition_kind = self.interner.try_definition(definition_id).map(|definition| definition.kind.clone()); + let mut bindings = TypeBindings::new(); + // Resolve any generics if we the variable we have resolved is a function // and if the turbofish operator was used. - let generics = definition_kind.and_then(|definition_kind| match &definition_kind { - DefinitionKind::Function(function) => { - self.resolve_function_turbofish_generics(function, unresolved_turbofish, span) + let generics = if let Some(DefinitionKind::Function(func_id)) = &definition_kind { + self.resolve_function_turbofish_generics(func_id, unresolved_turbofish, span) + } else { + None + }; + + // If this is a function call on a type that has generics, we need to bind those generic types. + if !type_generics.is_empty() { + if let Some(DefinitionKind::Function(func_id)) = &definition_kind { + // `all_generics` will always have the enclosing type generics first, so we need to bind those + let func_generics = &self.interner.function_meta(func_id).all_generics; + for (type_generic, func_generic) in type_generics.into_iter().zip(func_generics) { + let type_var = &func_generic.type_var; + bindings + .insert(type_var.id(), (type_var.clone(), type_var.kind(), type_generic)); + } } - _ => None, - }); + } let id = self.interner.push_expr(HirExpression::Ident(expr.clone(), generics.clone())); self.interner.push_expr_location(id, span, self.file); - let typ = self.type_check_variable(expr, id, generics); + let typ = self.type_check_variable_with_bindings(expr, id, generics, bindings); self.interner.push_expr_type(id, typ.clone()); (id, typ) } - fn resolve_variable(&mut self, path: Path) -> HirIdent { + /// Solve any generics that are part of the path before the function, for example: + /// + /// foo::Bar::::baz + /// ^^^^^ + /// solve these + fn resolve_item_turbofish(&mut self, item: PathResolutionItem) -> Vec { + match item { + PathResolutionItem::StructFunction(struct_id, Some(generics), _func_id) => { + let struct_type = self.interner.get_struct(struct_id); + let struct_type = struct_type.borrow(); + let struct_generics = struct_type.instantiate(self.interner); + self.resolve_struct_turbofish_generics( + &struct_type, + struct_generics, + Some(generics.generics), + generics.span, + ) + } + PathResolutionItem::TypeAliasFunction(_type_alias_id, Some(generics), _func_id) => { + // TODO: https://github.com/noir-lang/noir/issues/6311 + self.push_err(TypeCheckError::UnsupportedTurbofishUsage { span: generics.span }); + Vec::new() + } + PathResolutionItem::TraitFunction(_trait_id, Some(generics), _func_id) => { + // TODO: https://github.com/noir-lang/noir/issues/6310 + self.push_err(TypeCheckError::UnsupportedTurbofishUsage { span: generics.span }); + Vec::new() + } + _ => Vec::new(), + } + } + + fn resolve_variable(&mut self, path: Path) -> (HirIdent, Option) { if let Some(trait_path_resolution) = self.resolve_trait_generic_path(&path) { - if let Some(error) = trait_path_resolution.error { + for error in trait_path_resolution.errors { self.push_err(error); } - HirIdent { - location: Location::new(path.span, self.file), - id: self.interner.trait_method_id(trait_path_resolution.method.method_id), - impl_kind: ImplKind::TraitMethod(trait_path_resolution.method), - } + ( + HirIdent { + location: Location::new(path.span, self.file), + id: self.interner.trait_method_id(trait_path_resolution.method.method_id), + impl_kind: ImplKind::TraitMethod(trait_path_resolution.method), + }, + None, + ) } else { // If the Path is being used as an Expression, then it is referring to a global from a separate module // Otherwise, then it is referring to an Identifier // This lookup allows support of such statements: let x = foo::bar::SOME_GLOBAL + 10; // If the expression is a singular indent, we search the resolver's current scope as normal. let span = path.span(); - let (hir_ident, var_scope_index) = self.get_ident_from_path(path); + let ((hir_ident, var_scope_index), item) = self.get_ident_from_path(path); if hir_ident.id != DefinitionId::dummy_id() { match self.interner.definition(hir_ident.id).kind { @@ -557,7 +602,7 @@ impl<'context> Elaborator<'context> { } } - hir_ident + (hir_ident, item) } } @@ -567,8 +612,17 @@ impl<'context> Elaborator<'context> { expr_id: ExprId, generics: Option>, ) -> Type { - let mut bindings = TypeBindings::new(); + let bindings = TypeBindings::new(); + self.type_check_variable_with_bindings(ident, expr_id, generics, bindings) + } + pub(super) fn type_check_variable_with_bindings( + &mut self, + ident: HirIdent, + expr_id: ExprId, + generics: Option>, + mut bindings: TypeBindings, + ) -> Type { // Add type bindings from any constraints that were used. // We need to do this first since otherwise instantiating the type below // will replace each trait generic with a fresh type variable, rather than @@ -668,24 +722,31 @@ impl<'context> Elaborator<'context> { } } - pub fn get_ident_from_path(&mut self, path: Path) -> (HirIdent, usize) { + pub fn get_ident_from_path( + &mut self, + path: Path, + ) -> ((HirIdent, usize), Option) { let location = Location::new(path.last_ident().span(), self.file); let error = match path.as_ident().map(|ident| self.use_variable(ident)) { - Some(Ok(found)) => return found, + Some(Ok(found)) => return (found, None), // Try to look it up as a global, but still issue the first error if we fail Some(Err(error)) => match self.lookup_global(path) { - Ok(id) => return (HirIdent::non_trait_method(id, location), 0), + Ok((id, item)) => { + return ((HirIdent::non_trait_method(id, location), 0), Some(item)) + } Err(_) => error, }, None => match self.lookup_global(path) { - Ok(id) => return (HirIdent::non_trait_method(id, location), 0), + Ok((id, item)) => { + return ((HirIdent::non_trait_method(id, location), 0), Some(item)) + } Err(error) => error, }, }; self.push_err(error); let id = DefinitionId::dummy_id(); - (HirIdent::non_trait_method(id, location), 0) + ((HirIdent::non_trait_method(id, location), 0), None) } pub(super) fn elaborate_type_path(&mut self, path: TypePath) -> (ExprId, Type) { diff --git a/compiler/noirc_frontend/src/elaborator/scope.rs b/compiler/noirc_frontend/src/elaborator/scope.rs index d38b7a50175..8e033c914be 100644 --- a/compiler/noirc_frontend/src/elaborator/scope.rs +++ b/compiler/noirc_frontend/src/elaborator/scope.rs @@ -2,14 +2,11 @@ use noirc_errors::{Location, Spanned}; use crate::ast::{Ident, Path, PathKind, ERROR_IDENT}; use crate::hir::def_map::{LocalModuleId, ModuleId}; -use crate::hir::resolution::import::{PathResolution, PathResolutionResult}; +use crate::hir::resolution::import::{PathResolution, PathResolutionItem, PathResolutionResult}; use crate::hir::resolution::path_resolver::{PathResolver, StandardPathResolver}; use crate::hir::scope::{Scope as GenericScope, ScopeTree as GenericScopeTree}; use crate::{ - hir::{ - def_map::{ModuleDefId, TryFromModuleDefId}, - resolution::errors::ResolverError, - }, + hir::resolution::errors::ResolverError, hir_def::{ expr::{HirCapturedVar, HirIdent}, traits::Trait, @@ -26,16 +23,6 @@ type Scope = GenericScope; type ScopeTree = GenericScopeTree; impl<'context> Elaborator<'context> { - pub(super) fn lookup(&mut self, path: Path) -> Result { - let span = path.span(); - let id = self.resolve_path_or_error(path)?; - T::try_from(id).ok_or_else(|| ResolverError::Expected { - expected: T::description(), - got: id.as_str().to_owned(), - span, - }) - } - pub fn module_id(&self) -> ModuleId { assert_ne!(self.local_module, LocalModuleId::dummy_id(), "local_module is unset"); ModuleId { krate: self.crate_id, local_id: self.local_module } @@ -53,14 +40,14 @@ impl<'context> Elaborator<'context> { pub(super) fn resolve_path_or_error( &mut self, path: Path, - ) -> Result { + ) -> Result { let path_resolution = self.resolve_path(path)?; - if let Some(error) = path_resolution.error { + for error in path_resolution.errors { self.push_err(error); } - Ok(path_resolution.module_def_id) + Ok(path_resolution.item) } pub(super) fn resolve_path(&mut self, path: Path) -> PathResolutionResult { @@ -72,8 +59,8 @@ impl<'context> Elaborator<'context> { let struct_type = struct_type.borrow(); if path.segments.len() == 1 { return Ok(PathResolution { - module_def_id: ModuleDefId::TypeId(struct_type.id), - error: None, + item: PathResolutionItem::Struct(struct_type.id), + errors: Vec::new(), }); } @@ -132,8 +119,8 @@ impl<'context> Elaborator<'context> { Err(err) => return Err(err), }; - self.interner.add_module_def_id_reference( - path_resolution.module_def_id, + self.interner.add_path_resolution_kind_reference( + path_resolution.item.clone(), location, is_self_type_name, ); @@ -183,21 +170,24 @@ impl<'context> Elaborator<'context> { } } - pub(super) fn lookup_global(&mut self, path: Path) -> Result { + pub(super) fn lookup_global( + &mut self, + path: Path, + ) -> Result<(DefinitionId, PathResolutionItem), ResolverError> { let span = path.span(); - let id = self.resolve_path_or_error(path)?; + let item = self.resolve_path_or_error(path)?; - if let Some(function) = TryFromModuleDefId::try_from(id) { - return Ok(self.interner.function_definition_id(function)); + if let Some(function) = item.function_id() { + return Ok((self.interner.function_definition_id(function), item)); } - if let Some(global) = TryFromModuleDefId::try_from(id) { + if let PathResolutionItem::Global(global) = item { let global = self.interner.get_global(global); - return Ok(global.definition_id); + return Ok((global.definition_id, item)); } - let expected = "global variable".into(); - let got = "local variable".into(); + let expected = "global variable"; + let got = "local variable"; Err(ResolverError::Expected { span, expected, got }) } @@ -239,10 +229,22 @@ impl<'context> Elaborator<'context> { /// Lookup a given trait by name/path. pub fn lookup_trait_or_error(&mut self, path: Path) -> Option<&mut Trait> { - match self.lookup(path) { - Ok(trait_id) => Some(self.get_trait_mut(trait_id)), - Err(error) => { - self.push_err(error); + let span = path.span(); + match self.resolve_path_or_error(path) { + Ok(item) => { + if let PathResolutionItem::Trait(trait_id) = item { + Some(self.get_trait_mut(trait_id)) + } else { + self.push_err(ResolverError::Expected { + expected: "trait", + got: item.description(), + span, + }); + None + } + } + Err(err) => { + self.push_err(err); None } } @@ -250,10 +252,22 @@ impl<'context> Elaborator<'context> { /// Lookup a given struct type by name. pub fn lookup_struct_or_error(&mut self, path: Path) -> Option> { - match self.lookup(path) { - Ok(struct_id) => Some(self.get_struct(struct_id)), - Err(error) => { - self.push_err(error); + let span = path.span(); + match self.resolve_path_or_error(path) { + Ok(item) => { + if let PathResolutionItem::Struct(struct_id) = item { + Some(self.get_struct(struct_id)) + } else { + self.push_err(ResolverError::Expected { + expected: "type", + got: item.description(), + span, + }); + None + } + } + Err(err) => { + self.push_err(err); None } } @@ -271,20 +285,20 @@ impl<'context> Elaborator<'context> { let span = path.span; match self.resolve_path_or_error(path) { - Ok(ModuleDefId::TypeId(struct_id)) => { + Ok(PathResolutionItem::Struct(struct_id)) => { let struct_type = self.get_struct(struct_id); let generics = struct_type.borrow().instantiate(self.interner); Some(Type::Struct(struct_type, generics)) } - Ok(ModuleDefId::TypeAliasId(alias_id)) => { + Ok(PathResolutionItem::TypeAlias(alias_id)) => { let alias = self.interner.get_type_alias(alias_id); let alias = alias.borrow(); Some(alias.instantiate(self.interner)) } Ok(other) => { self.push_err(ResolverError::Expected { - expected: StructId::description(), - got: other.as_str().to_owned(), + expected: "type", + got: other.description(), span, }); None @@ -297,6 +311,11 @@ impl<'context> Elaborator<'context> { } pub fn lookup_type_alias(&mut self, path: Path) -> Option> { - self.lookup(path).ok().map(|id| self.interner.get_type_alias(id)) + match self.resolve_path_or_error(path) { + Ok(PathResolutionItem::TypeAlias(type_alias_id)) => { + Some(self.interner.get_type_alias(type_alias_id)) + } + _ => None, + } } } diff --git a/compiler/noirc_frontend/src/elaborator/statements.rs b/compiler/noirc_frontend/src/elaborator/statements.rs index 238160e5aa4..757def16a93 100644 --- a/compiler/noirc_frontend/src/elaborator/statements.rs +++ b/compiler/noirc_frontend/src/elaborator/statements.rs @@ -293,7 +293,8 @@ impl<'context> Elaborator<'context> { let mut mutable = true; let span = ident.span(); let path = Path::from_single(ident.0.contents, span); - let (ident, scope_index) = self.get_ident_from_path(path); + let ((ident, scope_index), _) = self.get_ident_from_path(path); + self.resolve_local_variable(ident.clone(), scope_index); let typ = if ident.id == DefinitionId::dummy_id() { diff --git a/compiler/noirc_frontend/src/elaborator/types.rs b/compiler/noirc_frontend/src/elaborator/types.rs index 8ffbd15bdab..2879204d3ee 100644 --- a/compiler/noirc_frontend/src/elaborator/types.rs +++ b/compiler/noirc_frontend/src/elaborator/types.rs @@ -14,8 +14,10 @@ use crate::{ hir::{ comptime::{Interpreter, Value}, def_collector::dc_crate::CompilationError, - def_map::ModuleDefId, - resolution::{errors::ResolverError, import::PathResolutionError}, + resolution::{ + errors::ResolverError, + import::{PathResolutionError, PathResolutionItem}, + }, type_check::{ generics::{Generic, TraitGenerics}, NoMatchingImplFoundError, Source, TypeCheckError, @@ -46,7 +48,7 @@ pub const WILDCARD_TYPE: &str = "_"; pub(super) struct TraitPathResolution { pub(super) method: TraitMethod, - pub(super) error: Option, + pub(super) errors: Vec, } impl<'context> Elaborator<'context> { @@ -404,7 +406,7 @@ impl<'context> Elaborator<'context> { // If we cannot find a local generic of the same name, try to look up a global match self.resolve_path_or_error(path.clone()) { - Ok(ModuleDefId::GlobalId(id)) => { + Ok(PathResolutionItem::Global(id)) => { if let Some(current_item) = self.current_item { self.interner.add_global_dependency(current_item, id); } @@ -551,7 +553,7 @@ impl<'context> Elaborator<'context> { let constraint = the_trait.as_constraint(path.span); return Some(TraitPathResolution { method: TraitMethod { method_id: method, constraint, assumed: true }, - error: None, + errors: Vec::new(), }); } } @@ -564,15 +566,14 @@ impl<'context> Elaborator<'context> { // E.g. `t.method()` with `where T: Foo` in scope will return `(Foo::method, T, vec![Bar])` fn resolve_trait_static_method(&mut self, path: &Path) -> Option { let path_resolution = self.resolve_path(path.clone()).ok()?; - let ModuleDefId::FunctionId(func_id) = path_resolution.module_def_id else { return None }; - + let func_id = path_resolution.item.function_id()?; let meta = self.interner.function_meta(&func_id); let the_trait = self.interner.get_trait(meta.trait_id?); let method = the_trait.find_method(path.last_name())?; let constraint = the_trait.as_constraint(path.span); Some(TraitPathResolution { method: TraitMethod { method_id: method, constraint, assumed: false }, - error: path_resolution.error, + errors: path_resolution.errors, }) } @@ -600,7 +601,7 @@ impl<'context> Elaborator<'context> { if let Some(method) = the_trait.find_method(path.last_name()) { return Some(TraitPathResolution { method: TraitMethod { method_id: method, constraint, assumed: true }, - error: None, + errors: Vec::new(), }); } } @@ -1826,19 +1827,6 @@ impl<'context> Elaborator<'context> { context.trait_constraints.push((constraint, expr_id)); } - pub fn check_unsupported_turbofish_usage(&mut self, path: &Path, exclude_last_segment: bool) { - for (index, segment) in path.segments.iter().enumerate() { - if exclude_last_segment && index == path.segments.len() - 1 { - continue; - } - - if segment.generics.is_some() { - let span = segment.turbofish_span(); - self.push_err(TypeCheckError::UnsupportedTurbofishUsage { span }); - } - } - } - pub fn bind_generics_from_trait_constraint( &self, constraint: &TraitConstraint, diff --git a/compiler/noirc_frontend/src/hir/def_collector/dc_crate.rs b/compiler/noirc_frontend/src/hir/def_collector/dc_crate.rs index 658812be324..20c162fbd3a 100644 --- a/compiler/noirc_frontend/src/hir/def_collector/dc_crate.rs +++ b/compiler/noirc_frontend/src/hir/def_collector/dc_crate.rs @@ -385,9 +385,8 @@ impl DefCollector { let current_def_map = context.def_maps.get_mut(&crate_id).unwrap(); let file_id = current_def_map.file_id(module_id); - let has_path_resolution_error = resolved_import.error.is_some(); - - if let Some(error) = resolved_import.error { + let has_path_resolution_error = !resolved_import.errors.is_empty(); + for error in resolved_import.errors { errors.push(( DefCollectorErrorKind::PathResolutionError(error).into(), file_id, @@ -557,7 +556,7 @@ fn inject_prelude( span: Span::default(), }; - if let Ok(PathResolution { module_def_id, error }) = path_resolver::resolve_path( + if let Ok(PathResolution { item, errors }) = path_resolver::resolve_path( &context.def_maps, ModuleId { krate: crate_id, local_id: crate_root }, None, @@ -565,8 +564,8 @@ fn inject_prelude( &mut context.def_interner.usage_tracker, &mut None, ) { - assert!(error.is_none(), "Tried to add private item to prelude"); - let module_id = module_def_id.as_module().expect("std::prelude should be a module"); + assert!(errors.is_empty(), "Tried to add private item to prelude"); + let module_id = item.module_id().expect("std::prelude should be a module"); let prelude = context.module(module_id).scope().names(); for path in prelude { diff --git a/compiler/noirc_frontend/src/hir/def_map/module_def.rs b/compiler/noirc_frontend/src/hir/def_map/module_def.rs index a487bda81b3..a751eacd2dd 100644 --- a/compiler/noirc_frontend/src/hir/def_map/module_def.rs +++ b/compiler/noirc_frontend/src/hir/def_map/module_def.rs @@ -99,79 +99,3 @@ impl From for ModuleDefId { ModuleDefId::TraitId(trait_id) } } - -pub trait TryFromModuleDefId: Sized { - fn try_from(id: ModuleDefId) -> Option; - fn dummy_id() -> Self; - fn description() -> String; -} - -impl TryFromModuleDefId for FuncId { - fn try_from(id: ModuleDefId) -> Option { - id.as_function() - } - - fn dummy_id() -> Self { - FuncId::dummy_id() - } - - fn description() -> String { - "function".to_string() - } -} - -impl TryFromModuleDefId for StructId { - fn try_from(id: ModuleDefId) -> Option { - id.as_type() - } - - fn dummy_id() -> Self { - StructId::dummy_id() - } - - fn description() -> String { - "type".to_string() - } -} - -impl TryFromModuleDefId for TypeAliasId { - fn try_from(id: ModuleDefId) -> Option { - id.as_type_alias() - } - - fn dummy_id() -> Self { - TypeAliasId::dummy_id() - } - - fn description() -> String { - "type alias".to_string() - } -} - -impl TryFromModuleDefId for TraitId { - fn try_from(id: ModuleDefId) -> Option { - id.as_trait() - } - - fn dummy_id() -> Self { - TraitId::dummy_id() - } - - fn description() -> String { - "trait".to_string() - } -} - -impl TryFromModuleDefId for GlobalId { - fn try_from(id: ModuleDefId) -> Option { - id.as_global() - } - - fn dummy_id() -> Self { - GlobalId::dummy_id() - } - - fn description() -> String { - "global".to_string() - } -} diff --git a/compiler/noirc_frontend/src/hir/def_map/namespace.rs b/compiler/noirc_frontend/src/hir/def_map/namespace.rs index 6fac6d2b991..a600d98dd8b 100644 --- a/compiler/noirc_frontend/src/hir/def_map/namespace.rs +++ b/compiler/noirc_frontend/src/hir/def_map/namespace.rs @@ -13,14 +13,6 @@ impl PerNs { PerNs { types: Some((t, ItemVisibility::Public, false)), values: None } } - pub fn take_types(self) -> Option { - self.types.map(|it| it.0) - } - - pub fn take_values(self) -> Option { - self.values.map(|it| it.0) - } - pub fn iter_defs(self) -> impl Iterator { self.types.map(|it| it.0).into_iter().chain(self.values.map(|it| it.0)) } diff --git a/compiler/noirc_frontend/src/hir/resolution/errors.rs b/compiler/noirc_frontend/src/hir/resolution/errors.rs index 3c4022b58bb..e1e60daff60 100644 --- a/compiler/noirc_frontend/src/hir/resolution/errors.rs +++ b/compiler/noirc_frontend/src/hir/resolution/errors.rs @@ -38,7 +38,7 @@ pub enum ResolverError { #[error("could not resolve path")] PathResolutionError(#[from] PathResolutionError), #[error("Expected")] - Expected { span: Span, expected: String, got: String }, + Expected { span: Span, expected: &'static str, got: &'static str }, #[error("Duplicate field in constructor")] DuplicateField { field: Ident }, #[error("No such field in struct")] diff --git a/compiler/noirc_frontend/src/hir/resolution/import.rs b/compiler/noirc_frontend/src/hir/resolution/import.rs index 93039b1ea7f..58a3a841801 100644 --- a/compiler/noirc_frontend/src/hir/resolution/import.rs +++ b/compiler/noirc_frontend/src/hir/resolution/import.rs @@ -3,12 +3,13 @@ use thiserror::Error; use crate::graph::CrateId; use crate::hir::def_collector::dc_crate::CompilationError; -use crate::node_interner::ReferenceId; + +use crate::node_interner::{FuncId, GlobalId, ReferenceId, StructId, TraitId, TypeAliasId}; use crate::usage_tracker::UsageTracker; use std::collections::BTreeMap; -use crate::ast::{Ident, ItemVisibility, Path, PathKind, PathSegment}; +use crate::ast::{Ident, ItemVisibility, Path, PathKind, PathSegment, UnresolvedType}; use crate::hir::def_map::{CrateDefMap, LocalModuleId, ModuleDefId, ModuleId, PerNs}; use super::errors::ResolverError; @@ -26,16 +27,80 @@ pub struct ImportDirective { struct NamespaceResolution { module_id: ModuleId, + item: PathResolutionItem, namespace: PerNs, - error: Option, + errors: Vec, } type NamespaceResolutionResult = Result; +#[derive(Debug)] pub struct PathResolution { - pub module_def_id: ModuleDefId, + pub item: PathResolutionItem, + pub errors: Vec, +} - pub error: Option, +/// All possible items that result from resolving a Path. +/// Note that this item doesn't include the last turbofish in a Path, +/// only intermediate ones, if any. +#[derive(Debug, Clone)] +pub enum PathResolutionItem { + Module(ModuleId), + Struct(StructId), + TypeAlias(TypeAliasId), + Trait(TraitId), + Global(GlobalId), + ModuleFunction(FuncId), + StructFunction(StructId, Option, FuncId), + TypeAliasFunction(TypeAliasId, Option, FuncId), + TraitFunction(TraitId, Option, FuncId), +} + +impl PathResolutionItem { + pub fn function_id(&self) -> Option { + match self { + PathResolutionItem::ModuleFunction(func_id) + | PathResolutionItem::StructFunction(_, _, func_id) + | PathResolutionItem::TypeAliasFunction(_, _, func_id) + | PathResolutionItem::TraitFunction(_, _, func_id) => Some(*func_id), + _ => None, + } + } + + pub fn module_id(&self) -> Option { + match self { + Self::Module(module_id) => Some(*module_id), + _ => None, + } + } + + pub fn description(&self) -> &'static str { + match self { + PathResolutionItem::Module(..) => "module", + PathResolutionItem::Struct(..) => "type", + PathResolutionItem::TypeAlias(..) => "type alias", + PathResolutionItem::Trait(..) => "trait", + PathResolutionItem::Global(..) => "global", + PathResolutionItem::ModuleFunction(..) + | PathResolutionItem::StructFunction(..) + | PathResolutionItem::TypeAliasFunction(..) + | PathResolutionItem::TraitFunction(..) => "function", + } + } +} + +#[derive(Debug, Clone)] +pub struct Turbofish { + pub generics: Vec, + pub span: Span, +} + +/// Any item that can appear before the last segment in a path. +#[derive(Debug)] +enum IntermediatePathResolutionItem { + Module(ModuleId), + Struct(StructId, Option), + Trait(TraitId, Option), } pub(crate) type PathResolutionResult = Result; @@ -48,6 +113,8 @@ pub enum PathResolutionError { Private(Ident), #[error("There is no super module")] NoSuper(Span), + #[error("turbofish (`::<_>`) not allowed on {item}")] + TurbofishNotAllowedOnItem { item: String, span: Span }, } #[derive(Debug)] @@ -56,10 +123,12 @@ pub struct ResolvedImport { pub name: Ident, // The symbol which we have resolved to pub resolved_namespace: PerNs, + // The item which we have resolved to + pub item: PathResolutionItem, // The module which we must add the resolved namespace to pub module_scope: LocalModuleId, pub is_prelude: bool, - pub error: Option, + pub errors: Vec, } impl From for CompilationError { @@ -83,6 +152,9 @@ impl<'a> From<&'a PathResolutionError> for CustomDiagnostic { PathResolutionError::NoSuper(span) => { CustomDiagnostic::simple_error(error.to_string(), String::new(), *span) } + PathResolutionError::TurbofishNotAllowedOnItem { item: _, span } => { + CustomDiagnostic::simple_error(error.to_string(), String::new(), *span) + } } } } @@ -95,15 +167,19 @@ pub fn resolve_import( path_references: &mut Option<&mut Vec>, ) -> Result { let module_scope = import_directive.module_id; - let NamespaceResolution { module_id: resolved_module, namespace: resolved_namespace, error } = - resolve_path_to_ns( - import_directive, - crate_id, - crate_id, - def_maps, - usage_tracker, - path_references, - )?; + let NamespaceResolution { + module_id: resolved_module, + item, + namespace: resolved_namespace, + mut errors, + } = resolve_path_to_ns( + import_directive, + crate_id, + crate_id, + def_maps, + usage_tracker, + path_references, + )?; let name = resolve_path_name(import_directive); @@ -113,28 +189,25 @@ pub fn resolve_import( .map(|(_, visibility, _)| visibility) .expect("Found empty namespace"); - let error = error.or_else(|| { - if import_directive.self_type_module_id == Some(resolved_module) - || can_reference_module_id( - def_maps, - crate_id, - import_directive.module_id, - resolved_module, - visibility, - ) - { - None - } else { - Some(PathResolutionError::Private(name.clone())) - } - }); + if !(import_directive.self_type_module_id == Some(resolved_module) + || can_reference_module_id( + def_maps, + crate_id, + import_directive.module_id, + resolved_module, + visibility, + )) + { + errors.push(PathResolutionError::Private(name.clone())); + } Ok(ResolvedImport { name, resolved_namespace, + item, module_scope, is_prelude: import_directive.is_prelude, - error, + errors, }) } @@ -275,13 +348,16 @@ fn resolve_name_in_module( let mut current_mod_id = ModuleId { krate, local_id: starting_mod }; let mut current_mod = &def_map.modules[current_mod_id.local_id.0]; + let mut intermediate_item = IntermediatePathResolutionItem::Module(current_mod_id); + // There is a possibility that the import path is empty // In that case, early return if import_path.is_empty() { return Ok(NamespaceResolution { module_id: current_mod_id, + item: PathResolutionItem::Module(current_mod_id), namespace: PerNs::types(current_mod_id.into()), - error: None, + errors: Vec::new(), }); } @@ -293,25 +369,34 @@ fn resolve_name_in_module( usage_tracker.mark_as_referenced(current_mod_id, first_segment); - let mut warning: Option = None; + let mut errors = Vec::new(); for (index, (last_segment, current_segment)) in import_path.iter().zip(import_path.iter().skip(1)).enumerate() { - let last_segment = &last_segment.ident; - let current_segment = ¤t_segment.ident; + let last_ident = &last_segment.ident; + let current_ident = ¤t_segment.ident; + let last_segment_generics = &last_segment.generics; let (typ, visibility) = match current_ns.types { - None => return Err(PathResolutionError::Unresolved(last_segment.clone())), + None => return Err(PathResolutionError::Unresolved(last_ident.clone())), Some((typ, visibility, _)) => (typ, visibility), }; // In the type namespace, only Mod can be used in a path. - current_mod_id = match typ { + (current_mod_id, intermediate_item) = match typ { ModuleDefId::ModuleId(id) => { if let Some(path_references) = path_references { path_references.push(ReferenceId::Module(id)); } - id + + if last_segment_generics.is_some() { + errors.push(PathResolutionError::TurbofishNotAllowedOnItem { + item: format!("module `{last_ident}`"), + span: last_segment.turbofish_span(), + }); + } + + (id, IntermediatePathResolutionItem::Module(id)) } ModuleDefId::FunctionId(_) => panic!("functions cannot be in the type namespace"), // TODO: If impls are ever implemented, types can be used in a path @@ -319,51 +404,75 @@ fn resolve_name_in_module( if let Some(path_references) = path_references { path_references.push(ReferenceId::Struct(id)); } - id.module_id() + + ( + id.module_id(), + IntermediatePathResolutionItem::Struct( + id, + last_segment_generics.as_ref().map(|generics| Turbofish { + generics: generics.clone(), + span: last_segment.turbofish_span(), + }), + ), + ) } ModuleDefId::TypeAliasId(_) => panic!("type aliases cannot be used in type namespace"), ModuleDefId::TraitId(id) => { if let Some(path_references) = path_references { path_references.push(ReferenceId::Trait(id)); } - id.0 + + ( + id.0, + IntermediatePathResolutionItem::Trait( + id, + last_segment_generics.as_ref().map(|generics| Turbofish { + generics: generics.clone(), + span: last_segment.turbofish_span(), + }), + ), + ) } ModuleDefId::GlobalId(_) => panic!("globals cannot be in the type namespace"), }; - warning = warning.or_else(|| { - // If the path is plain or crate, the first segment will always refer to - // something that's visible from the current module. - if (plain_or_crate && index == 0) - || can_reference_module_id( - def_maps, - importing_crate, - starting_mod, - current_mod_id, - visibility, - ) - { - None - } else { - Some(PathResolutionError::Private(last_segment.clone())) - } - }); + // If the path is plain or crate, the first segment will always refer to + // something that's visible from the current module. + if !((plain_or_crate && index == 0) + || can_reference_module_id( + def_maps, + importing_crate, + starting_mod, + current_mod_id, + visibility, + )) + { + errors.push(PathResolutionError::Private(last_ident.clone())); + } current_mod = &def_maps[¤t_mod_id.krate].modules[current_mod_id.local_id.0]; // Check if namespace - let found_ns = current_mod.find_name(current_segment); + let found_ns = current_mod.find_name(current_ident); if found_ns.is_none() { - return Err(PathResolutionError::Unresolved(current_segment.clone())); + return Err(PathResolutionError::Unresolved(current_ident.clone())); } - usage_tracker.mark_as_referenced(current_mod_id, current_segment); + usage_tracker.mark_as_referenced(current_mod_id, current_ident); current_ns = found_ns; } - Ok(NamespaceResolution { module_id: current_mod_id, namespace: current_ns, error: warning }) + let module_def_id = + current_ns.values.or(current_ns.types).map(|(id, _, _)| id).expect("Found empty namespace"); + + let item = merge_intermediate_path_resolution_item_with_module_def_id( + intermediate_item, + module_def_id, + ); + + Ok(NamespaceResolution { module_id: current_mod_id, item, namespace: current_ns, errors }) } fn resolve_path_name(import_directive: &ImportDirective) -> Ident { @@ -425,3 +534,27 @@ fn resolve_external_dep( path_references, ) } + +fn merge_intermediate_path_resolution_item_with_module_def_id( + intermediate_item: IntermediatePathResolutionItem, + module_def_id: ModuleDefId, +) -> PathResolutionItem { + match module_def_id { + ModuleDefId::ModuleId(module_id) => PathResolutionItem::Module(module_id), + ModuleDefId::TypeId(struct_id) => PathResolutionItem::Struct(struct_id), + ModuleDefId::TypeAliasId(type_alias_id) => PathResolutionItem::TypeAlias(type_alias_id), + ModuleDefId::TraitId(trait_id) => PathResolutionItem::Trait(trait_id), + ModuleDefId::GlobalId(global_id) => PathResolutionItem::Global(global_id), + ModuleDefId::FunctionId(func_id) => match intermediate_item { + IntermediatePathResolutionItem::Module(_) => { + PathResolutionItem::ModuleFunction(func_id) + } + IntermediatePathResolutionItem::Struct(struct_id, generics) => { + PathResolutionItem::StructFunction(struct_id, generics, func_id) + } + IntermediatePathResolutionItem::Trait(trait_id, generics) => { + PathResolutionItem::TraitFunction(trait_id, generics, func_id) + } + }, + } +} diff --git a/compiler/noirc_frontend/src/hir/resolution/path_resolver.rs b/compiler/noirc_frontend/src/hir/resolution/path_resolver.rs index 562366fae77..705820e9101 100644 --- a/compiler/noirc_frontend/src/hir/resolution/path_resolver.rs +++ b/compiler/noirc_frontend/src/hir/resolution/path_resolver.rs @@ -88,9 +88,5 @@ pub fn resolve_path( let resolved_import = resolve_import(module_id.krate, &import, def_maps, usage_tracker, path_references)?; - let namespace = resolved_import.resolved_namespace; - let id = - namespace.values.or(namespace.types).map(|(id, _, _)| id).expect("Found empty namespace"); - - Ok(PathResolution { module_def_id: id, error: resolved_import.error }) + Ok(PathResolution { item: resolved_import.item, errors: resolved_import.errors }) } diff --git a/compiler/noirc_frontend/src/locations.rs b/compiler/noirc_frontend/src/locations.rs index 48660142d0a..4dd699251a6 100644 --- a/compiler/noirc_frontend/src/locations.rs +++ b/compiler/noirc_frontend/src/locations.rs @@ -5,7 +5,10 @@ use rustc_hash::FxHashMap as HashMap; use crate::{ ast::{FunctionDefinition, ItemVisibility}, - hir::def_map::{ModuleDefId, ModuleId}, + hir::{ + def_map::{ModuleDefId, ModuleId}, + resolution::import::PathResolutionItem, + }, node_interner::{ DefinitionId, FuncId, GlobalId, NodeInterner, ReferenceId, StructId, TraitId, TypeAliasId, }, @@ -99,6 +102,37 @@ impl NodeInterner { }; } + pub(crate) fn add_path_resolution_kind_reference( + &mut self, + kind: PathResolutionItem, + location: Location, + is_self_type: bool, + ) { + match kind { + PathResolutionItem::Module(module_id) => { + self.add_module_reference(module_id, location); + } + PathResolutionItem::Struct(struct_id) => { + self.add_struct_reference(struct_id, location, is_self_type); + } + PathResolutionItem::TypeAlias(type_alias_id) => { + self.add_alias_reference(type_alias_id, location); + } + PathResolutionItem::Trait(trait_id) => { + self.add_trait_reference(trait_id, location, is_self_type); + } + PathResolutionItem::Global(global_id) => { + self.add_global_reference(global_id, location); + } + PathResolutionItem::ModuleFunction(func_id) + | PathResolutionItem::StructFunction(_, _, func_id) + | PathResolutionItem::TypeAliasFunction(_, _, func_id) + | PathResolutionItem::TraitFunction(_, _, func_id) => { + self.add_function_reference(func_id, location); + } + } + } + pub(crate) fn add_module_reference(&mut self, id: ModuleId, location: Location) { self.add_reference(ReferenceId::Module(id), location, false); } diff --git a/compiler/noirc_frontend/src/tests.rs b/compiler/noirc_frontend/src/tests.rs index 56c4b5e9a12..5ce3ec6686c 100644 --- a/compiler/noirc_frontend/src/tests.rs +++ b/compiler/noirc_frontend/src/tests.rs @@ -617,8 +617,8 @@ fn check_trait_impl_for_non_type() { for (err, _file_id) in errors { match &err { CompilationError::ResolverError(ResolverError::Expected { expected, got, .. }) => { - assert_eq!(expected, "type"); - assert_eq!(got, "function"); + assert_eq!(*expected, "type"); + assert_eq!(*got, "function"); } _ => { panic!("No other errors are expected! Found = {:?}", err); diff --git a/compiler/noirc_frontend/src/tests/turbofish.rs b/compiler/noirc_frontend/src/tests/turbofish.rs index 71e63e878e8..3ded243a280 100644 --- a/compiler/noirc_frontend/src/tests/turbofish.rs +++ b/compiler/noirc_frontend/src/tests/turbofish.rs @@ -1,4 +1,8 @@ -use crate::hir::{def_collector::dc_crate::CompilationError, type_check::TypeCheckError}; +use crate::hir::{ + def_collector::dc_crate::CompilationError, + resolution::{errors::ResolverError, import::PathResolutionError}, + type_check::TypeCheckError, +}; use super::{assert_no_errors, get_program_errors}; @@ -100,32 +104,6 @@ fn turbofish_in_constructor() { assert_eq!(expr_typ, "Field"); } -#[test] -fn turbofish_in_middle_of_variable_unsupported_yet() { - let src = r#" - struct Foo { - x: T - } - - impl Foo { - pub fn new(x: T) -> Self { - Foo { x } - } - } - - fn main() { - let _ = Foo::::new(1); - } - "#; - let errors = get_program_errors(src); - assert_eq!(errors.len(), 1); - - assert!(matches!( - errors[0].0, - CompilationError::TypeError(TypeCheckError::UnsupportedTurbofishUsage { .. }), - )); -} - #[test] fn turbofish_in_struct_pattern() { let src = r#" @@ -214,3 +192,80 @@ fn numeric_turbofish() { "#; assert_no_errors(src); } + +#[test] +fn errors_if_turbofish_after_module() { + let src = r#" + mod moo { + pub fn foo() {} + } + + fn main() { + moo::::foo(); + } + "#; + + let errors = get_program_errors(src); + assert_eq!(errors.len(), 1); + + let CompilationError::ResolverError(ResolverError::PathResolutionError( + PathResolutionError::TurbofishNotAllowedOnItem { item, .. }, + )) = &errors[0].0 + else { + panic!("Expected a turbofish not allowed on item error, got {:?}", errors[0].0); + }; + assert_eq!(item, "module `moo`"); +} + +#[test] +fn turbofish_in_type_before_call_does_not_error() { + let src = r#" + struct Foo { + x: T + } + + impl Foo { + fn new(x: T) -> Self { + Foo { x } + } + } + + fn main() { + let _ = Foo::::new(1); + } + "#; + assert_no_errors(src); +} + +#[test] +fn turbofish_in_type_before_call_errors() { + let src = r#" + struct Foo { + x: T + } + + impl Foo { + fn new(x: T) -> Self { + Foo { x } + } + } + + fn main() { + let _ = Foo::::new(true); + } + "#; + let errors = get_program_errors(src); + assert_eq!(errors.len(), 1); + + let CompilationError::TypeError(TypeCheckError::TypeMismatch { + expected_typ, + expr_typ, + expr_span: _, + }) = &errors[0].0 + else { + panic!("Expected a type mismatch error, got {:?}", errors[0].0); + }; + + assert_eq!(expected_typ, "i32"); + assert_eq!(expr_typ, "bool"); +} diff --git a/tooling/lsp/src/attribute_reference_finder.rs b/tooling/lsp/src/attribute_reference_finder.rs index 39e1385a6e8..22afa086303 100644 --- a/tooling/lsp/src/attribute_reference_finder.rs +++ b/tooling/lsp/src/attribute_reference_finder.rs @@ -13,7 +13,10 @@ use noirc_frontend::{ graph::CrateId, hir::{ def_map::{CrateDefMap, LocalModuleId, ModuleId}, - resolution::path_resolver::{PathResolver, StandardPathResolver}, + resolution::{ + import::PathResolutionItem, + path_resolver::{PathResolver, StandardPathResolver}, + }, }, node_interner::ReferenceId, parser::{ParsedSubModule, Parser}, @@ -22,8 +25,6 @@ use noirc_frontend::{ ParsedModule, }; -use crate::modules::module_def_id_to_reference_id; - pub(crate) struct AttributeReferenceFinder<'a> { byte_index: usize, /// The module ID in scope. This might change as we traverse the AST @@ -106,6 +107,20 @@ impl<'a> Visitor for AttributeReferenceFinder<'a> { return; }; - self.reference_id = Some(module_def_id_to_reference_id(result.module_def_id)); + self.reference_id = Some(path_resolution_item_to_reference_id(result.item)); + } +} + +fn path_resolution_item_to_reference_id(item: PathResolutionItem) -> ReferenceId { + match item { + PathResolutionItem::Module(module_id) => ReferenceId::Module(module_id), + PathResolutionItem::Struct(struct_id) => ReferenceId::Struct(struct_id), + PathResolutionItem::TypeAlias(type_alias_id) => ReferenceId::Alias(type_alias_id), + PathResolutionItem::Trait(trait_id) => ReferenceId::Trait(trait_id), + PathResolutionItem::Global(global_id) => ReferenceId::Global(global_id), + PathResolutionItem::ModuleFunction(func_id) + | PathResolutionItem::StructFunction(_, _, func_id) + | PathResolutionItem::TypeAliasFunction(_, _, func_id) + | PathResolutionItem::TraitFunction(_, _, func_id) => ReferenceId::Function(func_id), } } From 07c9322332e147c0e8fade5e238552ecbf3e7849 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Tue, 29 Oct 2024 15:35:06 -0300 Subject: [PATCH 27/30] fix: make keccak256 work with input lengths greater than 136 bytes (#6393) --- noir_stdlib/src/hash/keccak.nr | 22 +++++++++++++++++++--- noir_stdlib/src/hash/sha256.nr | 4 ++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/noir_stdlib/src/hash/keccak.nr b/noir_stdlib/src/hash/keccak.nr index 50fbab8a416..cb412bac9c6 100644 --- a/noir_stdlib/src/hash/keccak.nr +++ b/noir_stdlib/src/hash/keccak.nr @@ -12,7 +12,10 @@ fn keccakf1600(input: [u64; 25]) -> [u64; 25] {} #[no_predicates] pub(crate) fn keccak256(input: [u8; N], message_size: u32) -> [u8; 32] { assert(N >= message_size); - let mut block_bytes = [0; BLOCK_SIZE_IN_BYTES]; + + // Copy input to block bytes. For that we'll need at least input bytes (N) + // but we want it to be padded to a multiple of BLOCK_SIZE_IN_BYTES. + let mut block_bytes = [0; ((N / BLOCK_SIZE_IN_BYTES) + 1) * BLOCK_SIZE_IN_BYTES]; if is_unconstrained() { for i in 0..message_size { block_bytes[i] = input[i]; @@ -114,8 +117,7 @@ mod tests { #[test] fn hash_hello_world() { - // "hello world" - let input = [72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33]; + let input = "Hello world!".as_bytes(); let result = [ 0xec, 0xd0, 0xe1, 0x8, 0xa9, 0x8e, 0x19, 0x2a, 0xf1, 0xd2, 0xc2, 0x50, 0x55, 0xf4, 0xe3, 0xbe, 0xd7, 0x84, 0xb5, 0xc8, 0x77, 0x20, 0x4e, 0x73, 0x21, 0x9a, 0x52, 0x3, 0x25, 0x1f, @@ -137,4 +139,18 @@ mod tests { ]; assert_eq(keccak256(input, 13), result); } + + #[test] + fn hash_longer_than_136_bytes() { + let input = "123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789" + .as_bytes(); + assert(input.len() > 136); + + let result = [ + 0x1d, 0xca, 0xeb, 0xdf, 0xd9, 0xd6, 0x24, 0x67, 0x1c, 0x18, 0x16, 0xda, 0xd, 0x8a, 0xeb, + 0xa8, 0x75, 0x71, 0x2c, 0xc, 0x89, 0xe0, 0x25, 0x2, 0xe8, 0xb6, 0x5e, 0x16, 0x5, 0x55, + 0xe4, 0x40, + ]; + assert_eq(keccak256(input, input.len()), result); + } } diff --git a/noir_stdlib/src/hash/sha256.nr b/noir_stdlib/src/hash/sha256.nr index a3ac0b9e5da..d3ab1b7f1b8 100644 --- a/noir_stdlib/src/hash/sha256.nr +++ b/noir_stdlib/src/hash/sha256.nr @@ -519,10 +519,10 @@ fn hash_final_block(msg_block: MSG_BLOCK, mut state: STATE) -> HASH { mod tests { use super::{ - attach_len_to_msg_block, build_msg_block, byte_into_item, get_item_byte, lshift8, make_item, + attach_len_to_msg_block, build_msg_block, byte_into_item, get_item_byte, make_item, set_item_byte_then_zeros, set_item_zeros, }; - use super::{INT_BLOCK, INT_BLOCK_SIZE, MSG_BLOCK}; + use super::INT_BLOCK; use super::sha256_var; #[test] From 075c3d32481314d900cbdea0d277de83444747ab Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Tue, 29 Oct 2024 16:48:57 -0300 Subject: [PATCH 28/30] chore: use array instead of Vec in keccak256 (#6395) --- noir_stdlib/src/hash/keccak.nr | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/noir_stdlib/src/hash/keccak.nr b/noir_stdlib/src/hash/keccak.nr index cb412bac9c6..93b366d1ec1 100644 --- a/noir_stdlib/src/hash/keccak.nr +++ b/noir_stdlib/src/hash/keccak.nr @@ -1,4 +1,3 @@ -use crate::collections::vec::Vec; use crate::runtime::is_unconstrained; global BLOCK_SIZE_IN_BYTES: u32 = 136; //(1600 - BITS * 2) / WORD_SIZE; @@ -31,7 +30,6 @@ pub(crate) fn keccak256(input: [u8; N], message_size: u32) -> [u8; 3 //1. format_input_lanes let max_blocks = (N + BLOCK_SIZE_IN_BYTES) / BLOCK_SIZE_IN_BYTES; //maximum number of bytes to hash - let max_blocks_length = (BLOCK_SIZE_IN_BYTES * max_blocks); let real_max_blocks = (message_size + BLOCK_SIZE_IN_BYTES) / BLOCK_SIZE_IN_BYTES; let real_blocks_bytes = real_max_blocks * BLOCK_SIZE_IN_BYTES; @@ -39,9 +37,9 @@ pub(crate) fn keccak256(input: [u8; N], message_size: u32) -> [u8; 3 block_bytes[real_blocks_bytes - 1] = 0x80; // populate a vector of 64-bit limbs from our byte array - let num_limbs = max_blocks_length / WORD_SIZE; - let mut sliced_buffer = Vec::new(); - for i in 0..num_limbs { + let mut sliced_buffer = + [0; (((N / BLOCK_SIZE_IN_BYTES) + 1) * BLOCK_SIZE_IN_BYTES) / WORD_SIZE]; + for i in 0..sliced_buffer.len() { let limb_start = WORD_SIZE * i; let mut sliced = 0; @@ -51,7 +49,7 @@ pub(crate) fn keccak256(input: [u8; N], message_size: u32) -> [u8; 3 v *= 256; } - sliced_buffer.push(sliced as u64); + sliced_buffer[i] = sliced as u64; } //2. sponge_absorb @@ -62,11 +60,11 @@ pub(crate) fn keccak256(input: [u8; N], message_size: u32) -> [u8; 3 for i in 0..real_max_blocks { if (i == 0) { for j in 0..LIMBS_PER_BLOCK { - state[j] = sliced_buffer.get(j); + state[j] = sliced_buffer[j]; } } else { for j in 0..LIMBS_PER_BLOCK { - state[j] = state[j] ^ sliced_buffer.get(i * LIMBS_PER_BLOCK + j); + state[j] = state[j] ^ sliced_buffer[i * LIMBS_PER_BLOCK + j]; } } state = keccakf1600(state); @@ -76,13 +74,13 @@ pub(crate) fn keccak256(input: [u8; N], message_size: u32) -> [u8; 3 // We peel out the first block as to avoid a conditional inside of the loop. // Otherwise, a dynamic predicate can cause a blowup in a constrained runtime. for j in 0..LIMBS_PER_BLOCK { - state[j] = sliced_buffer.get(j); + state[j] = sliced_buffer[j]; } state = keccakf1600(state); for i in 1..max_blocks { if i < real_max_blocks { for j in 0..LIMBS_PER_BLOCK { - state[j] = state[j] ^ sliced_buffer.get(i * LIMBS_PER_BLOCK + j); + state[j] = state[j] ^ sliced_buffer[i * LIMBS_PER_BLOCK + j]; } state = keccakf1600(state); } From 713ff2250345ef5d9c33715bcea967beba382c93 Mon Sep 17 00:00:00 2001 From: Michael J Klein Date: Wed, 30 Oct 2024 07:01:08 -0400 Subject: [PATCH 29/30] chore: add regression tests for #6314 (#6381) Co-authored-by: Tom French <15848336+TomAFrench@users.noreply.github.com> Co-authored-by: Tom French --- compiler/noirc_frontend/src/tests/traits.rs | 58 +++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/compiler/noirc_frontend/src/tests/traits.rs b/compiler/noirc_frontend/src/tests/traits.rs index dd6430a94cc..2b5d6c1c8eb 100644 --- a/compiler/noirc_frontend/src/tests/traits.rs +++ b/compiler/noirc_frontend/src/tests/traits.rs @@ -262,6 +262,64 @@ fn errors_if_impl_trait_constraint_is_not_satisfied() { assert_eq!(impl_trait, "Foo"); } +#[test] +// Regression test for https://github.com/noir-lang/noir/issues/6314 +// Baz inherits from a single trait: Foo +fn regression_6314_single_inheritance() { + let src = r#" + trait Foo { + fn foo(self) -> Self; + } + + trait Baz: Foo {} + + impl Baz for T where T: Foo {} + + fn main() { } + "#; + assert_no_errors(src); +} + +#[test] +// Regression test for https://github.com/noir-lang/noir/issues/6314 +// Baz inherits from two traits: Foo and Bar +fn regression_6314_double_inheritance() { + let src = r#" + trait Foo { + fn foo(self) -> Self; + } + + trait Bar { + fn bar(self) -> Self; + } + + trait Baz: Foo + Bar {} + + impl Baz for T where T: Foo + Bar {} + + fn baz(x: T) -> T where T: Baz { + x.foo().bar() + } + + impl Foo for Field { + fn foo(self) -> Self { + self + 1 + } + } + + impl Bar for Field { + fn bar(self) -> Self { + self + 2 + } + } + + fn main() { + assert(0.foo().bar() == baz(0)); + }"#; + + assert_no_errors(src); +} + #[test] fn removes_assumed_parent_traits_after_function_ends() { let src = r#" From 4e44e1f4a71a1a8efc17462c55a85b9cb7fdeae3 Mon Sep 17 00:00:00 2001 From: Tom French <15848336+TomAFrench@users.noreply.github.com> Date: Wed, 30 Oct 2024 13:05:05 +0000 Subject: [PATCH 30/30] chore: add regression tests for #4372 (#6401) --- compiler/noirc_frontend/src/tests.rs | 26 ++++++++++++++------ compiler/noirc_frontend/src/tests/aliases.rs | 16 ++++++++++++ 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/compiler/noirc_frontend/src/tests.rs b/compiler/noirc_frontend/src/tests.rs index 5ce3ec6686c..96a72961ca3 100644 --- a/compiler/noirc_frontend/src/tests.rs +++ b/compiler/noirc_frontend/src/tests.rs @@ -3548,17 +3548,27 @@ fn uses_self_in_import() { } #[test] -fn alias_in_let_pattern() { +fn does_not_error_on_return_values_after_block_expression() { + // Regression test for https://github.com/noir-lang/noir/issues/4372 let src = r#" - struct Foo { x: T } - - type Bar = Foo; + fn case1() -> [Field] { + if true { + } + &[1] + } - fn main() { - let Bar { x } = Foo { x: [0] }; - // This is just to show the compiler knows this is an array. - let _: [Field; 1] = x; + fn case2() -> [u8] { + let mut var: u8 = 1; + { + var += 1; } + &[var] + } + + fn main() { + let _ = case1(); + let _ = case2(); + } "#; assert_no_errors(src); } diff --git a/compiler/noirc_frontend/src/tests/aliases.rs b/compiler/noirc_frontend/src/tests/aliases.rs index 5239abcb366..8d3433299f6 100644 --- a/compiler/noirc_frontend/src/tests/aliases.rs +++ b/compiler/noirc_frontend/src/tests/aliases.rs @@ -31,3 +31,19 @@ fn allows_usage_of_type_alias_as_return_type() { "#; assert_no_errors(src); } + +#[test] +fn alias_in_let_pattern() { + let src = r#" + struct Foo { x: T } + + type Bar = Foo; + + fn main() { + let Bar { x } = Foo { x: [0] }; + // This is just to show the compiler knows this is an array. + let _: [Field; 1] = x; + } + "#; + assert_no_errors(src); +}