From 91042c7bcebfebeb4e629162f44988e2cda1ed41 Mon Sep 17 00:00:00 2001 From: Aztec Bot <49558828+AztecBot@users.noreply.github.com> Date: Tue, 13 Aug 2024 17:01:10 -0400 Subject: [PATCH] feat: Sync from noir (#7945) Automated pull of development from the [noir](https://github.com/noir-lang/noir) programming language, a dependency of Aztec. BEGIN_COMMIT_OVERRIDE chore: allow passing custom executors to fuzzer (https://github.com/noir-lang/noir/pull/5710) feat: LSP path completion (https://github.com/noir-lang/noir/pull/5712) fix(debugger): Update the debugger to handle the new Brillig debug metadata format (https://github.com/noir-lang/noir/pull/5706) feat: add `Quoted::as_expr` and `Expr::as_function_call` (https://github.com/noir-lang/noir/pull/5708) feat: LSP autocompletion for use statement (https://github.com/noir-lang/noir/pull/5704) feat: add `Type::implements` (https://github.com/noir-lang/noir/pull/5701) END_COMMIT_OVERRIDE --------- Co-authored-by: Maxim Vezenov --- .noir-sync-commit | 2 +- noir/noir-repo/Cargo.lock | 6 +- .../acvm-repo/acvm/src/pwg/brillig.rs | 2 +- .../compiler/noirc_driver/src/debug.rs | 17 +- .../compiler/noirc_frontend/Cargo.toml | 4 +- .../src/hir/comptime/interpreter/builtin.rs | 74 +- .../interpreter/builtin/builtin_helpers.rs | 13 +- .../noirc_frontend/src/hir/comptime/value.rs | 7 +- .../src/hir/def_map/module_data.rs | 6 +- .../noirc_frontend/src/lexer/token.rs | 3 +- .../noirc_frontend/src/parser/errors.rs | 2 + .../noirc_frontend/src/parser/parser/path.rs | 30 +- noir/noir-repo/cspell.json | 1 + noir/noir-repo/noir_stdlib/src/meta/expr.nr | 6 + noir/noir-repo/noir_stdlib/src/meta/mod.nr | 1 + noir/noir-repo/noir_stdlib/src/meta/quoted.nr | 3 + noir/noir-repo/noir_stdlib/src/meta/typ.nr | 3 + .../comptime_exp/Nargo.toml | 7 + .../comptime_exp/src/main.nr | 8 + .../comptime_type/src/main.nr | 35 + .../noir-repo/tooling/debugger/src/context.rs | 264 +- noir/noir-repo/tooling/debugger/src/repl.rs | 35 +- noir/noir-repo/tooling/fuzzer/Cargo.toml | 1 - noir/noir-repo/tooling/fuzzer/src/lib.rs | 36 +- noir/noir-repo/tooling/lsp/Cargo.toml | 1 + noir/noir-repo/tooling/lsp/src/lib.rs | 23 +- .../tooling/lsp/src/notifications/mod.rs | 6 +- .../tooling/lsp/src/requests/completion.rs | 2204 +++++++++++++++++ .../lsp/src/requests/completion/builtins.rs | 127 + .../tooling/lsp/src/requests/hover.rs | 4 +- .../tooling/lsp/src/requests/inlay_hint.rs | 62 +- .../noir-repo/tooling/lsp/src/requests/mod.rs | 40 +- noir/noir-repo/tooling/lsp/src/types.rs | 9 +- noir/noir-repo/tooling/lsp/src/utils.rs | 59 + noir/noir-repo/tooling/nargo/Cargo.toml | 4 + noir/noir-repo/tooling/nargo/src/ops/test.rs | 96 +- noir/noir-repo/tooling/nargo_cli/Cargo.toml | 2 - .../tooling/nargo_cli/src/cli/test_cmd.rs | 56 +- .../tooling/nargo_cli/tests/stdlib-tests.rs | 60 +- .../tooling/noirc_artifacts/src/debug.rs | 17 +- 40 files changed, 3019 insertions(+), 317 deletions(-) create mode 100644 noir/noir-repo/noir_stdlib/src/meta/expr.nr create mode 100644 noir/noir-repo/test_programs/compile_success_empty/comptime_exp/Nargo.toml create mode 100644 noir/noir-repo/test_programs/compile_success_empty/comptime_exp/src/main.nr create mode 100644 noir/noir-repo/tooling/lsp/src/requests/completion.rs create mode 100644 noir/noir-repo/tooling/lsp/src/requests/completion/builtins.rs create mode 100644 noir/noir-repo/tooling/lsp/src/utils.rs diff --git a/.noir-sync-commit b/.noir-sync-commit index 93b78bbfbf9..d575ffe3e8c 100644 --- a/.noir-sync-commit +++ b/.noir-sync-commit @@ -1 +1 @@ -e4f7dbe63b55807b3ff0b4d6f47a8b7f847299fb +0ebf1fee471641db0bffcc8307d20327613c78c1 diff --git a/noir/noir-repo/Cargo.lock b/noir/noir-repo/Cargo.lock index 47b63ff2f4f..bacbf939786 100644 --- a/noir/noir-repo/Cargo.lock +++ b/noir/noir-repo/Cargo.lock @@ -2508,11 +2508,13 @@ dependencies = [ "jsonrpc-core-client", "jsonrpc-derive", "jsonrpc-http-server", + "noir_fuzzer", "noirc_abi", "noirc_driver", "noirc_errors", "noirc_frontend", "noirc_printable_type", + "proptest", "rand 0.8.5", "rayon", "serde", @@ -2545,7 +2547,6 @@ dependencies = [ "nargo_fmt", "nargo_toml", "noir_debugger", - "noir_fuzzer", "noir_lsp", "noirc_abi", "noirc_artifacts", @@ -2558,7 +2559,6 @@ dependencies = [ "pprof 0.13.0", "predicates 2.1.5", "prettytable-rs", - "proptest", "rayon", "serde", "serde_json", @@ -2697,7 +2697,6 @@ name = "noir_fuzzer" version = "0.33.0" dependencies = [ "acvm", - "nargo", "noirc_abi", "noirc_artifacts", "proptest", @@ -2737,6 +2736,7 @@ dependencies = [ "serde", "serde_json", "serde_with", + "strum", "thiserror", "tokio", "tower", diff --git a/noir/noir-repo/acvm-repo/acvm/src/pwg/brillig.rs b/noir/noir-repo/acvm-repo/acvm/src/pwg/brillig.rs index c12629b0543..5ec3224dbaa 100644 --- a/noir/noir-repo/acvm-repo/acvm/src/pwg/brillig.rs +++ b/noir/noir-repo/acvm-repo/acvm/src/pwg/brillig.rs @@ -32,7 +32,7 @@ pub struct BrilligSolver<'b, F, B: BlackBoxFunctionSolver> { /// This id references which Brillig function within the main ACIR program we are solving. /// This is used for appropriately resolving errors as the ACIR program artifacts /// set up their Brillig debug metadata by function id. - function_id: BrilligFunctionId, + pub function_id: BrilligFunctionId, } impl<'b, B: BlackBoxFunctionSolver, F: AcirField> BrilligSolver<'b, F, B> { diff --git a/noir/noir-repo/compiler/noirc_driver/src/debug.rs b/noir/noir-repo/compiler/noirc_driver/src/debug.rs index 5e309398cc5..f5eaede89b2 100644 --- a/noir/noir-repo/compiler/noirc_driver/src/debug.rs +++ b/noir/noir-repo/compiler/noirc_driver/src/debug.rs @@ -18,7 +18,7 @@ pub(crate) fn filter_relevant_files( debug_symbols: &[DebugInfo], file_manager: &FileManager, ) -> BTreeMap { - let files_with_debug_symbols: BTreeSet = debug_symbols + let mut files_with_debug_symbols: BTreeSet = debug_symbols .iter() .flat_map(|function_symbols| { function_symbols @@ -28,6 +28,21 @@ pub(crate) fn filter_relevant_files( }) .collect(); + let files_with_brillig_debug_symbols: BTreeSet = debug_symbols + .iter() + .flat_map(|function_symbols| { + let brillig_location_maps = + function_symbols.brillig_locations.values().flat_map(|brillig_location_map| { + brillig_location_map + .values() + .flat_map(|call_stack| call_stack.iter().map(|location| location.file)) + }); + brillig_location_maps + }) + .collect(); + + files_with_debug_symbols.extend(files_with_brillig_debug_symbols); + let mut file_map = BTreeMap::new(); for file_id in files_with_debug_symbols { diff --git a/noir/noir-repo/compiler/noirc_frontend/Cargo.toml b/noir/noir-repo/compiler/noirc_frontend/Cargo.toml index f7439a09204..7ef8870eaa8 100644 --- a/noir/noir-repo/compiler/noirc_frontend/Cargo.toml +++ b/noir/noir-repo/compiler/noirc_frontend/Cargo.toml @@ -32,12 +32,12 @@ tracing.workspace = true petgraph = "0.6" rangemap = "1.4.0" lalrpop-util = { version = "0.20.2", features = ["lexer"] } +strum = "0.24" +strum_macros = "0.24" [dev-dependencies] base64.workspace = true -strum = "0.24" -strum_macros = "0.24" [build-dependencies] lalrpop = "0.20.2" diff --git a/noir/noir-repo/compiler/noirc_frontend/src/hir/comptime/interpreter/builtin.rs b/noir/noir-repo/compiler/noirc_frontend/src/hir/comptime/interpreter/builtin.rs index 1eb5f211cce..ef7b9f2be55 100644 --- a/noir/noir-repo/compiler/noirc_frontend/src/hir/comptime/interpreter/builtin.rs +++ b/noir/noir-repo/compiler/noirc_frontend/src/hir/comptime/interpreter/builtin.rs @@ -6,7 +6,7 @@ use std::{ use acvm::{AcirField, FieldElement}; use builtin_helpers::{ check_argument_count, check_function_not_yet_resolved, check_one_argument, - check_three_arguments, check_two_arguments, get_function_def, get_module, get_quoted, + check_three_arguments, check_two_arguments, get_expr, get_function_def, get_module, get_quoted, get_slice, get_struct, get_trait_constraint, get_trait_def, get_tuple, get_type, get_u32, hir_pattern_to_tokens, mutate_func_meta_type, parse, parse_tokens, replace_func_meta_parameters, replace_func_meta_return_type, @@ -17,8 +17,8 @@ use rustc_hash::FxHashMap as HashMap; use crate::{ ast::{ - FunctionKind, FunctionReturnType, IntegerBitSize, UnresolvedType, UnresolvedTypeData, - Visibility, + ExpressionKind, FunctionKind, FunctionReturnType, IntegerBitSize, UnresolvedType, + UnresolvedTypeData, Visibility, }, hir::comptime::{errors::IResult, value::add_token_spans, InterpreterError, Value}, hir_def::function::FunctionBody, @@ -47,6 +47,7 @@ impl<'local, 'context> Interpreter<'local, 'context> { "array_as_str_unchecked" => array_as_str_unchecked(interner, arguments, location), "array_len" => array_len(interner, arguments, location), "as_slice" => as_slice(interner, arguments, location), + "expr_as_function_call" => expr_as_function_call(arguments, return_type, location), "is_unconstrained" => Ok(Value::Bool(true)), "function_def_name" => function_def_name(interner, arguments, location), "function_def_parameters" => function_def_parameters(interner, arguments, location), @@ -64,6 +65,7 @@ impl<'local, 'context> Interpreter<'local, 'context> { "modulus_le_bits" => modulus_le_bits(interner, arguments, location), "modulus_le_bytes" => modulus_le_bytes(interner, arguments, location), "modulus_num_bits" => modulus_num_bits(interner, arguments, location), + "quoted_as_expr" => quoted_as_expr(arguments, return_type, location), "quoted_as_module" => quoted_as_module(self, arguments, return_type, location), "quoted_as_trait_constraint" => quoted_as_trait_constraint(self, arguments, location), "quoted_as_type" => quoted_as_type(self, arguments, location), @@ -91,6 +93,7 @@ impl<'local, 'context> Interpreter<'local, 'context> { "type_as_struct" => type_as_struct(arguments, return_type, location), "type_as_tuple" => type_as_tuple(arguments, return_type, location), "type_eq" => type_eq(arguments, location), + "type_implements" => type_implements(interner, arguments, location), "type_is_bool" => type_is_bool(arguments, location), "type_is_field" => type_is_field(arguments, location), "type_of" => type_of(arguments, location), @@ -311,6 +314,20 @@ fn slice_insert( Ok(Value::Slice(values, typ)) } +// fn as_expr(quoted: Quoted) -> Option +fn quoted_as_expr( + arguments: Vec<(Value, Location)>, + return_type: Type, + location: Location, +) -> IResult { + let argument = check_one_argument(arguments, location)?; + + let expr = parse(argument, parser::expression(), "an expression").ok(); + let value = expr.map(|expr| Value::Expr(expr.kind)); + + option(return_type, value) +} + // fn as_module(quoted: Quoted) -> Option fn quoted_as_module( interpreter: &mut Interpreter, @@ -490,6 +507,21 @@ fn type_eq(arguments: Vec<(Value, Location)>, location: Location) -> IResult bool +fn type_implements( + interner: &NodeInterner, + arguments: Vec<(Value, Location)>, + location: Location, +) -> IResult { + let (typ, constraint) = check_two_arguments(arguments, location)?; + + let typ = get_type(typ)?; + let (trait_id, generics) = get_trait_constraint(constraint)?; + + let implements = interner.try_lookup_trait_implementation(&typ, trait_id, &generics).is_ok(); + Ok(Value::Bool(implements)) +} + // fn is_bool(self) -> bool fn type_is_bool(arguments: Vec<(Value, Location)>, location: Location) -> IResult { let value = check_one_argument(arguments, location)?; @@ -656,6 +688,42 @@ fn zeroed(return_type: Type) -> IResult { } } +// fn as_function_call(self) -> Option<(Expr, [Expr])> +fn expr_as_function_call( + arguments: Vec<(Value, Location)>, + return_type: Type, + location: Location, +) -> IResult { + expr_as(arguments, return_type, location, |expr| { + if let ExpressionKind::Call(call_expression) = expr { + let function = Value::Expr(call_expression.func.kind); + let arguments = call_expression.arguments.into_iter(); + let arguments = arguments.map(|argument| Value::Expr(argument.kind)).collect(); + let arguments = + Value::Slice(arguments, Type::Slice(Box::new(Type::Quoted(QuotedType::Expr)))); + Some(Value::Tuple(vec![function, arguments])) + } else { + None + } + }) +} + +// Helper function for implementing the `expr_as_...` functions. +fn expr_as( + arguments: Vec<(Value, Location)>, + return_type: Type, + location: Location, + f: F, +) -> IResult +where + F: FnOnce(ExpressionKind) -> Option, +{ + let self_argument = check_one_argument(arguments, location)?; + let expr = get_expr(self_argument)?; + let option_value = f(expr); + option(return_type, option_value) +} + // fn name(self) -> Quoted fn function_def_name( interner: &NodeInterner, diff --git a/noir/noir-repo/compiler/noirc_frontend/src/hir/comptime/interpreter/builtin/builtin_helpers.rs b/noir/noir-repo/compiler/noirc_frontend/src/hir/comptime/interpreter/builtin/builtin_helpers.rs index fac2913ff79..56f6c11974f 100644 --- a/noir/noir-repo/compiler/noirc_frontend/src/hir/comptime/interpreter/builtin/builtin_helpers.rs +++ b/noir/noir-repo/compiler/noirc_frontend/src/hir/comptime/interpreter/builtin/builtin_helpers.rs @@ -4,7 +4,7 @@ use acvm::FieldElement; use noirc_errors::Location; use crate::{ - ast::{IntegerBitSize, Signedness}, + ast::{ExpressionKind, IntegerBitSize, Signedness}, hir::{ comptime::{errors::IResult, value::add_token_spans, Interpreter, InterpreterError, Value}, def_map::ModuleId, @@ -145,6 +145,17 @@ pub(crate) fn get_u32((value, location): (Value, Location)) -> IResult { } } +pub(crate) fn get_expr((value, location): (Value, Location)) -> IResult { + match value { + Value::Expr(expr) => Ok(expr), + value => { + let expected = Type::Quoted(QuotedType::Expr); + let actual = value.get_type().into_owned(); + Err(InterpreterError::TypeMismatch { expected, actual, location }) + } + } +} + pub(crate) fn get_function_def((value, location): (Value, Location)) -> IResult { match value { Value::FunctionDefinition(id) => Ok(id), diff --git a/noir/noir-repo/compiler/noirc_frontend/src/hir/comptime/value.rs b/noir/noir-repo/compiler/noirc_frontend/src/hir/comptime/value.rs index 1264cd21635..d5408309e55 100644 --- a/noir/noir-repo/compiler/noirc_frontend/src/hir/comptime/value.rs +++ b/noir/noir-repo/compiler/noirc_frontend/src/hir/comptime/value.rs @@ -57,6 +57,7 @@ pub enum Value { ModuleDefinition(ModuleId), Type(Type), Zeroed(Type), + Expr(ExpressionKind), } impl Value { @@ -103,6 +104,7 @@ impl Value { Value::ModuleDefinition(_) => Type::Quoted(QuotedType::Module), Value::Type(_) => Type::Quoted(QuotedType::Type), Value::Zeroed(typ) => return Cow::Borrowed(typ), + Value::Expr(_) => Type::Quoted(QuotedType::Expr), }) } @@ -223,6 +225,7 @@ impl Value { } }; } + Value::Expr(expr) => expr, Value::Pointer(..) | Value::StructDefinition(_) | Value::TraitConstraint(..) @@ -345,7 +348,8 @@ impl Value { HirExpression::Literal(HirLiteral::Slice(HirArrayLiteral::Standard(elements))) } Value::Quoted(tokens) => HirExpression::Unquote(add_token_spans(tokens, location.span)), - Value::Pointer(..) + Value::Expr(..) + | Value::Pointer(..) | Value::StructDefinition(_) | Value::TraitConstraint(..) | Value::TraitDefinition(_) @@ -530,6 +534,7 @@ impl<'value, 'interner> Display for ValuePrinter<'value, 'interner> { Value::ModuleDefinition(_) => write!(f, "(module)"), Value::Zeroed(typ) => write!(f, "(zeroed {typ})"), Value::Type(typ) => write!(f, "{}", typ), + Value::Expr(expr) => write!(f, "{}", expr), } } } diff --git a/noir/noir-repo/compiler/noirc_frontend/src/hir/def_map/module_data.rs b/noir/noir-repo/compiler/noirc_frontend/src/hir/def_map/module_data.rs index 488ccc476d7..8a0125cfe95 100644 --- a/noir/noir-repo/compiler/noirc_frontend/src/hir/def_map/module_data.rs +++ b/noir/noir-repo/compiler/noirc_frontend/src/hir/def_map/module_data.rs @@ -38,10 +38,14 @@ impl ModuleData { } } - pub(crate) fn scope(&self) -> &ItemScope { + pub fn scope(&self) -> &ItemScope { &self.scope } + pub fn definitions(&self) -> &ItemScope { + &self.definitions + } + fn declare( &mut self, name: Ident, diff --git a/noir/noir-repo/compiler/noirc_frontend/src/lexer/token.rs b/noir/noir-repo/compiler/noirc_frontend/src/lexer/token.rs index 2284991bbc0..4222d2b585f 100644 --- a/noir/noir-repo/compiler/noirc_frontend/src/lexer/token.rs +++ b/noir/noir-repo/compiler/noirc_frontend/src/lexer/token.rs @@ -883,8 +883,7 @@ impl AsRef for SecondaryAttribute { /// Note that `self` is not present - it is a contextual keyword rather than a true one as it is /// only special within `impl`s. Otherwise `self` functions as a normal identifier. -#[derive(PartialEq, Eq, Hash, Debug, Copy, Clone, PartialOrd, Ord)] -#[cfg_attr(test, derive(strum_macros::EnumIter))] +#[derive(PartialEq, Eq, Hash, Debug, Copy, Clone, PartialOrd, Ord, strum_macros::EnumIter)] pub enum Keyword { As, Assert, diff --git a/noir/noir-repo/compiler/noirc_frontend/src/parser/errors.rs b/noir/noir-repo/compiler/noirc_frontend/src/parser/errors.rs index 390afbefcda..36d3ce5898c 100644 --- a/noir/noir-repo/compiler/noirc_frontend/src/parser/errors.rs +++ b/noir/noir-repo/compiler/noirc_frontend/src/parser/errors.rs @@ -16,6 +16,8 @@ pub enum ParserErrorReason { ExpectedFieldName(Token), #[error("expected a pattern but found a type - {0}")] ExpectedPatternButFoundType(Token), + #[error("expected an identifier after ::")] + ExpectedIdentifierAfterColons, #[error("Expected a ; separating these two statements")] MissingSeparatingSemi, #[error("constrain keyword is deprecated")] diff --git a/noir/noir-repo/compiler/noirc_frontend/src/parser/parser/path.rs b/noir/noir-repo/compiler/noirc_frontend/src/parser/parser/path.rs index d0f7d66b270..ae3a1bc0b93 100644 --- a/noir/noir-repo/compiler/noirc_frontend/src/parser/parser/path.rs +++ b/noir/noir-repo/compiler/noirc_frontend/src/parser/parser/path.rs @@ -19,7 +19,19 @@ pub fn path_no_turbofish() -> impl NoirParser { } fn path_inner<'a>(segment: impl NoirParser + 'a) -> impl NoirParser + 'a { - let segments = segment.separated_by(just(Token::DoubleColon)).at_least(1); + let segments = segment + .separated_by(just(Token::DoubleColon)) + .at_least(1) + .then(just(Token::DoubleColon).then_ignore(none_of(Token::LeftBrace).rewind()).or_not()) + .validate(|(path_segments, trailing_colons), span, emit_error| { + if trailing_colons.is_some() { + emit_error(ParserError::with_reason( + ParserErrorReason::ExpectedIdentifierAfterColons, + span, + )); + } + path_segments + }); let make_path = |kind| move |segments, span| Path { segments, kind, span }; let prefix = |key| keyword(key).ignore_then(just(Token::DoubleColon)); @@ -69,7 +81,7 @@ mod test { use super::*; use crate::parser::{ parse_type, - parser::test_helpers::{parse_all_failing, parse_with}, + parser::test_helpers::{parse_all_failing, parse_recover, parse_with}, }; #[test] @@ -111,4 +123,18 @@ mod test { vec!["crate", "crate::std::crate", "foo::bar::crate", "foo::dep"], ); } + + #[test] + fn parse_path_with_trailing_colons() { + let src = "foo::bar::"; + + let (path, errors) = parse_recover(path_no_turbofish(), src); + let path = path.unwrap(); + assert_eq!(path.segments.len(), 2); + assert_eq!(path.segments[0].ident.0.contents, "foo"); + assert_eq!(path.segments[1].ident.0.contents, "bar"); + + assert_eq!(errors.len(), 1); + assert_eq!(errors[0].message, "expected an identifier after ::"); + } } diff --git a/noir/noir-repo/cspell.json b/noir/noir-repo/cspell.json index b9199bea4bd..e939cd583e8 100644 --- a/noir/noir-repo/cspell.json +++ b/noir/noir-repo/cspell.json @@ -88,6 +88,7 @@ "foralls", "formatcp", "frontends", + "fuzzer", "fxhash", "getrandom", "gloo", diff --git a/noir/noir-repo/noir_stdlib/src/meta/expr.nr b/noir/noir-repo/noir_stdlib/src/meta/expr.nr new file mode 100644 index 00000000000..54681632543 --- /dev/null +++ b/noir/noir-repo/noir_stdlib/src/meta/expr.nr @@ -0,0 +1,6 @@ +use crate::option::Option; + +impl Expr { + #[builtin(expr_as_function_call)] + fn as_function_call(self) -> Option<(Expr, [Expr])> {} +} diff --git a/noir/noir-repo/noir_stdlib/src/meta/mod.nr b/noir/noir-repo/noir_stdlib/src/meta/mod.nr index e00c8d41d11..2763685fd0d 100644 --- a/noir/noir-repo/noir_stdlib/src/meta/mod.nr +++ b/noir/noir-repo/noir_stdlib/src/meta/mod.nr @@ -2,6 +2,7 @@ use crate::collections::umap::UHashMap; use crate::hash::BuildHasherDefault; use crate::hash::poseidon2::Poseidon2Hasher; +mod expr; mod function_def; mod module; mod struct_def; diff --git a/noir/noir-repo/noir_stdlib/src/meta/quoted.nr b/noir/noir-repo/noir_stdlib/src/meta/quoted.nr index 9c8de23258c..cccc3fe0f12 100644 --- a/noir/noir-repo/noir_stdlib/src/meta/quoted.nr +++ b/noir/noir-repo/noir_stdlib/src/meta/quoted.nr @@ -2,6 +2,9 @@ use crate::cmp::Eq; use crate::option::Option; impl Quoted { + #[builtin(quoted_as_expr)] + fn as_expr(self) -> Option {} + #[builtin(quoted_as_module)] fn as_module(self) -> Option {} diff --git a/noir/noir-repo/noir_stdlib/src/meta/typ.nr b/noir/noir-repo/noir_stdlib/src/meta/typ.nr index 33c0a6799e9..ad669e93c0a 100644 --- a/noir/noir-repo/noir_stdlib/src/meta/typ.nr +++ b/noir/noir-repo/noir_stdlib/src/meta/typ.nr @@ -20,6 +20,9 @@ impl Type { #[builtin(type_as_tuple)] fn as_tuple(self) -> Option<[Type]> {} + #[builtin(type_implements)] + fn implements(self, constraint: TraitConstraint) -> bool {} + #[builtin(type_is_bool)] fn is_bool(self) -> bool {} diff --git a/noir/noir-repo/test_programs/compile_success_empty/comptime_exp/Nargo.toml b/noir/noir-repo/test_programs/compile_success_empty/comptime_exp/Nargo.toml new file mode 100644 index 00000000000..df36e0e05b0 --- /dev/null +++ b/noir/noir-repo/test_programs/compile_success_empty/comptime_exp/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "comptime_exp" +type = "bin" +authors = [""] +compiler_version = ">=0.31.0" + +[dependencies] \ No newline at end of file diff --git a/noir/noir-repo/test_programs/compile_success_empty/comptime_exp/src/main.nr b/noir/noir-repo/test_programs/compile_success_empty/comptime_exp/src/main.nr new file mode 100644 index 00000000000..8b6f7b480c7 --- /dev/null +++ b/noir/noir-repo/test_programs/compile_success_empty/comptime_exp/src/main.nr @@ -0,0 +1,8 @@ +fn main() { + comptime + { + let expr = quote { foo(bar) }.as_expr().unwrap(); + let (_function, args) = expr.as_function_call().unwrap(); + assert_eq(args.len(), 1); + } +} diff --git a/noir/noir-repo/test_programs/compile_success_empty/comptime_type/src/main.nr b/noir/noir-repo/test_programs/compile_success_empty/comptime_type/src/main.nr index e3a924ee41e..170292b0e37 100644 --- a/noir/noir-repo/test_programs/compile_success_empty/comptime_type/src/main.nr +++ b/noir/noir-repo/test_programs/compile_success_empty/comptime_type/src/main.nr @@ -4,6 +4,21 @@ struct Foo { x: T } +trait SomeTrait { + +} +struct StructImplementsSomeTrait { + +} + +impl SomeTrait for StructImplementsSomeTrait { + +} + +struct StructDoesNotImplementSomeTrait { + +} + fn main() { comptime { @@ -82,5 +97,25 @@ fn main() { assert_eq(generics.len(), 1); assert_eq(generics[0], field_type_1); + + // Check Type::implements + let some_trait_i32 = quote { SomeTrait }.as_trait_constraint(); + let struct_implements_some_trait = quote { StructImplementsSomeTrait }.as_type(); + let struct_does_not_implement_some_trait = quote { StructDoesNotImplementSomeTrait }.as_type(); + assert(struct_implements_some_trait.implements(some_trait_i32)); + assert(!struct_does_not_implement_some_trait.implements(some_trait_i32)); + + let some_trait_field = quote { SomeTrait }.as_trait_constraint(); + assert(!struct_implements_some_trait.implements(some_trait_field)); + assert(!struct_does_not_implement_some_trait.implements(some_trait_field)); + } +} + +fn function_with_where(_x: T) where T: SomeTrait { + comptime + { + let t = quote { T }.as_type(); + let some_trait_i32 = quote { SomeTrait }.as_trait_constraint(); + assert(t.implements(some_trait_i32)); } } diff --git a/noir/noir-repo/tooling/debugger/src/context.rs b/noir/noir-repo/tooling/debugger/src/context.rs index 2a550b84020..6ec1aff8325 100644 --- a/noir/noir-repo/tooling/debugger/src/context.rs +++ b/noir/noir-repo/tooling/debugger/src/context.rs @@ -1,6 +1,6 @@ use crate::foreign_calls::DebugForeignCallExecutor; use acvm::acir::brillig::BitSize; -use acvm::acir::circuit::brillig::BrilligBytecode; +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::MemoryValue; @@ -57,8 +57,25 @@ use std::collections::{hash_set::Iter, HashSet}; pub struct AddressMap { addresses: Vec>, - // virtual address of the last opcode of the program + /// Virtual address of the last opcode of the program last_valid_address: usize, + + /// Maps the "holes" in the `addresses` nodes to the Brillig function ID + /// associated with that address space. + brillig_addresses: Vec, +} + +/// Associates a BrilligFunctionId with the address space. +/// A BrilligFunctionId is found by checking whether an address is between +/// the `start_address` and `end_address` +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +struct BrilligAddressSpace { + /// The start of the Brillig call address space + start_address: usize, + /// The end of the Brillig address space + end_address: usize, + /// The Brillig function id associated with this address space + brillig_function_id: BrilligFunctionId, } impl AddressMap { @@ -68,25 +85,35 @@ impl AddressMap { ) -> Self { let opcode_address_size = |opcode: &Opcode| { if let Opcode::BrilligCall { id, .. } = opcode { - unconstrained_functions[id.as_usize()].bytecode.len() + (unconstrained_functions[id.as_usize()].bytecode.len(), Some(*id)) } else { - 1 + (1, None) } }; let mut addresses = Vec::with_capacity(circuits.len()); let mut next_address = 0usize; + let mut brillig_addresses = Vec::new(); for circuit in circuits { let mut circuit_addresses = Vec::with_capacity(circuit.opcodes.len()); for opcode in &circuit.opcodes { circuit_addresses.push(next_address); - next_address += opcode_address_size(opcode); + let (address_size, brillig_function_id) = opcode_address_size(opcode); + if let Some(brillig_function_id) = brillig_function_id { + let brillig_address_space = BrilligAddressSpace { + start_address: next_address, + end_address: next_address + address_size, + brillig_function_id, + }; + brillig_addresses.push(brillig_address_space); + } + next_address += address_size; } addresses.push(circuit_addresses); } - Self { addresses, last_valid_address: next_address - 1 } + Self { addresses, last_valid_address: next_address - 1, brillig_addresses } } /// Returns the absolute address of the opcode at the given location. @@ -120,16 +147,26 @@ impl AddressMap { // We binary search among the selected `circuit_id`` list of opcodes // If Err(insert_index) this means that the given address // is a Brillig addresses that's contained in previous index ACIR opcode index - let opcode_location = match self.addresses[circuit_id].binary_search(&address) { - Ok(found_index) => OpcodeLocation::Acir(found_index), - Err(insert_index) => { - let acir_index = insert_index - 1; - let base_offset = self.addresses[circuit_id][acir_index]; - let brillig_index = address - base_offset; - OpcodeLocation::Brillig { acir_index, brillig_index } - } - }; - Some(DebugLocation { circuit_id: circuit_id as u32, opcode_location }) + let (opcode_location, brillig_function_id) = + match self.addresses[circuit_id].binary_search(&address) { + Ok(found_index) => (OpcodeLocation::Acir(found_index), None), + Err(insert_index) => { + let acir_index = insert_index - 1; + let base_offset = self.addresses[circuit_id][acir_index]; + let brillig_index = address - base_offset; + let brillig_function_id = self + .brillig_addresses + .iter() + .find(|brillig_address_space| { + address >= brillig_address_space.start_address + && address <= brillig_address_space.end_address + }) + .map(|brillig_address_space| brillig_address_space.brillig_function_id); + (OpcodeLocation::Brillig { acir_index, brillig_index }, brillig_function_id) + } + }; + + Some(DebugLocation { circuit_id: circuit_id as u32, opcode_location, brillig_function_id }) } } @@ -137,6 +174,7 @@ impl AddressMap { pub struct DebugLocation { pub circuit_id: u32, pub opcode_location: OpcodeLocation, + pub brillig_function_id: Option, } impl std::fmt::Display for DebugLocation { @@ -165,13 +203,13 @@ impl std::str::FromStr for DebugLocation { match parts.len() { 1 => OpcodeLocation::from_str(parts[0]).map_or(error, |opcode_location| { - Ok(DebugLocation { circuit_id: 0, opcode_location }) + Ok(DebugLocation { circuit_id: 0, opcode_location, brillig_function_id: None }) }), 2 => { let first_part = parts[0].parse().ok(); let second_part = OpcodeLocation::from_str(parts[1]).ok(); if let (Some(circuit_id), Some(opcode_location)) = (first_part, second_part) { - Ok(DebugLocation { circuit_id, opcode_location }) + Ok(DebugLocation { circuit_id, opcode_location, brillig_function_id: None }) } else { error } @@ -276,12 +314,24 @@ impl<'a, B: BlackBoxFunctionSolver> DebugContext<'a, B> { if ip >= self.get_opcodes().len() { None } else { - let opcode_location = if let Some(ref solver) = self.brillig_solver { - OpcodeLocation::Brillig { acir_index: ip, brillig_index: solver.program_counter() } - } else { - OpcodeLocation::Acir(ip) - }; - Some(DebugLocation { circuit_id: self.current_circuit_id, opcode_location }) + let (opcode_location, brillig_function_id) = + if let Some(ref solver) = self.brillig_solver { + let function_id = solver.function_id; + ( + OpcodeLocation::Brillig { + acir_index: ip, + brillig_index: solver.program_counter(), + }, + Some(function_id), + ) + } else { + (OpcodeLocation::Acir(ip), None) + }; + Some(DebugLocation { + circuit_id: self.current_circuit_id, + brillig_function_id, + opcode_location, + }) } } @@ -293,6 +343,7 @@ impl<'a, B: BlackBoxFunctionSolver> DebugContext<'a, B> { .map(|ExecutionFrame { circuit_id, acvm }| DebugLocation { circuit_id: *circuit_id, opcode_location: OpcodeLocation::Acir(acvm.instruction_pointer()), + brillig_function_id: None, }) .collect(); @@ -306,11 +357,13 @@ impl<'a, B: BlackBoxFunctionSolver> DebugContext<'a, B> { acir_index: instruction_pointer, brillig_index: *program_counter, }, + brillig_function_id: Some(solver.function_id), })); } else if instruction_pointer < self.get_opcodes().len() { frames.push(DebugLocation { circuit_id, opcode_location: OpcodeLocation::Acir(instruction_pointer), + brillig_function_id: None, }); } frames @@ -388,15 +441,24 @@ impl<'a, B: BlackBoxFunctionSolver> DebugContext<'a, B> { ) -> Vec { self.debug_artifact.debug_symbols[debug_location.circuit_id as usize] .opcode_location(&debug_location.opcode_location) - .map(|source_locations| { - source_locations - .into_iter() - .filter(|source_location| { - !self.is_source_location_in_debug_module(source_location) - }) - .collect() + .unwrap_or_else(|| { + if let Some(brillig_function_id) = debug_location.brillig_function_id { + let brillig_locations = self.debug_artifact.debug_symbols + [debug_location.circuit_id as usize] + .brillig_locations + .get(&brillig_function_id); + brillig_locations + .unwrap() + .get(&debug_location.opcode_location) + .cloned() + .unwrap_or_default() + } else { + vec![] + } }) - .unwrap_or_default() + .into_iter() + .filter(|source_location| !self.is_source_location_in_debug_module(source_location)) + .collect() } /// Returns the current call stack with expanded source locations. In @@ -630,6 +692,7 @@ impl<'a, B: BlackBoxFunctionSolver> DebugContext<'a, B> { Some(DebugLocation { circuit_id, opcode_location: OpcodeLocation::Acir(acir_index), + .. }) => { matches!( self.get_opcodes_of_circuit(circuit_id)[acir_index], @@ -821,24 +884,22 @@ fn build_source_to_opcode_debug_mappings( let mut result: BTreeMap> = BTreeMap::new(); for (circuit_id, debug_symbols) in debug_artifact.debug_symbols.iter().enumerate() { - for (opcode_location, source_locations) in &debug_symbols.locations { - source_locations.iter().for_each(|source_location| { - let span = source_location.span; - let file_id = source_location.file; - let Some(file) = simple_files.get(&file_id) else { - return; - }; - let Ok(line_index) = file.line_index((), span.start() as usize) else { - return; - }; - let line_number = line_index + 1; + add_opcode_locations_map( + &debug_symbols.locations, + &mut result, + &simple_files, + circuit_id, + None, + ); - let debug_location = DebugLocation { - circuit_id: circuit_id as u32, - opcode_location: *opcode_location, - }; - result.entry(file_id).or_default().push((line_number, debug_location)); - }); + for (brillig_function_id, brillig_locations_map) in &debug_symbols.brillig_locations { + add_opcode_locations_map( + brillig_locations_map, + &mut result, + &simple_files, + circuit_id, + Some(*brillig_function_id), + ); } } result.iter_mut().for_each(|(_, file_locations)| file_locations.sort_by_key(|x| (x.0, x.1))); @@ -846,6 +907,35 @@ fn build_source_to_opcode_debug_mappings( result } +fn add_opcode_locations_map( + opcode_to_locations: &BTreeMap>, + source_to_locations: &mut BTreeMap>, + simple_files: &BTreeMap<&FileId, SimpleFile<&str, &str>>, + circuit_id: usize, + brillig_function_id: Option, +) { + for (opcode_location, source_locations) in opcode_to_locations { + source_locations.iter().for_each(|source_location| { + let span = source_location.span; + let file_id = source_location.file; + let Some(file) = simple_files.get(&file_id) else { + return; + }; + let Ok(line_index) = file.line_index((), span.start() as usize) else { + return; + }; + let line_number = line_index + 1; + + let debug_location = DebugLocation { + circuit_id: circuit_id as u32, + opcode_location: *opcode_location, + brillig_function_id, + }; + source_to_locations.entry(file_id).or_default().push((line_number, debug_location)); + }); + } +} + #[cfg(test)] mod tests { use super::*; @@ -928,7 +1018,11 @@ mod tests { assert_eq!( context.get_current_debug_location(), - Some(DebugLocation { circuit_id: 0, opcode_location: OpcodeLocation::Acir(0) }) + Some(DebugLocation { + circuit_id: 0, + opcode_location: OpcodeLocation::Acir(0), + brillig_function_id: None, + }) ); // Execute the first Brillig opcode (calldata copy) @@ -938,7 +1032,8 @@ mod tests { context.get_current_debug_location(), Some(DebugLocation { circuit_id: 0, - opcode_location: OpcodeLocation::Brillig { acir_index: 0, brillig_index: 1 } + opcode_location: OpcodeLocation::Brillig { acir_index: 0, brillig_index: 1 }, + brillig_function_id: Some(BrilligFunctionId(0)), }) ); @@ -949,7 +1044,8 @@ mod tests { context.get_current_debug_location(), Some(DebugLocation { circuit_id: 0, - opcode_location: OpcodeLocation::Brillig { acir_index: 0, brillig_index: 2 } + opcode_location: OpcodeLocation::Brillig { acir_index: 0, brillig_index: 2 }, + brillig_function_id: Some(BrilligFunctionId(0)), }) ); @@ -960,7 +1056,8 @@ mod tests { context.get_current_debug_location(), Some(DebugLocation { circuit_id: 0, - opcode_location: OpcodeLocation::Brillig { acir_index: 0, brillig_index: 2 } + opcode_location: OpcodeLocation::Brillig { acir_index: 0, brillig_index: 2 }, + brillig_function_id: Some(BrilligFunctionId(0)), }) ); @@ -971,7 +1068,8 @@ mod tests { context.get_current_debug_location(), Some(DebugLocation { circuit_id: 0, - opcode_location: OpcodeLocation::Brillig { acir_index: 0, brillig_index: 3 } + opcode_location: OpcodeLocation::Brillig { acir_index: 0, brillig_index: 3 }, + brillig_function_id: Some(BrilligFunctionId(0)), }) ); @@ -1056,6 +1154,7 @@ mod tests { let breakpoint_location = DebugLocation { circuit_id: 0, opcode_location: OpcodeLocation::Brillig { acir_index: 0, brillig_index: 1 }, + brillig_function_id: Some(BrilligFunctionId(0)), }; assert!(context.add_breakpoint(breakpoint_location)); @@ -1069,7 +1168,11 @@ mod tests { assert!(matches!(result, DebugCommandResult::Ok)); assert_eq!( context.get_current_debug_location(), - Some(DebugLocation { circuit_id: 0, opcode_location: OpcodeLocation::Acir(1) }) + Some(DebugLocation { + circuit_id: 0, + opcode_location: OpcodeLocation::Acir(1), + brillig_function_id: None + }) ); // last ACIR opcode @@ -1144,24 +1247,51 @@ mod tests { assert_eq!( locations, vec![ - Some(DebugLocation { circuit_id: 0, opcode_location: OpcodeLocation::Acir(0) }), - Some(DebugLocation { circuit_id: 0, opcode_location: OpcodeLocation::Acir(1) }), Some(DebugLocation { circuit_id: 0, - opcode_location: OpcodeLocation::Brillig { acir_index: 1, brillig_index: 1 } + opcode_location: OpcodeLocation::Acir(0), + brillig_function_id: None + }), + Some(DebugLocation { + circuit_id: 0, + opcode_location: OpcodeLocation::Acir(1), + brillig_function_id: None + }), + Some(DebugLocation { + circuit_id: 0, + opcode_location: OpcodeLocation::Brillig { acir_index: 1, brillig_index: 1 }, + brillig_function_id: Some(BrilligFunctionId(0)), + }), + Some(DebugLocation { + circuit_id: 0, + opcode_location: OpcodeLocation::Acir(2), + brillig_function_id: None + }), + Some(DebugLocation { + circuit_id: 0, + opcode_location: OpcodeLocation::Acir(3), + brillig_function_id: None + }), + Some(DebugLocation { + circuit_id: 1, + opcode_location: OpcodeLocation::Acir(0), + brillig_function_id: None + }), + Some(DebugLocation { + circuit_id: 1, + opcode_location: OpcodeLocation::Brillig { acir_index: 0, brillig_index: 1 }, + brillig_function_id: Some(BrilligFunctionId(1)), }), - Some(DebugLocation { circuit_id: 0, opcode_location: OpcodeLocation::Acir(2) }), - Some(DebugLocation { circuit_id: 0, opcode_location: OpcodeLocation::Acir(3) }), - Some(DebugLocation { circuit_id: 1, opcode_location: OpcodeLocation::Acir(0) }), Some(DebugLocation { circuit_id: 1, - opcode_location: OpcodeLocation::Brillig { acir_index: 0, brillig_index: 1 } + opcode_location: OpcodeLocation::Brillig { acir_index: 0, brillig_index: 2 }, + brillig_function_id: Some(BrilligFunctionId(1)), }), Some(DebugLocation { circuit_id: 1, - opcode_location: OpcodeLocation::Brillig { acir_index: 0, brillig_index: 2 } + opcode_location: OpcodeLocation::Acir(1), + brillig_function_id: None }), - Some(DebugLocation { circuit_id: 1, opcode_location: OpcodeLocation::Acir(1) }), ] ); @@ -1180,14 +1310,16 @@ mod tests { 1, context.debug_location_to_address(&DebugLocation { circuit_id: 0, - opcode_location: OpcodeLocation::Brillig { acir_index: 1, brillig_index: 0 } + opcode_location: OpcodeLocation::Brillig { acir_index: 1, brillig_index: 0 }, + brillig_function_id: Some(BrilligFunctionId(0)), }) ); assert_eq!( 5, context.debug_location_to_address(&DebugLocation { circuit_id: 1, - opcode_location: OpcodeLocation::Brillig { acir_index: 0, brillig_index: 0 } + opcode_location: OpcodeLocation::Brillig { acir_index: 0, brillig_index: 0 }, + brillig_function_id: Some(BrilligFunctionId(1)), }) ); } diff --git a/noir/noir-repo/tooling/debugger/src/repl.rs b/noir/noir-repo/tooling/debugger/src/repl.rs index b4b2bff53be..1a7c2d6c7a8 100644 --- a/noir/noir-repo/tooling/debugger/src/repl.rs +++ b/noir/noir-repo/tooling/debugger/src/repl.rs @@ -1,7 +1,7 @@ use crate::context::{DebugCommandResult, DebugContext, DebugLocation}; use acvm::acir::brillig::{BitSize, IntegerBitSize}; -use acvm::acir::circuit::brillig::BrilligBytecode; +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; @@ -168,36 +168,41 @@ impl<'a, B: BlackBoxFunctionSolver> ReplDebugger<'a, B> { } else if self.context.is_breakpoint_set(&DebugLocation { circuit_id, opcode_location: OpcodeLocation::Acir(acir_index), + brillig_function_id: None, }) { " *" } else { "" } }; - let brillig_marker = |acir_index, brillig_index| { + let brillig_marker = |acir_index, brillig_index, brillig_function_id| { if current_acir_index == Some(acir_index) && brillig_index == current_brillig_index { "->" } else if self.context.is_breakpoint_set(&DebugLocation { circuit_id, opcode_location: OpcodeLocation::Brillig { acir_index, brillig_index }, + brillig_function_id: Some(brillig_function_id), }) { " *" } else { "" } }; - let print_brillig_bytecode = |acir_index, bytecode: &[BrilligOpcode]| { - for (brillig_index, brillig_opcode) in bytecode.iter().enumerate() { - println!( - "{:>2}:{:>3}.{:<2} |{:2} {:?}", - circuit_id, - acir_index, - brillig_index, - brillig_marker(acir_index, brillig_index), - brillig_opcode - ); - } - }; + let print_brillig_bytecode = + |acir_index, + bytecode: &[BrilligOpcode], + brillig_function_id: BrilligFunctionId| { + for (brillig_index, brillig_opcode) in bytecode.iter().enumerate() { + println!( + "{:>2}:{:>3}.{:<2} |{:2} {:?}", + circuit_id, + acir_index, + brillig_index, + brillig_marker(acir_index, brillig_index, brillig_function_id), + brillig_opcode + ); + } + }; for (acir_index, opcode) in opcodes.iter().enumerate() { let marker = outer_marker(acir_index); match &opcode { @@ -208,7 +213,7 @@ impl<'a, B: BlackBoxFunctionSolver> ReplDebugger<'a, B> { ); println!(" | outputs={:?}", outputs); let bytecode = &self.unconstrained_functions[id.as_usize()].bytecode; - print_brillig_bytecode(acir_index, bytecode); + print_brillig_bytecode(acir_index, bytecode, *id); } _ => println!("{:>2}:{:>3} {:2} {:?}", circuit_id, acir_index, marker, opcode), } diff --git a/noir/noir-repo/tooling/fuzzer/Cargo.toml b/noir/noir-repo/tooling/fuzzer/Cargo.toml index ef49d707d6a..106d8abead1 100644 --- a/noir/noir-repo/tooling/fuzzer/Cargo.toml +++ b/noir/noir-repo/tooling/fuzzer/Cargo.toml @@ -11,7 +11,6 @@ license.workspace = true [dependencies] acvm.workspace = true -nargo.workspace = true noirc_artifacts.workspace = true noirc_abi.workspace = true proptest.workspace = true diff --git a/noir/noir-repo/tooling/fuzzer/src/lib.rs b/noir/noir-repo/tooling/fuzzer/src/lib.rs index 35a614663dc..28a43279c95 100644 --- a/noir/noir-repo/tooling/fuzzer/src/lib.rs +++ b/noir/noir-repo/tooling/fuzzer/src/lib.rs @@ -3,7 +3,13 @@ //! //! Code is used under the MIT license. -use acvm::{blackbox_solver::StubbedBlackBoxSolver, FieldElement}; +use acvm::{ + acir::{ + circuit::Program, + native_types::{WitnessMap, WitnessStack}, + }, + FieldElement, +}; use dictionary::build_dictionary_from_program; use noirc_abi::InputMap; use proptest::test_runner::{TestCaseError, TestError, TestRunner}; @@ -16,25 +22,32 @@ use types::{CaseOutcome, CounterExampleOutcome, FuzzOutcome, FuzzTestResult}; use noirc_artifacts::program::ProgramArtifact; -use nargo::ops::{execute_program, DefaultForeignCallExecutor}; - /// An executor for Noir programs which which provides fuzzing support using [`proptest`]. /// /// After instantiation, calling `fuzz` will proceed to hammer the program with /// inputs, until it finds a counterexample. The provided [`TestRunner`] contains all the /// configuration which can be overridden via [environment variables](proptest::test_runner::Config) -pub struct FuzzedExecutor { +pub struct FuzzedExecutor { /// The program to be fuzzed program: ProgramArtifact, + /// A function which executes the programs with a given set of inputs + executor: E, + /// The fuzzer runner: TestRunner, } -impl FuzzedExecutor { +impl< + E: Fn( + &Program, + WitnessMap, + ) -> Result, String>, + > FuzzedExecutor +{ /// Instantiates a fuzzed executor given a testrunner - pub fn new(program: ProgramArtifact, runner: TestRunner) -> Self { - Self { program, runner } + pub fn new(program: ProgramArtifact, executor: E, runner: TestRunner) -> Self { + Self { program, executor, runner } } /// Fuzzes the provided program. @@ -76,19 +89,14 @@ impl FuzzedExecutor { /// or a `CounterExampleOutcome` pub fn single_fuzz(&self, input_map: InputMap) -> Result { let initial_witness = self.program.abi.encode(&input_map, None).unwrap(); - let result = execute_program( - &self.program.bytecode, - initial_witness, - &StubbedBlackBoxSolver, - &mut DefaultForeignCallExecutor::::new(false, None, None, None), - ); + let result = (self.executor)(&self.program.bytecode, initial_witness); // TODO: Add handling for `vm.assume` equivalent match result { Ok(_) => Ok(FuzzOutcome::Case(CaseOutcome { case: input_map })), Err(err) => Ok(FuzzOutcome::CounterExample(CounterExampleOutcome { - exit_reason: err.to_string(), + exit_reason: err, counterexample: input_map, })), } diff --git a/noir/noir-repo/tooling/lsp/Cargo.toml b/noir/noir-repo/tooling/lsp/Cargo.toml index ac3e3b1d30a..03c6c9105ba 100644 --- a/noir/noir-repo/tooling/lsp/Cargo.toml +++ b/noir/noir-repo/tooling/lsp/Cargo.toml @@ -22,6 +22,7 @@ noirc_frontend.workspace = true noirc_artifacts.workspace = true serde.workspace = true serde_json.workspace = true +strum = "0.24" tower.workspace = true async-lsp = { workspace = true, features = ["omni-trait"] } serde_with = "3.2.0" diff --git a/noir/noir-repo/tooling/lsp/src/lib.rs b/noir/noir-repo/tooling/lsp/src/lib.rs index 88aab65c6fa..ca34d7686fd 100644 --- a/noir/noir-repo/tooling/lsp/src/lib.rs +++ b/noir/noir-repo/tooling/lsp/src/lib.rs @@ -22,8 +22,8 @@ use fm::{codespan_files as files, FileManager}; use fxhash::FxHashSet; use lsp_types::{ request::{ - DocumentSymbolRequest, HoverRequest, InlayHintRequest, PrepareRenameRequest, References, - Rename, + Completion, DocumentSymbolRequest, HoverRequest, InlayHintRequest, PrepareRenameRequest, + References, Rename, }, CodeLens, }; @@ -36,7 +36,10 @@ use nargo_toml::{find_file_manifest, resolve_workspace_from_toml, PackageSelecti use noirc_driver::{file_manager_with_stdlib, prepare_crate, NOIR_ARTIFACT_VERSION_STRING}; use noirc_frontend::{ graph::{CrateId, CrateName}, - hir::{def_map::parse_file, Context, FunctionNameMatch, ParsedFiles}, + hir::{ + def_map::{parse_file, CrateDefMap}, + Context, FunctionNameMatch, ParsedFiles, + }, node_interner::NodeInterner, parser::ParserError, ParsedModule, @@ -48,11 +51,11 @@ use notifications::{ on_did_open_text_document, on_did_save_text_document, on_exit, on_initialized, }; use requests::{ - on_code_lens_request, on_document_symbol_request, on_formatting, on_goto_declaration_request, - on_goto_definition_request, on_goto_type_definition_request, on_hover_request, on_initialize, - on_inlay_hint_request, on_prepare_rename_request, on_profile_run_request, - on_references_request, on_rename_request, on_shutdown, on_test_run_request, on_tests_request, - LspInitializationOptions, + on_code_lens_request, on_completion_request, on_document_symbol_request, on_formatting, + on_goto_declaration_request, on_goto_definition_request, on_goto_type_definition_request, + on_hover_request, on_initialize, on_inlay_hint_request, on_prepare_rename_request, + on_profile_run_request, on_references_request, on_rename_request, on_shutdown, + on_test_run_request, on_tests_request, LspInitializationOptions, }; use serde_json::Value as JsonValue; use thiserror::Error; @@ -62,6 +65,7 @@ mod notifications; mod requests; mod solver; mod types; +mod utils; #[cfg(test)] mod test_utils; @@ -86,6 +90,7 @@ pub struct LspState { cached_lenses: HashMap>, cached_definitions: HashMap, cached_parsed_files: HashMap))>, + cached_def_maps: HashMap>, options: LspInitializationOptions, } @@ -103,6 +108,7 @@ impl LspState { cached_definitions: HashMap::new(), open_documents_count: 0, cached_parsed_files: HashMap::new(), + cached_def_maps: HashMap::new(), options: Default::default(), } } @@ -136,6 +142,7 @@ impl NargoLspService { .request::(on_rename_request) .request::(on_hover_request) .request::(on_inlay_hint_request) + .request::(on_completion_request) .notification::(on_initialized) .notification::(on_did_change_configuration) .notification::(on_did_open_text_document) diff --git a/noir/noir-repo/tooling/lsp/src/notifications/mod.rs b/noir/noir-repo/tooling/lsp/src/notifications/mod.rs index 56aef90cfde..3a60de15c4a 100644 --- a/noir/noir-repo/tooling/lsp/src/notifications/mod.rs +++ b/noir/noir-repo/tooling/lsp/src/notifications/mod.rs @@ -30,7 +30,7 @@ pub(super) fn on_did_change_configuration( ControlFlow::Continue(()) } -pub(super) fn on_did_open_text_document( +pub(crate) fn on_did_open_text_document( state: &mut LspState, params: DidOpenTextDocumentParams, ) -> ControlFlow> { @@ -153,8 +153,8 @@ pub(crate) fn process_workspace_for_noir_document( Some(&file_path), ); state.cached_lenses.insert(document_uri.to_string(), collected_lenses); - - state.cached_definitions.insert(package_root_dir, context.def_interner); + state.cached_definitions.insert(package_root_dir.clone(), context.def_interner); + state.cached_def_maps.insert(package_root_dir.clone(), context.def_maps); let fm = &context.file_manager; let files = fm.as_file_map(); diff --git a/noir/noir-repo/tooling/lsp/src/requests/completion.rs b/noir/noir-repo/tooling/lsp/src/requests/completion.rs new file mode 100644 index 00000000000..48616c0f52d --- /dev/null +++ b/noir/noir-repo/tooling/lsp/src/requests/completion.rs @@ -0,0 +1,2204 @@ +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + future::{self, Future}, +}; + +use async_lsp::ResponseError; +use builtins::{builtin_integer_types, keyword_builtin_function, keyword_builtin_type}; +use fm::{FileId, PathString}; +use lsp_types::{ + CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionParams, + CompletionResponse, InsertTextFormat, +}; +use noirc_errors::{Location, Span}; +use noirc_frontend::{ + ast::{ + ArrayLiteral, AsTraitPath, BlockExpression, CallExpression, CastExpression, + ConstrainStatement, ConstructorExpression, Expression, ForLoopStatement, ForRange, + FunctionReturnType, Ident, IfExpression, IndexExpression, InfixExpression, LValue, Lambda, + LetStatement, Literal, MemberAccessExpression, MethodCallExpression, NoirFunction, + NoirStruct, NoirTrait, NoirTraitImpl, NoirTypeAlias, Path, PathKind, PathSegment, Pattern, + Statement, TraitImplItem, TraitItem, TypeImpl, UnresolvedGeneric, UnresolvedGenerics, + UnresolvedType, UseTree, UseTreeKind, + }, + graph::{CrateId, Dependency}, + hir::{ + def_map::{CrateDefMap, LocalModuleId, ModuleId}, + resolution::path_resolver::{PathResolver, StandardPathResolver}, + }, + hir_def::{function::FuncMeta, stmt::HirPattern}, + macros_api::{ModuleDefId, NodeInterner, StructId}, + node_interner::{FuncId, GlobalId, ReferenceId, TraitId, TypeAliasId}, + parser::{Item, ItemKind}, + token::Keyword, + ParsedModule, Type, +}; +use strum::IntoEnumIterator; + +use crate::{utils, LspState}; + +use super::process_request; + +mod builtins; + +/// When finding items in a module, whether to show only direct children or all visible items. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum ModuleCompletionKind { + // Only show a module's direct children. This is used when completing a use statement + // or a path after the first segment. + DirectChildren, + // Show all of a module's visible items. This is used when completing a path outside + // of a use statement (in regular code) when the path is just a single segment: + // we want to find items exposed in the current module. + AllVisibleItems, +} + +/// When suggest a function as a result of completion, whether to autocomplete its name or its name and parameters. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum FunctionCompleteKind { + // Only complete a function's name. This is used in use statement. + Name, + // Complete a function's name and parameters (as a snippet). This is used in regular code. + NameAndParameters, +} + +/// When requesting completions, whether to list all items or just types. +/// For example, when writing `let x: S` we only want to suggest types at this +/// point (modules too, because they might include types too). +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum RequestedItems { + // Suggest any items (types, functions, etc.). + AnyItems, + // Only suggest types. + OnlyTypes, +} + +pub(crate) fn on_completion_request( + state: &mut LspState, + params: CompletionParams, +) -> impl Future, ResponseError>> { + let uri = params.text_document_position.clone().text_document.uri; + + let result = process_request(state, params.text_document_position.clone(), |args| { + let path = PathString::from_path(uri.to_file_path().unwrap()); + args.files.get_file_id(&path).and_then(|file_id| { + utils::position_to_byte_index( + args.files, + file_id, + ¶ms.text_document_position.position, + ) + .and_then(|byte_index| { + let file = args.files.get_file(file_id).unwrap(); + let source = file.source(); + let byte = source.as_bytes().get(byte_index - 1).copied(); + let (parsed_module, _errors) = noirc_frontend::parse_program(source); + + let mut finder = NodeFinder::new( + file_id, + byte_index, + byte, + args.crate_id, + args.def_maps, + args.dependencies, + args.interner, + ); + finder.find(&parsed_module) + }) + }) + }); + future::ready(result) +} + +struct NodeFinder<'a> { + file: FileId, + byte_index: usize, + byte: Option, + /// The module ID of the current file. + root_module_id: ModuleId, + /// The module ID in scope. This might change as we traverse the AST + /// if we are analyzing something inside an inline module declaration. + module_id: ModuleId, + def_maps: &'a BTreeMap, + dependencies: &'a Vec, + interner: &'a NodeInterner, + /// Completion items we find along the way. + completion_items: Vec, + /// Local variables in the current scope, mapped to their locations. + /// As we traverse the AST, we collect local variables. + local_variables: HashMap, + /// Type parameters in the current scope. These are collected when entering + /// a struct, a function, etc., and cleared afterwards. + type_parameters: HashSet, +} + +impl<'a> NodeFinder<'a> { + fn new( + file: FileId, + byte_index: usize, + byte: Option, + krate: CrateId, + def_maps: &'a BTreeMap, + dependencies: &'a Vec, + interner: &'a NodeInterner, + ) -> Self { + // Find the module the current file belongs to + let def_map = &def_maps[&krate]; + let root_module_id = ModuleId { krate, local_id: def_map.root() }; + let local_id = if let Some((module_index, _)) = + def_map.modules().iter().find(|(_, module_data)| module_data.location.file == file) + { + LocalModuleId(module_index) + } else { + def_map.root() + }; + let module_id = ModuleId { krate, local_id }; + Self { + file, + byte_index, + byte, + root_module_id, + module_id, + def_maps, + dependencies, + interner, + completion_items: Vec::new(), + local_variables: HashMap::new(), + type_parameters: HashSet::new(), + } + } + + fn find(&mut self, parsed_module: &ParsedModule) -> Option { + self.find_in_parsed_module(parsed_module); + + if self.completion_items.is_empty() { + None + } else { + Some(CompletionResponse::Array(std::mem::take(&mut self.completion_items))) + } + } + + fn find_in_parsed_module(&mut self, parsed_module: &ParsedModule) { + for item in &parsed_module.items { + self.find_in_item(item); + } + } + + fn find_in_item(&mut self, item: &Item) { + if !self.includes_span(item.span) { + return; + } + + match &item.kind { + ItemKind::Import(use_tree) => { + let mut prefixes = Vec::new(); + self.find_in_use_tree(use_tree, &mut prefixes); + } + ItemKind::Submodules(parsed_sub_module) => { + // Switch `self.module_id` to the submodule + let previous_module_id = self.module_id; + + let def_map = &self.def_maps[&self.module_id.krate]; + let Some(module_data) = def_map.modules().get(self.module_id.local_id.0) else { + return; + }; + if let Some(child_module) = module_data.children.get(&parsed_sub_module.name) { + self.module_id = + ModuleId { krate: self.module_id.krate, local_id: *child_module }; + } + + self.find_in_parsed_module(&parsed_sub_module.contents); + + // Restore the old module before continuing + self.module_id = previous_module_id; + } + ItemKind::Function(noir_function) => self.find_in_noir_function(noir_function), + ItemKind::TraitImpl(noir_trait_impl) => self.find_in_noir_trait_impl(noir_trait_impl), + ItemKind::Impl(type_impl) => self.find_in_type_impl(type_impl), + ItemKind::Global(let_statement) => self.find_in_let_statement(let_statement, false), + ItemKind::TypeAlias(noir_type_alias) => self.find_in_noir_type_alias(noir_type_alias), + ItemKind::Struct(noir_struct) => self.find_in_noir_struct(noir_struct), + ItemKind::Trait(noir_trait) => self.find_in_noir_trait(noir_trait), + ItemKind::ModuleDecl(_) => (), + } + } + + fn find_in_noir_function(&mut self, noir_function: &NoirFunction) { + let old_type_parameters = self.type_parameters.clone(); + self.collect_type_parameters_in_generics(&noir_function.def.generics); + + for param in &noir_function.def.parameters { + self.find_in_unresolved_type(¶m.typ); + } + + self.find_in_function_return_type(&noir_function.def.return_type); + + self.local_variables.clear(); + for param in &noir_function.def.parameters { + self.collect_local_variables(¶m.pattern); + } + + self.find_in_block_expression(&noir_function.def.body); + + self.type_parameters = old_type_parameters; + } + + fn find_in_noir_trait_impl(&mut self, noir_trait_impl: &NoirTraitImpl) { + self.type_parameters.clear(); + self.collect_type_parameters_in_generics(&noir_trait_impl.impl_generics); + + for item in &noir_trait_impl.items { + self.find_in_trait_impl_item(item); + } + + self.type_parameters.clear(); + } + + fn find_in_trait_impl_item(&mut self, item: &TraitImplItem) { + match item { + TraitImplItem::Function(noir_function) => self.find_in_noir_function(noir_function), + TraitImplItem::Constant(_, _, _) => (), + TraitImplItem::Type { .. } => (), + } + } + + fn find_in_type_impl(&mut self, type_impl: &TypeImpl) { + self.type_parameters.clear(); + self.collect_type_parameters_in_generics(&type_impl.generics); + + for (method, span) in &type_impl.methods { + self.find_in_noir_function(method); + + // Optimization: stop looking in functions past the completion cursor + if span.end() as usize > self.byte_index { + break; + } + } + + self.type_parameters.clear(); + } + + fn find_in_noir_type_alias(&mut self, noir_type_alias: &NoirTypeAlias) { + self.find_in_unresolved_type(&noir_type_alias.typ); + } + + fn find_in_noir_struct(&mut self, noir_struct: &NoirStruct) { + self.type_parameters.clear(); + self.collect_type_parameters_in_generics(&noir_struct.generics); + + for (_name, unresolved_type) in &noir_struct.fields { + self.find_in_unresolved_type(unresolved_type); + } + + self.type_parameters.clear(); + } + + fn find_in_noir_trait(&mut self, noir_trait: &NoirTrait) { + for item in &noir_trait.items { + self.find_in_trait_item(item); + } + } + + fn find_in_trait_item(&mut self, trait_item: &TraitItem) { + match trait_item { + TraitItem::Function { + name: _, + generics, + parameters, + return_type, + where_clause, + body, + } => { + let old_type_parameters = self.type_parameters.clone(); + self.collect_type_parameters_in_generics(generics); + + for (_name, unresolved_type) in parameters { + self.find_in_unresolved_type(unresolved_type); + } + + self.find_in_function_return_type(return_type); + + for unresolved_trait_constraint in where_clause { + self.find_in_unresolved_type(&unresolved_trait_constraint.typ); + } + + if let Some(body) = body { + self.local_variables.clear(); + for (name, _) in parameters { + self.local_variables.insert(name.to_string(), name.span()); + } + self.find_in_block_expression(body); + }; + + self.type_parameters = old_type_parameters; + } + TraitItem::Constant { name: _, typ, default_value } => { + self.find_in_unresolved_type(typ); + + if let Some(default_value) = default_value { + self.find_in_expression(default_value); + } + } + TraitItem::Type { name: _ } => (), + } + } + + fn find_in_block_expression(&mut self, block_expression: &BlockExpression) { + let old_local_variables = self.local_variables.clone(); + for statement in &block_expression.statements { + self.find_in_statement(statement); + + // Optimization: stop looking in statements past the completion cursor + if statement.span.end() as usize > self.byte_index { + break; + } + } + self.local_variables = old_local_variables; + } + + fn find_in_statement(&mut self, statement: &Statement) { + match &statement.kind { + noirc_frontend::ast::StatementKind::Let(let_statement) => { + self.find_in_let_statement(let_statement, true); + } + noirc_frontend::ast::StatementKind::Constrain(constrain_statement) => { + self.find_in_constrain_statement(constrain_statement); + } + noirc_frontend::ast::StatementKind::Expression(expression) => { + self.find_in_expression(expression); + } + noirc_frontend::ast::StatementKind::Assign(assign_statement) => { + self.find_in_assign_statement(assign_statement); + } + noirc_frontend::ast::StatementKind::For(for_loop_statement) => { + self.find_in_for_loop_statement(for_loop_statement); + } + noirc_frontend::ast::StatementKind::Comptime(statement) => { + // When entering a comptime block, regular local variables shouldn't be offered anymore + let old_local_variables = self.local_variables.clone(); + self.local_variables.clear(); + + self.find_in_statement(statement); + + self.local_variables = old_local_variables; + } + noirc_frontend::ast::StatementKind::Semi(expression) => { + self.find_in_expression(expression); + } + noirc_frontend::ast::StatementKind::Break + | noirc_frontend::ast::StatementKind::Continue + | noirc_frontend::ast::StatementKind::Error => (), + } + } + + fn find_in_let_statement( + &mut self, + let_statement: &LetStatement, + collect_local_variables: bool, + ) { + self.find_in_unresolved_type(&let_statement.r#type); + self.find_in_expression(&let_statement.expression); + + if collect_local_variables { + self.collect_local_variables(&let_statement.pattern); + } + } + + fn find_in_constrain_statement(&mut self, constrain_statement: &ConstrainStatement) { + self.find_in_expression(&constrain_statement.0); + + if let Some(exp) = &constrain_statement.1 { + self.find_in_expression(exp); + } + } + + fn find_in_assign_statement( + &mut self, + assign_statement: &noirc_frontend::ast::AssignStatement, + ) { + self.find_in_lvalue(&assign_statement.lvalue); + self.find_in_expression(&assign_statement.expression); + } + + fn find_in_for_loop_statement(&mut self, for_loop_statement: &ForLoopStatement) { + let old_local_variables = self.local_variables.clone(); + let ident = &for_loop_statement.identifier; + self.local_variables.insert(ident.to_string(), ident.span()); + + self.find_in_for_range(&for_loop_statement.range); + self.find_in_expression(&for_loop_statement.block); + + self.local_variables = old_local_variables; + } + + fn find_in_lvalue(&mut self, lvalue: &LValue) { + match lvalue { + LValue::Ident(_) => (), + LValue::MemberAccess { object, field_name: _, span: _ } => self.find_in_lvalue(object), + LValue::Index { array, index, span: _ } => { + self.find_in_lvalue(array); + self.find_in_expression(index); + } + LValue::Dereference(lvalue, _) => self.find_in_lvalue(lvalue), + } + } + + fn find_in_for_range(&mut self, for_range: &ForRange) { + match for_range { + ForRange::Range(start, end) => { + self.find_in_expression(start); + self.find_in_expression(end); + } + ForRange::Array(expression) => self.find_in_expression(expression), + } + } + + fn find_in_expressions(&mut self, expressions: &[Expression]) { + for expression in expressions { + self.find_in_expression(expression); + } + } + + fn find_in_expression(&mut self, expression: &Expression) { + match &expression.kind { + noirc_frontend::ast::ExpressionKind::Literal(literal) => self.find_in_literal(literal), + noirc_frontend::ast::ExpressionKind::Block(block_expression) => { + self.find_in_block_expression(block_expression); + } + noirc_frontend::ast::ExpressionKind::Prefix(prefix_expression) => { + self.find_in_expression(&prefix_expression.rhs); + } + noirc_frontend::ast::ExpressionKind::Index(index_expression) => { + self.find_in_index_expression(index_expression); + } + noirc_frontend::ast::ExpressionKind::Call(call_expression) => { + self.find_in_call_expression(call_expression); + } + noirc_frontend::ast::ExpressionKind::MethodCall(method_call_expression) => { + self.find_in_method_call_expression(method_call_expression); + } + noirc_frontend::ast::ExpressionKind::Constructor(constructor_expression) => { + self.find_in_constructor_expression(constructor_expression); + } + noirc_frontend::ast::ExpressionKind::MemberAccess(member_access_expression) => { + self.find_in_member_access_expression(member_access_expression); + } + noirc_frontend::ast::ExpressionKind::Cast(cast_expression) => { + self.find_in_cast_expression(cast_expression); + } + noirc_frontend::ast::ExpressionKind::Infix(infix_expression) => { + self.find_in_infix_expression(infix_expression); + } + noirc_frontend::ast::ExpressionKind::If(if_expression) => { + self.find_in_if_expression(if_expression); + } + noirc_frontend::ast::ExpressionKind::Variable(path) => { + self.find_in_path(path, RequestedItems::AnyItems); + } + noirc_frontend::ast::ExpressionKind::Tuple(expressions) => { + self.find_in_expressions(expressions); + } + noirc_frontend::ast::ExpressionKind::Lambda(lambda) => self.find_in_lambda(lambda), + noirc_frontend::ast::ExpressionKind::Parenthesized(expression) => { + self.find_in_expression(expression); + } + noirc_frontend::ast::ExpressionKind::Unquote(expression) => { + self.find_in_expression(expression); + } + noirc_frontend::ast::ExpressionKind::Comptime(block_expression, _) => { + // When entering a comptime block, regular local variables shouldn't be offered anymore + let old_local_variables = self.local_variables.clone(); + self.local_variables.clear(); + + self.find_in_block_expression(block_expression); + + self.local_variables = old_local_variables; + } + noirc_frontend::ast::ExpressionKind::AsTraitPath(as_trait_path) => { + self.find_in_as_trait_path(as_trait_path); + } + noirc_frontend::ast::ExpressionKind::Quote(_) + | noirc_frontend::ast::ExpressionKind::Resolved(_) + | noirc_frontend::ast::ExpressionKind::Error => (), + } + } + + fn find_in_literal(&mut self, literal: &Literal) { + match literal { + Literal::Array(array_literal) => self.find_in_array_literal(array_literal), + Literal::Slice(array_literal) => self.find_in_array_literal(array_literal), + Literal::Bool(_) + | Literal::Integer(_, _) + | Literal::Str(_) + | Literal::RawStr(_, _) + | Literal::FmtStr(_) + | Literal::Unit => (), + } + } + + fn find_in_array_literal(&mut self, array_literal: &ArrayLiteral) { + match array_literal { + ArrayLiteral::Standard(expressions) => self.find_in_expressions(expressions), + ArrayLiteral::Repeated { repeated_element, length } => { + self.find_in_expression(repeated_element); + self.find_in_expression(length); + } + } + } + + fn find_in_index_expression(&mut self, index_expression: &IndexExpression) { + self.find_in_expression(&index_expression.collection); + self.find_in_expression(&index_expression.index); + } + + fn find_in_call_expression(&mut self, call_expression: &CallExpression) { + self.find_in_expression(&call_expression.func); + self.find_in_expressions(&call_expression.arguments); + } + + fn find_in_method_call_expression(&mut self, method_call_expression: &MethodCallExpression) { + self.find_in_expression(&method_call_expression.object); + self.find_in_expressions(&method_call_expression.arguments); + } + + fn find_in_constructor_expression(&mut self, constructor_expression: &ConstructorExpression) { + self.find_in_path(&constructor_expression.type_name, RequestedItems::OnlyTypes); + + for (_field_name, expression) in &constructor_expression.fields { + self.find_in_expression(expression); + } + } + + fn find_in_member_access_expression( + &mut self, + member_access_expression: &MemberAccessExpression, + ) { + self.find_in_expression(&member_access_expression.lhs); + } + + fn find_in_cast_expression(&mut self, cast_expression: &CastExpression) { + self.find_in_expression(&cast_expression.lhs); + } + + fn find_in_infix_expression(&mut self, infix_expression: &InfixExpression) { + self.find_in_expression(&infix_expression.lhs); + self.find_in_expression(&infix_expression.rhs); + } + + fn find_in_if_expression(&mut self, if_expression: &IfExpression) { + self.find_in_expression(&if_expression.condition); + + let old_local_variables = self.local_variables.clone(); + self.find_in_expression(&if_expression.consequence); + self.local_variables = old_local_variables; + + if let Some(alternative) = &if_expression.alternative { + let old_local_variables = self.local_variables.clone(); + self.find_in_expression(alternative); + self.local_variables = old_local_variables; + } + } + + fn find_in_lambda(&mut self, lambda: &Lambda) { + for (_, unresolved_type) in &lambda.parameters { + self.find_in_unresolved_type(unresolved_type); + } + + let old_local_variables = self.local_variables.clone(); + for (pattern, _) in &lambda.parameters { + self.collect_local_variables(pattern); + } + + self.find_in_expression(&lambda.body); + + self.local_variables = old_local_variables; + } + + fn find_in_as_trait_path(&mut self, as_trait_path: &AsTraitPath) { + self.find_in_path(&as_trait_path.trait_path, RequestedItems::OnlyTypes); + } + + fn find_in_function_return_type(&mut self, return_type: &FunctionReturnType) { + match return_type { + noirc_frontend::ast::FunctionReturnType::Default(_) => (), + noirc_frontend::ast::FunctionReturnType::Ty(unresolved_type) => { + self.find_in_unresolved_type(unresolved_type); + } + } + } + + fn find_in_unresolved_types(&mut self, unresolved_type: &[UnresolvedType]) { + for unresolved_type in unresolved_type { + self.find_in_unresolved_type(unresolved_type); + } + } + + fn find_in_unresolved_type(&mut self, unresolved_type: &UnresolvedType) { + if let Some(span) = unresolved_type.span { + if !self.includes_span(span) { + return; + } + } + + match &unresolved_type.typ { + noirc_frontend::ast::UnresolvedTypeData::Array(_, unresolved_type) => { + self.find_in_unresolved_type(unresolved_type); + } + noirc_frontend::ast::UnresolvedTypeData::Slice(unresolved_type) => { + self.find_in_unresolved_type(unresolved_type); + } + noirc_frontend::ast::UnresolvedTypeData::Parenthesized(unresolved_type) => { + self.find_in_unresolved_type(unresolved_type); + } + noirc_frontend::ast::UnresolvedTypeData::Named(path, unresolved_types, _) => { + self.find_in_path(path, RequestedItems::OnlyTypes); + self.find_in_unresolved_types(unresolved_types); + } + noirc_frontend::ast::UnresolvedTypeData::TraitAsType(path, unresolved_types) => { + self.find_in_path(path, RequestedItems::OnlyTypes); + self.find_in_unresolved_types(unresolved_types); + } + noirc_frontend::ast::UnresolvedTypeData::MutableReference(unresolved_type) => { + self.find_in_unresolved_type(unresolved_type); + } + noirc_frontend::ast::UnresolvedTypeData::Tuple(unresolved_types) => { + self.find_in_unresolved_types(unresolved_types); + } + noirc_frontend::ast::UnresolvedTypeData::Function(args, ret, env) => { + self.find_in_unresolved_types(args); + self.find_in_unresolved_type(ret); + self.find_in_unresolved_type(env); + } + noirc_frontend::ast::UnresolvedTypeData::AsTraitPath(as_trait_path) => { + self.find_in_as_trait_path(as_trait_path); + } + noirc_frontend::ast::UnresolvedTypeData::Expression(_) + | noirc_frontend::ast::UnresolvedTypeData::FormatString(_, _) + | noirc_frontend::ast::UnresolvedTypeData::String(_) + | noirc_frontend::ast::UnresolvedTypeData::Unspecified + | noirc_frontend::ast::UnresolvedTypeData::Quoted(_) + | noirc_frontend::ast::UnresolvedTypeData::FieldElement + | noirc_frontend::ast::UnresolvedTypeData::Integer(_, _) + | noirc_frontend::ast::UnresolvedTypeData::Bool + | noirc_frontend::ast::UnresolvedTypeData::Unit + | noirc_frontend::ast::UnresolvedTypeData::Resolved(_) + | noirc_frontend::ast::UnresolvedTypeData::Error => (), + } + } + + fn find_in_path(&mut self, path: &Path, requested_items: RequestedItems) { + // Only offer completions if we are right at the end of the path + if self.byte_index != path.span.end() as usize { + return; + } + + let after_colons = self.byte == Some(b':'); + + let mut idents: Vec = + path.segments.iter().map(|segment| segment.ident.clone()).collect(); + let prefix; + let at_root; + + if after_colons { + prefix = String::new(); + at_root = false; + } else { + prefix = idents.pop().unwrap().to_string(); + at_root = idents.is_empty(); + } + + let is_single_segment = !after_colons && idents.is_empty() && path.kind == PathKind::Plain; + + let module_id = + if idents.is_empty() { Some(self.module_id) } else { self.resolve_module(idents) }; + let Some(module_id) = module_id else { + return; + }; + + let module_completion_kind = if after_colons { + ModuleCompletionKind::DirectChildren + } else { + ModuleCompletionKind::AllVisibleItems + }; + let function_completion_kind = FunctionCompleteKind::NameAndParameters; + + self.complete_in_module( + module_id, + &prefix, + path.kind, + at_root, + module_completion_kind, + function_completion_kind, + requested_items, + ); + + if is_single_segment { + match requested_items { + RequestedItems::AnyItems => { + self.local_variables_completion(&prefix); + self.builtin_functions_completion(&prefix); + self.builtin_values_completion(&prefix); + } + RequestedItems::OnlyTypes => { + self.builtin_types_completion(&prefix); + self.type_parameters_completion(&prefix); + } + } + } + } + + fn local_variables_completion(&mut self, prefix: &str) { + for (name, span) in &self.local_variables { + if name_matches(name, prefix) { + let location = Location::new(*span, self.file); + let description = if let Some(ReferenceId::Local(definition_id)) = + self.interner.reference_at_location(location) + { + let typ = self.interner.definition_type(definition_id); + Some(typ.to_string()) + } else { + None + }; + + self.completion_items.push(simple_completion_item( + name, + CompletionItemKind::VARIABLE, + description, + )); + } + } + } + + fn type_parameters_completion(&mut self, prefix: &str) { + for name in &self.type_parameters { + if name_matches(name, prefix) { + self.completion_items.push(simple_completion_item( + name, + CompletionItemKind::TYPE_PARAMETER, + None, + )); + } + } + } + + fn find_in_use_tree(&mut self, use_tree: &UseTree, prefixes: &mut Vec) { + match &use_tree.kind { + UseTreeKind::Path(ident, alias) => { + prefixes.push(use_tree.prefix.clone()); + self.find_in_use_tree_path(prefixes, ident, alias); + prefixes.pop(); + } + UseTreeKind::List(use_trees) => { + prefixes.push(use_tree.prefix.clone()); + for use_tree in use_trees { + self.find_in_use_tree(use_tree, prefixes); + } + prefixes.pop(); + } + } + } + + fn find_in_use_tree_path( + &mut self, + prefixes: &Vec, + ident: &Ident, + alias: &Option, + ) { + if let Some(_alias) = alias { + // Won't handle completion if there's an alias (for now) + return; + } + + let after_colons = self.byte == Some(b':'); + let at_ident_end = self.byte_index == ident.span().end() as usize; + let at_ident_colons_end = + after_colons && self.byte_index - 2 == ident.span().end() as usize; + + if !(at_ident_end || at_ident_colons_end) { + return; + } + + let path_kind = prefixes[0].kind; + + let mut segments: Vec = Vec::new(); + for prefix in prefixes { + for segment in &prefix.segments { + segments.push(segment.ident.clone()); + } + } + + let module_completion_kind = ModuleCompletionKind::DirectChildren; + let function_completion_kind = FunctionCompleteKind::Name; + let requested_items = RequestedItems::AnyItems; + + if after_colons { + // We are right after "::" + segments.push(ident.clone()); + + if let Some(module_id) = self.resolve_module(segments) { + let prefix = String::new(); + let at_root = false; + self.complete_in_module( + module_id, + &prefix, + path_kind, + at_root, + module_completion_kind, + function_completion_kind, + requested_items, + ); + }; + } else { + // We are right after the last segment + let prefix = ident.to_string(); + if segments.is_empty() { + let at_root = true; + self.complete_in_module( + self.module_id, + &prefix, + path_kind, + at_root, + module_completion_kind, + function_completion_kind, + requested_items, + ); + } else if let Some(module_id) = self.resolve_module(segments) { + let at_root = false; + self.complete_in_module( + module_id, + &prefix, + path_kind, + at_root, + module_completion_kind, + function_completion_kind, + requested_items, + ); + } + } + } + + fn collect_local_variables(&mut self, pattern: &Pattern) { + match pattern { + Pattern::Identifier(ident) => { + self.local_variables.insert(ident.to_string(), ident.span()); + } + Pattern::Mutable(pattern, _, _) => self.collect_local_variables(pattern), + Pattern::Tuple(patterns, _) => { + for pattern in patterns { + self.collect_local_variables(pattern); + } + } + Pattern::Struct(_, patterns, _) => { + for (_, pattern) in patterns { + self.collect_local_variables(pattern); + } + } + } + } + + fn collect_type_parameters_in_generics(&mut self, generics: &UnresolvedGenerics) { + for generic in generics { + self.collect_type_parameters_in_generic(generic); + } + } + + fn collect_type_parameters_in_generic(&mut self, generic: &UnresolvedGeneric) { + match generic { + UnresolvedGeneric::Variable(ident) => { + self.type_parameters.insert(ident.to_string()); + } + UnresolvedGeneric::Numeric { ident, typ: _ } => { + self.type_parameters.insert(ident.to_string()); + } + UnresolvedGeneric::Resolved(..) => (), + }; + } + + #[allow(clippy::too_many_arguments)] + fn complete_in_module( + &mut self, + module_id: ModuleId, + prefix: &str, + path_kind: PathKind, + at_root: bool, + module_completion_kind: ModuleCompletionKind, + function_completion_kind: FunctionCompleteKind, + requested_items: RequestedItems, + ) { + let def_map = &self.def_maps[&module_id.krate]; + let Some(mut module_data) = def_map.modules().get(module_id.local_id.0) else { + return; + }; + + if at_root { + match path_kind { + PathKind::Crate => { + let Some(root_module_data) = def_map.modules().get(def_map.root().0) else { + return; + }; + module_data = root_module_data; + } + PathKind::Super => { + let Some(parent) = module_data.parent else { + return; + }; + let Some(parent_module_data) = def_map.modules().get(parent.0) else { + return; + }; + module_data = parent_module_data; + } + PathKind::Dep => (), + PathKind::Plain => (), + } + } + + let items = match module_completion_kind { + ModuleCompletionKind::DirectChildren => module_data.definitions(), + ModuleCompletionKind::AllVisibleItems => module_data.scope(), + }; + + for ident in items.names() { + let name = &ident.0.contents; + + if name_matches(name, prefix) { + let per_ns = module_data.find_name(ident); + if let Some((module_def_id, _, _)) = per_ns.types { + if let Some(completion_item) = self.module_def_id_completion_item( + module_def_id, + name.clone(), + function_completion_kind, + requested_items, + ) { + self.completion_items.push(completion_item); + } + } + + if let Some((module_def_id, _, _)) = per_ns.values { + if let Some(completion_item) = self.module_def_id_completion_item( + module_def_id, + name.clone(), + function_completion_kind, + requested_items, + ) { + self.completion_items.push(completion_item); + } + } + } + } + + if at_root && path_kind == PathKind::Plain { + for dependency in self.dependencies { + let dependency_name = dependency.as_name(); + if name_matches(&dependency_name, prefix) { + self.completion_items.push(crate_completion_item(dependency_name)); + } + } + + if name_matches("crate::", prefix) { + self.completion_items.push(simple_completion_item( + "crate::", + CompletionItemKind::KEYWORD, + None, + )); + } + + if module_data.parent.is_some() && name_matches("super::", prefix) { + self.completion_items.push(simple_completion_item( + "super::", + CompletionItemKind::KEYWORD, + None, + )); + } + } + } + + fn module_def_id_completion_item( + &self, + module_def_id: ModuleDefId, + name: String, + function_completion_kind: FunctionCompleteKind, + requested_items: RequestedItems, + ) -> Option { + match requested_items { + RequestedItems::OnlyTypes => match module_def_id { + ModuleDefId::FunctionId(_) | ModuleDefId::GlobalId(_) => return None, + ModuleDefId::ModuleId(_) + | ModuleDefId::TypeId(_) + | ModuleDefId::TypeAliasId(_) + | ModuleDefId::TraitId(_) => (), + }, + RequestedItems::AnyItems => (), + } + + let completion_item = match module_def_id { + ModuleDefId::ModuleId(_) => module_completion_item(name), + ModuleDefId::FunctionId(func_id) => { + self.function_completion_item(func_id, function_completion_kind) + } + ModuleDefId::TypeId(struct_id) => self.struct_completion_item(struct_id), + ModuleDefId::TypeAliasId(type_alias_id) => { + self.type_alias_completion_item(type_alias_id) + } + ModuleDefId::TraitId(trait_id) => self.trait_completion_item(trait_id), + ModuleDefId::GlobalId(global_id) => self.global_completion_item(global_id), + }; + Some(completion_item) + } + + fn function_completion_item( + &self, + func_id: FuncId, + function_completion_kind: FunctionCompleteKind, + ) -> CompletionItem { + let func_meta = self.interner.function_meta(&func_id); + let name = self.interner.function_name(&func_id).to_string(); + + let mut typ = &func_meta.typ; + if let Type::Forall(_, typ_) = typ { + typ = typ_; + } + let description = typ.to_string(); + let description = description.strip_suffix(" -> ()").unwrap_or(&description).to_string(); + + match function_completion_kind { + FunctionCompleteKind::Name => { + simple_completion_item(name, CompletionItemKind::FUNCTION, Some(description)) + } + FunctionCompleteKind::NameAndParameters => { + let label = format!("{}(…)", name); + let kind = CompletionItemKind::FUNCTION; + let insert_text = self.compute_function_insert_text(func_meta, &name); + + snippet_completion_item(label, kind, insert_text, Some(description)) + } + } + } + + fn compute_function_insert_text(&self, func_meta: &FuncMeta, name: &str) -> String { + let mut text = String::new(); + text.push_str(name); + text.push('('); + for (index, (pattern, _, _)) in func_meta.parameters.0.iter().enumerate() { + if index > 0 { + text.push_str(", "); + } + + text.push_str("${"); + text.push_str(&(index + 1).to_string()); + text.push(':'); + self.hir_pattern_to_argument(pattern, &mut text); + text.push('}'); + } + text.push(')'); + text + } + + fn hir_pattern_to_argument(&self, pattern: &HirPattern, text: &mut String) { + match pattern { + HirPattern::Identifier(hir_ident) => { + text.push_str(self.interner.definition_name(hir_ident.id)); + } + HirPattern::Mutable(pattern, _) => self.hir_pattern_to_argument(pattern, text), + HirPattern::Tuple(_, _) | HirPattern::Struct(_, _, _) => text.push('_'), + } + } + + fn struct_completion_item(&self, struct_id: StructId) -> CompletionItem { + let struct_type = self.interner.get_struct(struct_id); + let struct_type = struct_type.borrow(); + let name = struct_type.name.to_string(); + + simple_completion_item(name.clone(), CompletionItemKind::STRUCT, Some(name)) + } + + fn type_alias_completion_item(&self, type_alias_id: TypeAliasId) -> CompletionItem { + let type_alias = self.interner.get_type_alias(type_alias_id); + let type_alias = type_alias.borrow(); + let name = type_alias.name.to_string(); + + simple_completion_item(name.clone(), CompletionItemKind::STRUCT, Some(name)) + } + + fn trait_completion_item(&self, trait_id: TraitId) -> CompletionItem { + let trait_ = self.interner.get_trait(trait_id); + let name = trait_.name.to_string(); + + simple_completion_item(name.clone(), CompletionItemKind::INTERFACE, Some(name)) + } + + fn global_completion_item(&self, global_id: GlobalId) -> CompletionItem { + let global_definition = self.interner.get_global_definition(global_id); + let name = global_definition.name.clone(); + + let global = self.interner.get_global(global_id); + let typ = self.interner.definition_type(global.definition_id); + let description = typ.to_string(); + + simple_completion_item(name, CompletionItemKind::CONSTANT, Some(description)) + } + + fn resolve_module(&self, segments: Vec) -> Option { + if let Some(ModuleDefId::ModuleId(module_id)) = self.resolve_path(segments) { + Some(module_id) + } else { + None + } + } + + fn resolve_path(&self, segments: Vec) -> Option { + let path_segments = segments.into_iter().map(PathSegment::from).collect(); + let path = Path { segments: path_segments, kind: PathKind::Plain, span: Span::default() }; + + let path_resolver = StandardPathResolver::new(self.root_module_id); + match path_resolver.resolve(self.def_maps, path, &mut None) { + Ok(path_resolution) => Some(path_resolution.module_def_id), + Err(_) => None, + } + } + + fn builtin_functions_completion(&mut self, prefix: &str) { + for keyword in Keyword::iter() { + if let Some(func) = keyword_builtin_function(&keyword) { + if name_matches(func.name, prefix) { + self.completion_items.push(snippet_completion_item( + format!("{}(…)", func.name), + CompletionItemKind::FUNCTION, + format!("{}({})", func.name, func.parameters), + Some(func.description.to_string()), + )); + } + } + } + } + + fn builtin_values_completion(&mut self, prefix: &str) { + for keyword in ["false", "true"] { + if name_matches(keyword, prefix) { + self.completion_items.push(simple_completion_item( + keyword, + CompletionItemKind::KEYWORD, + Some("bool".to_string()), + )); + } + } + } + + fn builtin_types_completion(&mut self, prefix: &str) { + for keyword in Keyword::iter() { + if let Some(typ) = keyword_builtin_type(&keyword) { + if name_matches(typ, prefix) { + self.completion_items.push(simple_completion_item( + typ, + CompletionItemKind::STRUCT, + Some(typ.to_string()), + )); + } + } + } + + for typ in builtin_integer_types() { + if name_matches(typ, prefix) { + self.completion_items.push(simple_completion_item( + typ, + CompletionItemKind::STRUCT, + Some(typ.to_string()), + )); + } + } + } + + fn includes_span(&self, span: Span) -> bool { + span.start() as usize <= self.byte_index && self.byte_index <= span.end() as usize + } +} + +fn name_matches(name: &str, prefix: &str) -> bool { + name.starts_with(prefix) +} + +fn module_completion_item(name: impl Into) -> CompletionItem { + simple_completion_item(name, CompletionItemKind::MODULE, None) +} + +fn crate_completion_item(name: impl Into) -> CompletionItem { + simple_completion_item(name, CompletionItemKind::MODULE, None) +} + +fn simple_completion_item( + label: impl Into, + kind: CompletionItemKind, + description: Option, +) -> CompletionItem { + CompletionItem { + label: label.into(), + label_details: Some(CompletionItemLabelDetails { detail: None, description }), + kind: Some(kind), + detail: None, + documentation: None, + deprecated: None, + preselect: None, + sort_text: None, + filter_text: None, + insert_text: None, + insert_text_format: None, + insert_text_mode: None, + text_edit: None, + additional_text_edits: None, + command: None, + commit_characters: None, + data: None, + tags: None, + } +} + +fn snippet_completion_item( + label: impl Into, + kind: CompletionItemKind, + insert_text: impl Into, + description: Option, +) -> CompletionItem { + CompletionItem { + label: label.into(), + label_details: Some(CompletionItemLabelDetails { detail: None, description }), + kind: Some(kind), + insert_text_format: Some(InsertTextFormat::SNIPPET), + insert_text: Some(insert_text.into()), + detail: None, + documentation: None, + deprecated: None, + preselect: None, + sort_text: None, + filter_text: None, + insert_text_mode: None, + text_edit: None, + additional_text_edits: None, + command: None, + commit_characters: None, + data: None, + tags: None, + } +} + +#[cfg(test)] +mod completion_tests { + use crate::{notifications::on_did_open_text_document, test_utils}; + + use super::*; + use lsp_types::{ + DidOpenTextDocumentParams, PartialResultParams, Position, TextDocumentIdentifier, + TextDocumentItem, TextDocumentPositionParams, WorkDoneProgressParams, + }; + use tokio::test; + + async fn assert_completion(src: &str, expected: Vec) { + let (mut state, noir_text_document) = test_utils::init_lsp_server("document_symbol").await; + + let (line, column) = src + .lines() + .enumerate() + .filter_map(|(line_index, line)| { + line.find(">|<").map(|char_index| (line_index, char_index)) + }) + .next() + .expect("Expected to find one >|< in the source code"); + + let src = src.replace(">|<", ""); + + on_did_open_text_document( + &mut state, + DidOpenTextDocumentParams { + text_document: TextDocumentItem { + uri: noir_text_document.clone(), + language_id: "noir".to_string(), + version: 0, + text: src.to_string(), + }, + }, + ); + + // Get inlay hints. These should now be relative to the changed text, + // not the saved file's text. + let response = on_completion_request( + &mut state, + CompletionParams { + text_document_position: TextDocumentPositionParams { + text_document: TextDocumentIdentifier { uri: noir_text_document }, + position: Position { line: line as u32, character: column as u32 }, + }, + work_done_progress_params: WorkDoneProgressParams { work_done_token: None }, + partial_result_params: PartialResultParams { partial_result_token: None }, + context: None, + }, + ) + .await + .expect("Could not execute on_completion_request") + .unwrap(); + + let CompletionResponse::Array(items) = response else { + panic!("Expected response to be CompletionResponse::Array"); + }; + + let mut items = items.clone(); + items.sort_by_key(|item| item.label.clone()); + + let mut expected = expected.clone(); + expected.sort_by_key(|item| item.label.clone()); + + if items != expected { + println!( + "Items: {:?}", + items.iter().map(|item| item.label.clone()).collect::>() + ); + println!( + "Expected: {:?}", + expected.iter().map(|item| item.label.clone()).collect::>() + ); + } + + assert_eq!(items, expected); + } + + #[test] + async fn test_use_first_segment() { + let src = r#" + mod foo {} + mod foobar {} + use f>|< + "#; + + assert_completion( + src, + vec![module_completion_item("foo"), module_completion_item("foobar")], + ) + .await; + } + + #[test] + async fn test_use_second_segment() { + let src = r#" + mod foo { + mod bar {} + mod baz {} + } + use foo::>|< + "#; + + assert_completion(src, vec![module_completion_item("bar"), module_completion_item("baz")]) + .await; + } + + #[test] + async fn test_use_second_segment_after_typing() { + let src = r#" + mod foo { + mod bar {} + mod brave {} + } + use foo::ba>|< + "#; + + assert_completion(src, vec![module_completion_item("bar")]).await; + } + + #[test] + async fn test_use_struct() { + let src = r#" + mod foo { + struct Foo {} + } + use foo::>|< + "#; + + assert_completion( + src, + vec![simple_completion_item( + "Foo", + CompletionItemKind::STRUCT, + Some("Foo".to_string()), + )], + ) + .await; + } + + #[test] + async fn test_use_function() { + let src = r#" + mod foo { + fn bar(x: i32) -> u64 { 0 } + } + use foo::>|< + "#; + + assert_completion( + src, + vec![simple_completion_item( + "bar", + CompletionItemKind::FUNCTION, + Some("fn(i32) -> u64".to_string()), + )], + ) + .await; + } + + #[test] + async fn test_use_after_crate_and_letter() { + // Prove that "std" shows up + let src = r#" + use s>|< + "#; + assert_completion(src, vec![crate_completion_item("std")]).await; + + // "std" doesn't show up anymore because of the "crate::" prefix + let src = r#" + mod something {} + use crate::s>|< + "#; + assert_completion(src, vec![module_completion_item("something")]).await; + } + + #[test] + async fn test_use_suggests_hardcoded_crate() { + let src = r#" + use c>|< + "#; + + assert_completion( + src, + vec![simple_completion_item("crate::", CompletionItemKind::KEYWORD, None)], + ) + .await; + } + + #[test] + async fn test_use_in_tree_after_letter() { + let src = r#" + mod foo { + mod bar {} + } + use foo::{b>|<} + "#; + + assert_completion(src, vec![module_completion_item("bar")]).await; + } + + #[test] + async fn test_use_in_tree_after_colons() { + let src = r#" + mod foo { + mod bar { + mod baz {} + } + } + use foo::{bar::>|<} + "#; + + assert_completion(src, vec![module_completion_item("baz")]).await; + } + + #[test] + async fn test_use_in_tree_after_colons_after_another_segment() { + let src = r#" + mod foo { + mod bar {} + mod qux {} + } + use foo::{bar, q>|<} + "#; + + assert_completion(src, vec![module_completion_item("qux")]).await; + } + + #[test] + async fn test_use_in_nested_module() { + let src = r#" + mod foo { + mod something {} + + use s>|< + } + "#; + + assert_completion( + src, + vec![ + module_completion_item("something"), + crate_completion_item("std"), + simple_completion_item("super::", CompletionItemKind::KEYWORD, None), + ], + ) + .await; + } + + #[test] + async fn test_use_after_super() { + let src = r#" + mod foo {} + + mod bar { + mod something {} + + use super::f>|< + } + "#; + + assert_completion(src, vec![module_completion_item("foo")]).await; + } + + #[test] + async fn test_use_after_crate_and_letter_nested_in_module() { + let src = r#" + mod something { + mod something_else {} + use crate::s>|< + } + + "#; + assert_completion(src, vec![module_completion_item("something")]).await; + } + + #[test] + async fn test_use_after_crate_segment_and_letter_nested_in_module() { + let src = r#" + mod something { + mod something_else {} + use crate::something::s>|< + } + + "#; + assert_completion(src, vec![module_completion_item("something_else")]).await; + } + + #[test] + async fn test_complete_path_shows_module() { + let src = r#" + mod foobar {} + + fn main() { + fo>|< + } + "#; + assert_completion(src, vec![module_completion_item("foobar")]).await; + } + + #[test] + async fn test_complete_path_after_colons_shows_submodule() { + let src = r#" + mod foo { + mod bar {} + } + + fn main() { + foo::>|< + } + "#; + assert_completion(src, vec![module_completion_item("bar")]).await; + } + + #[test] + async fn test_complete_path_after_colons_and_letter_shows_submodule() { + let src = r#" + mod foo { + mod bar {} + } + + fn main() { + foo::b>|< + } + "#; + assert_completion(src, vec![module_completion_item("bar")]).await; + } + + #[test] + async fn test_complete_path_with_local_variable() { + let src = r#" + fn main() { + let local = 1; + l>|< + } + "#; + assert_completion( + src, + vec![simple_completion_item( + "local", + CompletionItemKind::VARIABLE, + Some("Field".to_string()), + )], + ) + .await; + } + + #[test] + async fn test_complete_path_with_shadowed_local_variable() { + let src = r#" + fn main() { + let local = 1; + let local = true; + l>|< + } + "#; + assert_completion( + src, + vec![simple_completion_item( + "local", + CompletionItemKind::VARIABLE, + Some("bool".to_string()), + )], + ) + .await; + } + + #[test] + async fn test_complete_path_with_function_argument() { + let src = r#" + fn main(local: Field) { + l>|< + } + "#; + assert_completion( + src, + vec![simple_completion_item( + "local", + CompletionItemKind::VARIABLE, + Some("Field".to_string()), + )], + ) + .await; + } + + #[test] + async fn test_complete_function() { + let src = r#" + fn hello(x: i32, y: Field) { } + + fn main() { + h>|< + } + "#; + assert_completion( + src, + vec![snippet_completion_item( + "hello(…)", + CompletionItemKind::FUNCTION, + "hello(${1:x}, ${2:y})", + Some("fn(i32, Field)".to_string()), + )], + ) + .await; + } + + #[test] + async fn test_complete_builtin_functions() { + let src = r#" + fn main() { + a>|< + } + "#; + assert_completion( + src, + vec![ + snippet_completion_item( + "assert(…)", + CompletionItemKind::FUNCTION, + "assert(${1:predicate})", + Some("fn(T)".to_string()), + ), + snippet_completion_item( + "assert_constant(…)", + CompletionItemKind::FUNCTION, + "assert_constant(${1:x})", + Some("fn(T)".to_string()), + ), + snippet_completion_item( + "assert_eq(…)", + CompletionItemKind::FUNCTION, + "assert_eq(${1:lhs}, ${2:rhs})", + Some("fn(T, T)".to_string()), + ), + ], + ) + .await; + } + + #[test] + async fn test_complete_path_in_impl() { + let src = r#" + struct SomeStruct {} + + impl SomeStruct { + fn foo() { + S>|< + } + } + "#; + assert_completion( + src, + vec![simple_completion_item( + "SomeStruct", + CompletionItemKind::STRUCT, + Some("SomeStruct".to_string()), + )], + ) + .await; + } + + #[test] + async fn test_complete_path_in_trait_impl() { + let src = r#" + struct SomeStruct {} + trait Trait {} + + impl Trait for SomeStruct { + fn foo() { + S>|< + } + } + "#; + assert_completion( + src, + vec![simple_completion_item( + "SomeStruct", + CompletionItemKind::STRUCT, + Some("SomeStruct".to_string()), + )], + ) + .await; + } + + #[test] + async fn test_complete_path_with_for_argument() { + let src = r#" + fn main() { + for index in 0..10 { + i>|< + } + } + "#; + assert_completion( + src, + vec![simple_completion_item( + "index", + CompletionItemKind::VARIABLE, + Some("u32".to_string()), + )], + ) + .await; + } + + #[test] + async fn test_complete_path_with_lambda_argument() { + let src = r#" + fn lambda(f: fn(i32)) { } + + fn main() { + lambda(|var| v>|<) + } + "#; + assert_completion( + src, + vec![simple_completion_item( + "var", + CompletionItemKind::VARIABLE, + Some("_".to_string()), + )], + ) + .await; + } + + #[test] + async fn test_suggest_type_in_struct_field_type() { + let src = r#" + struct Something {} + + fn SomeFunction() {} + + struct Another { + some: So>|< + } + "#; + assert_completion( + src, + vec![simple_completion_item( + "Something", + CompletionItemKind::STRUCT, + Some("Something".to_string()), + )], + ) + .await; + } + + #[test] + async fn test_suggest_type_in_function_parameter() { + let src = r#" + struct Something {} + + fn foo(x: So>|<) {} + "#; + assert_completion( + src, + vec![simple_completion_item( + "Something", + CompletionItemKind::STRUCT, + Some("Something".to_string()), + )], + ) + .await; + } + + #[test] + async fn test_suggest_type_in_function_return_type() { + let src = r#" + struct Something {} + + fn foo() -> So>|< {} + "#; + assert_completion( + src, + vec![simple_completion_item( + "Something", + CompletionItemKind::STRUCT, + Some("Something".to_string()), + )], + ) + .await; + } + + #[test] + async fn test_suggest_type_in_type_alias() { + let src = r#" + struct Something {} + + type Foo = So>|< + "#; + assert_completion( + src, + vec![simple_completion_item( + "Something", + CompletionItemKind::STRUCT, + Some("Something".to_string()), + )], + ) + .await; + } + + #[test] + async fn test_suggest_type_in_trait_function() { + let src = r#" + struct Something {} + + trait Trait { + fn foo(s: So>|<); + } + "#; + assert_completion( + src, + vec![simple_completion_item( + "Something", + CompletionItemKind::STRUCT, + Some("Something".to_string()), + )], + ) + .await; + } + + #[test] + async fn test_suggest_type_in_trait_function_return_type() { + let src = r#" + struct Something {} + + trait Trait { + fn foo() -> So>|<; + } + "#; + assert_completion( + src, + vec![simple_completion_item( + "Something", + CompletionItemKind::STRUCT, + Some("Something".to_string()), + )], + ) + .await; + } + + #[test] + async fn test_suggest_type_in_let_type() { + let src = r#" + struct Something {} + + fn main() { + let x: So>|< + } + "#; + assert_completion( + src, + vec![simple_completion_item( + "Something", + CompletionItemKind::STRUCT, + Some("Something".to_string()), + )], + ) + .await; + } + + #[test] + async fn test_suggest_type_in_lambda_parameter() { + let src = r#" + struct Something {} + + fn main() { + foo(|s: So>|<| s) + } + "#; + assert_completion( + src, + vec![simple_completion_item( + "Something", + CompletionItemKind::STRUCT, + Some("Something".to_string()), + )], + ) + .await; + } + + #[test] + async fn test_suggest_builtin_types() { + let src = r#" + fn foo(x: i>|<) {} + "#; + assert_completion( + src, + vec![ + simple_completion_item("i8", CompletionItemKind::STRUCT, Some("i8".to_string())), + simple_completion_item("i16", CompletionItemKind::STRUCT, Some("i16".to_string())), + simple_completion_item("i32", CompletionItemKind::STRUCT, Some("i32".to_string())), + simple_completion_item("i64", CompletionItemKind::STRUCT, Some("i64".to_string())), + ], + ) + .await; + } + + #[test] + async fn test_suggest_true() { + let src = r#" + fn main() { + let x = t>|< + } + "#; + assert_completion( + src, + vec![simple_completion_item( + "true", + CompletionItemKind::KEYWORD, + Some("bool".to_string()), + )], + ) + .await; + } + + #[test] + async fn test_suggest_regarding_if_scope() { + let src = r#" + fn main() { + let good = 1; + if true { + let great = 2; + g>|< + } else { + let greater = 3; + } + } + "#; + assert_completion( + src, + vec![ + simple_completion_item( + "good", + CompletionItemKind::VARIABLE, + Some("Field".to_string()), + ), + simple_completion_item( + "great", + CompletionItemKind::VARIABLE, + Some("Field".to_string()), + ), + ], + ) + .await; + + let src = r#" + fn main() { + let good = 1; + if true { + let great = 2; + } else { + let greater = 3; + g>|< + } + } + "#; + assert_completion( + src, + vec![ + simple_completion_item( + "good", + CompletionItemKind::VARIABLE, + Some("Field".to_string()), + ), + simple_completion_item( + "greater", + CompletionItemKind::VARIABLE, + Some("Field".to_string()), + ), + ], + ) + .await; + + let src = r#" + fn main() { + let good = 1; + if true { + let great = 2; + } else { + let greater = 3; + } + g>|< + } + "#; + assert_completion( + src, + vec![simple_completion_item( + "good", + CompletionItemKind::VARIABLE, + Some("Field".to_string()), + )], + ) + .await; + } + + #[test] + async fn test_suggest_regarding_block_scope() { + let src = r#" + fn main() { + let good = 1; + { + let great = 2; + g>|< + } + } + "#; + assert_completion( + src, + vec![ + simple_completion_item( + "good", + CompletionItemKind::VARIABLE, + Some("Field".to_string()), + ), + simple_completion_item( + "great", + CompletionItemKind::VARIABLE, + Some("Field".to_string()), + ), + ], + ) + .await; + + let src = r#" + fn main() { + let good = 1; + { + let great = 2; + } + g>|< + } + "#; + assert_completion( + src, + vec![simple_completion_item( + "good", + CompletionItemKind::VARIABLE, + Some("Field".to_string()), + )], + ) + .await; + } + + #[test] + async fn test_suggest_struct_type_parameter() { + let src = r#" + struct Foo { + context: C>|< + } + "#; + assert_completion( + src, + vec![simple_completion_item("Context", CompletionItemKind::TYPE_PARAMETER, None)], + ) + .await; + } + + #[test] + async fn test_suggest_impl_type_parameter() { + let src = r#" + struct Foo {} + + impl Foo { + fn foo() { + let x: TypeP>|< + } + } + "#; + assert_completion( + src, + vec![simple_completion_item("TypeParam", CompletionItemKind::TYPE_PARAMETER, None)], + ) + .await; + } + + #[test] + async fn test_suggest_trait_impl_type_parameter() { + let src = r#" + struct Foo {} + trait Trait {} + + impl Trait for Foo { + fn foo() { + let x: TypeP>|< + } + } + "#; + assert_completion( + src, + vec![simple_completion_item("TypeParam", CompletionItemKind::TYPE_PARAMETER, None)], + ) + .await; + } + + #[test] + async fn test_suggest_trait_function_type_parameter() { + let src = r#" + struct Foo {} + trait Trait { + fn foo() { + let x: TypeP>|< + } + } + "#; + assert_completion( + src, + vec![simple_completion_item("TypeParam", CompletionItemKind::TYPE_PARAMETER, None)], + ) + .await; + } + + #[test] + async fn test_suggest_function_type_parameters() { + let src = r#" + fn foo(x: C>|<) {} + "#; + assert_completion( + src, + vec![simple_completion_item("Context", CompletionItemKind::TYPE_PARAMETER, None)], + ) + .await; + } +} diff --git a/noir/noir-repo/tooling/lsp/src/requests/completion/builtins.rs b/noir/noir-repo/tooling/lsp/src/requests/completion/builtins.rs new file mode 100644 index 00000000000..070be109f13 --- /dev/null +++ b/noir/noir-repo/tooling/lsp/src/requests/completion/builtins.rs @@ -0,0 +1,127 @@ +use noirc_frontend::token::Keyword; + +pub(super) fn builtin_integer_types() -> [&'static str; 8] { + ["i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64"] +} + +/// If a keyword corresponds to a built-in type, returns that type's name. +pub(super) fn keyword_builtin_type(keyword: &Keyword) -> Option<&'static str> { + match keyword { + Keyword::Bool => Some("bool"), + Keyword::Expr => Some("Expr"), + Keyword::Field => Some("Field"), + Keyword::FunctionDefinition => Some("FunctionDefinition"), + Keyword::StructDefinition => Some("StructDefinition"), + Keyword::TraitConstraint => Some("TraitConstraint"), + Keyword::TraitDefinition => Some("TraitDefinition"), + Keyword::TypeType => Some("Type"), + + Keyword::As + | Keyword::Assert + | Keyword::AssertEq + | Keyword::Break + | Keyword::CallData + | Keyword::Char + | Keyword::Comptime + | Keyword::Constrain + | Keyword::Continue + | Keyword::Contract + | Keyword::Crate + | Keyword::Dep + | Keyword::Else + | Keyword::Fn + | Keyword::For + | Keyword::FormatString + | Keyword::Global + | Keyword::If + | Keyword::Impl + | Keyword::In + | Keyword::Let + | Keyword::Mod + | Keyword::Module + | Keyword::Mut + | Keyword::Pub + | Keyword::Quoted + | Keyword::Return + | Keyword::ReturnData + | Keyword::String + | Keyword::Struct + | Keyword::Super + | Keyword::TopLevelItem + | Keyword::Trait + | Keyword::Type + | Keyword::Unchecked + | Keyword::Unconstrained + | Keyword::Use + | Keyword::Where + | Keyword::While => None, + } +} + +pub(super) struct BuiltInFunction { + pub(super) name: &'static str, + pub(super) parameters: &'static str, + pub(super) description: &'static str, +} + +/// If a keyword corresponds to a built-in function, returns info about it +pub(super) fn keyword_builtin_function(keyword: &Keyword) -> Option { + match keyword { + Keyword::Assert => Some(BuiltInFunction { + name: "assert", + parameters: "${1:predicate}", + description: "fn(T)", + }), + Keyword::AssertEq => Some(BuiltInFunction { + name: "assert_eq", + parameters: "${1:lhs}, ${2:rhs}", + description: "fn(T, T)", + }), + + Keyword::As + | Keyword::Bool + | Keyword::Break + | Keyword::CallData + | Keyword::Char + | Keyword::Comptime + | Keyword::Constrain + | Keyword::Continue + | Keyword::Contract + | Keyword::Crate + | Keyword::Dep + | Keyword::Else + | Keyword::Expr + | Keyword::Field + | Keyword::Fn + | Keyword::For + | Keyword::FormatString + | Keyword::FunctionDefinition + | Keyword::Global + | Keyword::If + | Keyword::Impl + | Keyword::In + | Keyword::Let + | Keyword::Mod + | Keyword::Module + | Keyword::Mut + | Keyword::Pub + | Keyword::Quoted + | Keyword::Return + | Keyword::ReturnData + | Keyword::String + | Keyword::Struct + | Keyword::StructDefinition + | Keyword::Super + | Keyword::TopLevelItem + | Keyword::Trait + | Keyword::TraitConstraint + | Keyword::TraitDefinition + | Keyword::Type + | Keyword::TypeType + | Keyword::Unchecked + | Keyword::Unconstrained + | Keyword::Use + | Keyword::Where + | Keyword::While => None, + } +} diff --git a/noir/noir-repo/tooling/lsp/src/requests/hover.rs b/noir/noir-repo/tooling/lsp/src/requests/hover.rs index 73ea504b496..b6fdc6f7842 100644 --- a/noir/noir-repo/tooling/lsp/src/requests/hover.rs +++ b/noir/noir-repo/tooling/lsp/src/requests/hover.rs @@ -321,9 +321,9 @@ fn format_parent_module_from_module_id( ) -> bool { let crate_id = module.krate; let crate_name = match crate_id { - CrateId::Root(_) => Some(args.root_crate_name.clone()), + CrateId::Root(_) => Some(args.crate_name.clone()), CrateId::Crate(_) => args - .root_crate_dependencies + .dependencies .iter() .find(|dep| dep.crate_id == crate_id) .map(|dep| format!("{}", dep.name)), diff --git a/noir/noir-repo/tooling/lsp/src/requests/inlay_hint.rs b/noir/noir-repo/tooling/lsp/src/requests/inlay_hint.rs index bc7567b3237..8c3d8a05652 100644 --- a/noir/noir-repo/tooling/lsp/src/requests/inlay_hint.rs +++ b/noir/noir-repo/tooling/lsp/src/requests/inlay_hint.rs @@ -1,4 +1,3 @@ -use fm::codespan_files::Files; use std::future::{self, Future}; use async_lsp::ResponseError; @@ -21,7 +20,7 @@ use noirc_frontend::{ ParsedModule, Type, TypeBinding, TypeVariable, TypeVariableKind, }; -use crate::LspState; +use crate::{utils, LspState}; use super::{process_request, to_lsp_location, InlayHintsOptions}; @@ -43,7 +42,7 @@ pub(crate) fn on_inlay_hint_request( let source = file.source(); let (parsed_moduled, _errors) = noirc_frontend::parse_program(source); - let span = range_to_byte_span(args.files, file_id, ¶ms.range) + let span = utils::range_to_byte_span(args.files, file_id, ¶ms.range) .map(|range| Span::from(range.start as u32..range.end as u32)); let mut collector = @@ -691,63 +690,6 @@ fn get_expression_name(expression: &Expression) -> Option { } } -// These functions are copied from the codespan_lsp crate, except that they never panic -// (the library will sometimes panic, so functions returning Result are not always accurate) - -fn range_to_byte_span( - files: &FileMap, - file_id: FileId, - range: &lsp_types::Range, -) -> Option> { - Some( - position_to_byte_index(files, file_id, &range.start)? - ..position_to_byte_index(files, file_id, &range.end)?, - ) -} - -fn position_to_byte_index( - files: &FileMap, - file_id: FileId, - position: &lsp_types::Position, -) -> Option { - let Ok(source) = files.source(file_id) else { - return None; - }; - - let Ok(line_span) = files.line_range(file_id, position.line as usize) else { - return None; - }; - let line_str = source.get(line_span.clone())?; - - let byte_offset = character_to_line_offset(line_str, position.character)?; - - Some(line_span.start + byte_offset) -} - -fn character_to_line_offset(line: &str, character: u32) -> Option { - let line_len = line.len(); - let mut character_offset = 0; - - let mut chars = line.chars(); - while let Some(ch) = chars.next() { - if character_offset == character { - let chars_off = chars.as_str().len(); - let ch_off = ch.len_utf8(); - - return Some(line_len - chars_off - ch_off); - } - - character_offset += ch.len_utf16() as u32; - } - - // Handle positions after the last character on the line - if character_offset == character { - Some(line_len) - } else { - None - } -} - #[cfg(test)] mod inlay_hints_tests { use crate::{ diff --git a/noir/noir-repo/tooling/lsp/src/requests/mod.rs b/noir/noir-repo/tooling/lsp/src/requests/mod.rs index aef5d782c46..e138f839600 100644 --- a/noir/noir-repo/tooling/lsp/src/requests/mod.rs +++ b/noir/noir-repo/tooling/lsp/src/requests/mod.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use std::path::PathBuf; use std::{collections::HashMap, future::Future}; @@ -15,6 +16,8 @@ use lsp_types::{ }; use nargo_fmt::Config; use noirc_driver::file_manager_with_stdlib; +use noirc_frontend::graph::CrateId; +use noirc_frontend::hir::def_map::CrateDefMap; use noirc_frontend::{graph::Dependency, macros_api::NodeInterner}; use serde::{Deserialize, Serialize}; @@ -34,6 +37,7 @@ use crate::{ // and params passed in. mod code_lens_request; +mod completion; mod document_symbol; mod goto_declaration; mod goto_definition; @@ -47,12 +51,12 @@ mod tests; pub(crate) use { code_lens_request::collect_lenses_for_package, code_lens_request::on_code_lens_request, - document_symbol::on_document_symbol_request, goto_declaration::on_goto_declaration_request, - goto_definition::on_goto_definition_request, goto_definition::on_goto_type_definition_request, - hover::on_hover_request, inlay_hint::on_inlay_hint_request, - profile_run::on_profile_run_request, references::on_references_request, - rename::on_prepare_rename_request, rename::on_rename_request, test_run::on_test_run_request, - tests::on_tests_request, + completion::on_completion_request, document_symbol::on_document_symbol_request, + goto_declaration::on_goto_declaration_request, goto_definition::on_goto_definition_request, + goto_definition::on_goto_type_definition_request, hover::on_hover_request, + inlay_hint::on_inlay_hint_request, profile_run::on_profile_run_request, + references::on_references_request, rename::on_prepare_rename_request, + rename::on_rename_request, test_run::on_test_run_request, tests::on_tests_request, }; /// LSP client will send initialization request after the server has started. @@ -228,6 +232,15 @@ pub(crate) fn on_initialize( label: Some("Noir".to_string()), }, )), + completion_provider: Some(lsp_types::OneOf::Right(lsp_types::CompletionOptions { + resolve_provider: None, + trigger_characters: Some(vec![":".to_string()]), + all_commit_characters: None, + work_done_progress_options: WorkDoneProgressOptions { + work_done_progress: None, + }, + completion_item: None, + })), }, server_info: None, }) @@ -375,8 +388,10 @@ pub(crate) struct ProcessRequestCallbackArgs<'a> { files: &'a FileMap, interner: &'a NodeInterner, interners: &'a HashMap, - root_crate_name: String, - root_crate_dependencies: &'a Vec, + crate_id: CrateId, + crate_name: String, + dependencies: &'a Vec, + def_maps: &'a BTreeMap, } pub(crate) fn process_request( @@ -411,12 +426,15 @@ where crate::prepare_package(&workspace_file_manager, &parsed_files, package); let interner; + let def_maps; if let Some(def_interner) = state.cached_definitions.get(&package_root_path) { interner = def_interner; + def_maps = state.cached_def_maps.get(&package_root_path).unwrap(); } else { // We ignore the warnings and errors produced by compilation while resolving the definition let _ = noirc_driver::check_crate(&mut context, crate_id, &Default::default()); interner = &context.def_interner; + def_maps = &context.def_maps; } let files = context.file_manager.as_file_map(); @@ -432,8 +450,10 @@ where files, interner, interners: &state.cached_definitions, - root_crate_name: package.name.to_string(), - root_crate_dependencies: &context.crate_graph[context.root_crate_id()].dependencies, + crate_id, + crate_name: package.name.to_string(), + dependencies: &context.crate_graph[context.root_crate_id()].dependencies, + def_maps, })) } pub(crate) fn find_all_references_in_workspace( diff --git a/noir/noir-repo/tooling/lsp/src/types.rs b/noir/noir-repo/tooling/lsp/src/types.rs index fa3234cf3bb..5afda0d292a 100644 --- a/noir/noir-repo/tooling/lsp/src/types.rs +++ b/noir/noir-repo/tooling/lsp/src/types.rs @@ -1,7 +1,8 @@ use fm::FileId; use lsp_types::{ - DeclarationCapability, DefinitionOptions, DocumentSymbolOptions, HoverOptions, - InlayHintOptions, OneOf, ReferencesOptions, RenameOptions, TypeDefinitionProviderCapability, + CompletionOptions, DeclarationCapability, DefinitionOptions, DocumentSymbolOptions, + HoverOptions, InlayHintOptions, OneOf, ReferencesOptions, RenameOptions, + TypeDefinitionProviderCapability, }; use noirc_driver::DebugFile; use noirc_errors::{debug_info::OpCodesCount, Location}; @@ -156,6 +157,10 @@ pub(crate) struct ServerCapabilities { /// The server provides document symbol support. #[serde(skip_serializing_if = "Option::is_none")] pub(crate) document_symbol_provider: Option>, + + /// The server provides completion support. + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) completion_provider: Option>, } #[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)] diff --git a/noir/noir-repo/tooling/lsp/src/utils.rs b/noir/noir-repo/tooling/lsp/src/utils.rs new file mode 100644 index 00000000000..96db1f7bfa2 --- /dev/null +++ b/noir/noir-repo/tooling/lsp/src/utils.rs @@ -0,0 +1,59 @@ +// These functions are copied from the codespan_lsp crate, except that they never panic +// (the library will sometimes panic, so functions returning Result are not always accurate) + +use fm::codespan_files::Files; +use fm::{FileId, FileMap}; + +pub(crate) fn range_to_byte_span( + files: &FileMap, + file_id: FileId, + range: &lsp_types::Range, +) -> Option> { + Some( + position_to_byte_index(files, file_id, &range.start)? + ..position_to_byte_index(files, file_id, &range.end)?, + ) +} + +pub(crate) fn position_to_byte_index( + files: &FileMap, + file_id: FileId, + position: &lsp_types::Position, +) -> Option { + let Ok(source) = files.source(file_id) else { + return None; + }; + + let Ok(line_span) = files.line_range(file_id, position.line as usize) else { + return None; + }; + let line_str = source.get(line_span.clone())?; + + let byte_offset = character_to_line_offset(line_str, position.character)?; + + Some(line_span.start + byte_offset) +} + +pub(crate) fn character_to_line_offset(line: &str, character: u32) -> Option { + let line_len = line.len(); + let mut character_offset = 0; + + let mut chars = line.chars(); + while let Some(ch) = chars.next() { + if character_offset == character { + let chars_off = chars.as_str().len(); + let ch_off = ch.len_utf8(); + + return Some(line_len - chars_off - ch_off); + } + + character_offset += ch.len_utf16() as u32; + } + + // Handle positions after the last character on the line + if character_offset == character { + Some(line_len) + } else { + None + } +} diff --git a/noir/noir-repo/tooling/nargo/Cargo.toml b/noir/noir-repo/tooling/nargo/Cargo.toml index b0cf1cfcbb1..56e88dacf2d 100644 --- a/noir/noir-repo/tooling/nargo/Cargo.toml +++ b/noir/noir-repo/tooling/nargo/Cargo.toml @@ -25,6 +25,10 @@ jsonrpc.workspace = true rand.workspace = true serde.workspace = true +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +noir_fuzzer.workspace = true +proptest.workspace = true + [dev-dependencies] # TODO: This dependency is used to generate unit tests for `get_all_paths_in_dir` # TODO: once that method is moved to nargo_cli, we can move this dependency to nargo_cli diff --git a/noir/noir-repo/tooling/nargo/src/ops/test.rs b/noir/noir-repo/tooling/nargo/src/ops/test.rs index b2af5d9780b..efe648e09b0 100644 --- a/noir/noir-repo/tooling/nargo/src/ops/test.rs +++ b/noir/noir-repo/tooling/nargo/src/ops/test.rs @@ -36,28 +36,82 @@ pub fn run_test>( package_name: Option, config: &CompileOptions, ) -> TestStatus { - let compiled_program = compile_no_check(context, config, test_function.get_id(), None, false); - match compiled_program { + let test_function_has_no_arguments = context + .def_interner + .function_meta(&test_function.get_id()) + .function_signature() + .0 + .is_empty(); + + match compile_no_check(context, config, test_function.get_id(), None, false) { Ok(compiled_program) => { - // Run the backend to ensure the PWG evaluates functions like std::hash::pedersen, - // otherwise constraints involving these expressions will not error. - let circuit_execution = execute_program( - &compiled_program.program, - WitnessMap::new(), - blackbox_solver, - &mut DefaultForeignCallExecutor::new( - show_output, - foreign_call_resolver_url, - root_path, - package_name, - ), - ); - test_status_program_compile_pass( - test_function, - compiled_program.abi, - compiled_program.debug, - circuit_execution, - ) + if test_function_has_no_arguments { + // Run the backend to ensure the PWG evaluates functions like std::hash::pedersen, + // otherwise constraints involving these expressions will not error. + let circuit_execution = execute_program( + &compiled_program.program, + WitnessMap::new(), + blackbox_solver, + &mut DefaultForeignCallExecutor::new( + show_output, + foreign_call_resolver_url, + root_path, + package_name, + ), + ); + test_status_program_compile_pass( + test_function, + compiled_program.abi, + compiled_program.debug, + circuit_execution, + ) + } else { + #[cfg(target_arch = "wasm32")] + { + // We currently don't support fuzz testing on wasm32 as the u128 strategies do not exist on this platform. + TestStatus::Fail { + message: "Fuzz tests are not supported on wasm32".to_string(), + error_diagnostic: None, + } + } + + #[cfg(not(target_arch = "wasm32"))] + { + use acvm::acir::circuit::Program; + use noir_fuzzer::FuzzedExecutor; + use proptest::test_runner::TestRunner; + let runner = TestRunner::default(); + + let executor = + |program: &Program, + initial_witness: WitnessMap| + -> Result, String> { + execute_program( + program, + initial_witness, + blackbox_solver, + &mut DefaultForeignCallExecutor::::new( + false, + foreign_call_resolver_url, + root_path.clone(), + package_name.clone(), + ), + ) + .map_err(|err| err.to_string()) + }; + let fuzzer = FuzzedExecutor::new(compiled_program.into(), executor, runner); + + let result = fuzzer.fuzz(); + if result.success { + TestStatus::Pass + } else { + TestStatus::Fail { + message: result.reason.unwrap_or_default(), + error_diagnostic: None, + } + } + } + } } Err(err) => test_status_program_compile_fail(err, test_function), } diff --git a/noir/noir-repo/tooling/nargo_cli/Cargo.toml b/noir/noir-repo/tooling/nargo_cli/Cargo.toml index c6cf842a623..dabb779ae8d 100644 --- a/noir/noir-repo/tooling/nargo_cli/Cargo.toml +++ b/noir/noir-repo/tooling/nargo_cli/Cargo.toml @@ -32,7 +32,6 @@ noirc_driver.workspace = true noirc_frontend = { workspace = true, features = ["bn254"] } noirc_abi.workspace = true noirc_errors.workspace = true -noir_fuzzer.workspace = true noirc_artifacts.workspace = true acvm = { workspace = true, features = ["bn254"] } bn254_blackbox_solver.workspace = true @@ -51,7 +50,6 @@ color-eyre.workspace = true tokio = { version = "1.0", features = ["io-std", "rt"] } dap.workspace = true clap-markdown = { git = "https://github.com/noir-lang/clap-markdown", rev = "450d759532c88f0dba70891ceecdbc9ff8f25d2b", optional = true } -proptest.workspace = true notify = "6.1.1" notify-debouncer-full = "0.3.1" diff --git a/noir/noir-repo/tooling/nargo_cli/src/cli/test_cmd.rs b/noir/noir-repo/tooling/nargo_cli/src/cli/test_cmd.rs index 71b89c526b3..0d7c8fc8bf7 100644 --- a/noir/noir-repo/tooling/nargo_cli/src/cli/test_cmd.rs +++ b/noir/noir-repo/tooling/nargo_cli/src/cli/test_cmd.rs @@ -10,8 +10,7 @@ use nargo::{ }; use nargo_toml::{get_package_manifest, resolve_workspace_from_toml, PackageSelection}; use noirc_driver::{ - check_crate, compile_no_check, file_manager_with_stdlib, CompileOptions, - NOIR_ARTIFACT_VERSION_STRING, + check_crate, file_manager_with_stdlib, CompileOptions, NOIR_ARTIFACT_VERSION_STRING, }; use noirc_frontend::{ graph::CrateName, @@ -190,49 +189,16 @@ fn run_test + Default>( let blackbox_solver = S::default(); - let test_function_has_no_arguments = context - .def_interner - .function_meta(&test_function.get_id()) - .function_signature() - .0 - .is_empty(); - - if test_function_has_no_arguments { - nargo::ops::run_test( - &blackbox_solver, - &mut context, - test_function, - show_output, - foreign_call_resolver_url, - root_path, - package_name, - compile_options, - ) - } else { - use noir_fuzzer::FuzzedExecutor; - use proptest::test_runner::TestRunner; - - let compiled_program = - compile_no_check(&mut context, compile_options, test_function.get_id(), None, false); - match compiled_program { - Ok(compiled_program) => { - let runner = TestRunner::default(); - - let fuzzer = FuzzedExecutor::new(compiled_program.into(), runner); - - let result = fuzzer.fuzz(); - if result.success { - TestStatus::Pass - } else { - TestStatus::Fail { - message: result.reason.unwrap_or_default(), - error_diagnostic: None, - } - } - } - Err(err) => TestStatus::CompileError(err.into()), - } - } + nargo::ops::run_test( + &blackbox_solver, + &mut context, + test_function, + show_output, + foreign_call_resolver_url, + root_path, + package_name, + compile_options, + ) } fn get_tests_in_package( diff --git a/noir/noir-repo/tooling/nargo_cli/tests/stdlib-tests.rs b/noir/noir-repo/tooling/nargo_cli/tests/stdlib-tests.rs index c863d70376c..847250c1dc0 100644 --- a/noir/noir-repo/tooling/nargo_cli/tests/stdlib-tests.rs +++ b/noir/noir-repo/tooling/nargo_cli/tests/stdlib-tests.rs @@ -2,7 +2,7 @@ use std::io::Write; use std::{collections::BTreeMap, path::PathBuf}; use fm::FileManager; -use noirc_driver::{check_crate, compile_no_check, file_manager_with_stdlib, CompileOptions}; +use noirc_driver::{check_crate, file_manager_with_stdlib, CompileOptions}; use noirc_frontend::hir::FunctionNameMatch; use nargo::{ @@ -47,54 +47,16 @@ fn run_stdlib_tests() { let test_report: Vec<(String, TestStatus)> = test_functions .into_iter() .map(|(test_name, test_function)| { - let test_function_has_no_arguments = context - .def_interner - .function_meta(&test_function.get_id()) - .function_signature() - .0 - .is_empty(); - - let status = if test_function_has_no_arguments { - run_test( - &bn254_blackbox_solver::Bn254BlackBoxSolver, - &mut context, - &test_function, - false, - None, - None, - None, - &CompileOptions::default(), - ) - } else { - use noir_fuzzer::FuzzedExecutor; - use proptest::test_runner::TestRunner; - - let compiled_program = compile_no_check( - &mut context, - &CompileOptions::default(), - test_function.get_id(), - None, - false, - ); - match compiled_program { - Ok(compiled_program) => { - let runner = TestRunner::default(); - - let fuzzer = FuzzedExecutor::new(compiled_program.into(), runner); - - let result = fuzzer.fuzz(); - if result.success { - TestStatus::Pass - } else { - TestStatus::Fail { - message: result.reason.unwrap_or_default(), - error_diagnostic: None, - } - } - } - Err(err) => TestStatus::CompileError(err.into()), - } - }; + let status = run_test( + &bn254_blackbox_solver::Bn254BlackBoxSolver, + &mut context, + &test_function, + false, + None, + Some(dummy_package.root_dir.clone()), + Some(dummy_package.name.to_string()), + &CompileOptions::default(), + ); (test_name, status) }) .collect(); diff --git a/noir/noir-repo/tooling/noirc_artifacts/src/debug.rs b/noir/noir-repo/tooling/noirc_artifacts/src/debug.rs index 8ae4156a5f6..8e2add70ae7 100644 --- a/noir/noir-repo/tooling/noirc_artifacts/src/debug.rs +++ b/noir/noir-repo/tooling/noirc_artifacts/src/debug.rs @@ -23,7 +23,7 @@ impl DebugArtifact { pub fn new(debug_symbols: Vec, file_manager: &FileManager) -> Self { let mut file_map = BTreeMap::new(); - let files_with_debug_symbols: BTreeSet = debug_symbols + let mut files_with_debug_symbols: BTreeSet = debug_symbols .iter() .flat_map(|function_symbols| { function_symbols @@ -33,6 +33,21 @@ impl DebugArtifact { }) .collect(); + let files_with_brillig_debug_symbols: BTreeSet = debug_symbols + .iter() + .flat_map(|function_symbols| { + let brillig_location_maps = + function_symbols.brillig_locations.values().flat_map(|brillig_location_map| { + brillig_location_map + .values() + .flat_map(|call_stack| call_stack.iter().map(|location| location.file)) + }); + brillig_location_maps + }) + .collect(); + + files_with_debug_symbols.extend(files_with_brillig_debug_symbols); + for file_id in files_with_debug_symbols { let file_path = file_manager.path(file_id).expect("file should exist"); let file_source = file_manager.fetch_file(file_id).expect("file should exist");