From 8ba3ab2f3570870bf8528eaf6dd1377d9a52d546 Mon Sep 17 00:00:00 2001 From: jfecher Date: Mon, 6 Mar 2023 16:32:25 -0600 Subject: [PATCH] feat: Implement basic contracts (#944) * Implement basic contracts * Remove target directory * Add doc comment --- crates/nargo/src/cli/compile_cmd.rs | 87 ++++++++++++------- crates/nargo/src/cli/contract_cmd.rs | 2 +- crates/nargo/src/cli/execute_cmd.rs | 14 ++- crates/nargo/src/cli/test_cmd.rs | 2 +- .../tests/test_data/contracts/Nargo.toml | 5 ++ .../tests/test_data/contracts/Prover.toml | 2 + .../tests/test_data/contracts/src/main.nr | 8 ++ crates/noirc_driver/src/lib.rs | 68 +++++++++------ crates/noirc_driver/src/main.rs | 3 +- .../src/hir/def_collector/dc_mod.rs | 28 +++--- crates/noirc_frontend/src/hir/def_map/mod.rs | 60 ++++++++++++- .../src/hir/def_map/module_data.rs | 21 ++++- crates/noirc_frontend/src/lexer/token.rs | 3 + crates/noirc_frontend/src/parser/mod.rs | 1 + crates/noirc_frontend/src/parser/parser.rs | 19 +++- 15 files changed, 234 insertions(+), 89 deletions(-) create mode 100644 crates/nargo/tests/test_data/contracts/Nargo.toml create mode 100644 crates/nargo/tests/test_data/contracts/Prover.toml create mode 100644 crates/nargo/tests/test_data/contracts/src/main.nr diff --git a/crates/nargo/src/cli/compile_cmd.rs b/crates/nargo/src/cli/compile_cmd.rs index f8be06beb5f..71d6ceef6f6 100644 --- a/crates/nargo/src/cli/compile_cmd.rs +++ b/crates/nargo/src/cli/compile_cmd.rs @@ -1,6 +1,8 @@ use acvm::acir::circuit::Circuit; use acvm::ProofSystemCompiler; use noirc_driver::CompileOptions; +use noirc_driver::Driver; +use noirc_frontend::node_interner::FuncId; use std::path::{Path, PathBuf}; use clap::Args; @@ -16,49 +18,76 @@ pub(crate) struct CompileCommand { /// The name of the ACIR file circuit_name: String, + /// Compile each contract function used within the program + #[arg(short, long)] + contracts: bool, + #[clap(flatten)] compile_options: CompileOptions, } -pub(crate) fn run(args: CompileCommand, config: NargoConfig) -> Result<(), CliError> { - let mut circuit_path = config.program_dir.clone(); - circuit_path.push(TARGET_DIR); - - let circuit_path = compile_and_preprocess_circuit( - &args.circuit_name, - config.program_dir, - circuit_path, - &args.compile_options, - )?; - - println!("Generated ACIR code into {}", circuit_path.display()); +pub(crate) fn run(mut args: CompileCommand, config: NargoConfig) -> Result<(), CliError> { + let driver = check_crate(&config.program_dir, &args.compile_options)?; + + let mut circuit_dir = config.program_dir; + circuit_dir.push(TARGET_DIR); + + // If contracts is set we're compiling every function in a 'contract' rather than just 'main'. + if args.contracts { + let circuit_name = args.circuit_name.clone(); + + for contract in driver.get_all_contracts() { + for function in contract.functions { + let name = driver.function_name(function); + args.circuit_name = format!("{}-{}-{name}", circuit_name, &contract.name); + compile_and_save_program(&driver, function, &args, &circuit_dir)?; + } + } + Ok(()) + } else { + let main = driver.main_function(); + compile_and_save_program(&driver, main, &args, &circuit_dir) + } +} - Ok(()) +fn setup_driver(program_dir: &Path) -> Result { + let backend = crate::backends::ConcreteBackend; + let mut driver = Resolver::resolve_root_config(program_dir, backend.np_language())?; + add_std_lib(&mut driver); + Ok(driver) } -fn compile_and_preprocess_circuit>( - circuit_name: &str, - program_dir: P, - circuit_dir: P, - compile_options: &CompileOptions, -) -> Result { - let compiled_program = compile_circuit(program_dir, compile_options)?; - let circuit_path = save_program_to_file(&compiled_program, circuit_name, &circuit_dir); +/// Compile and save a program to disk with the given main function. +fn compile_and_save_program( + driver: &Driver, + main: FuncId, + args: &CompileCommand, + circuit_dir: &Path, +) -> Result<(), CliError> { + let compiled_program = driver + .compile_no_check(&args.compile_options, main) + .map_err(|_| CliError::Generic(format!("'{}' failed to compile", args.circuit_name)))?; + + let circuit_path = save_program_to_file(&compiled_program, &args.circuit_name, circuit_dir); - preprocess_with_path(circuit_name, circuit_dir, &compiled_program.circuit)?; + preprocess_with_path(&args.circuit_name, circuit_dir, &compiled_program.circuit)?; - Ok(circuit_path) + println!("Generated ACIR code into {}", circuit_path.display()); + Ok(()) } -pub(crate) fn compile_circuit>( - program_dir: P, +pub(crate) fn compile_circuit( + program_dir: &Path, compile_options: &CompileOptions, ) -> Result { - let backend = crate::backends::ConcreteBackend; - let mut driver = Resolver::resolve_root_config(program_dir.as_ref(), backend.np_language())?; - add_std_lib(&mut driver); + let mut driver = setup_driver(program_dir)?; + driver.compile_main(compile_options).map_err(|_| CliError::CompilationError) +} - driver.into_compiled_program(compile_options).map_err(|_| CliError::CompilationError) +fn check_crate(program_dir: &Path, options: &CompileOptions) -> Result { + let mut driver = setup_driver(program_dir)?; + driver.check_crate(options).map_err(|_| CliError::CompilationError)?; + Ok(driver) } fn preprocess_with_path>( diff --git a/crates/nargo/src/cli/contract_cmd.rs b/crates/nargo/src/cli/contract_cmd.rs index a8f2fb3a8b1..35318de2884 100644 --- a/crates/nargo/src/cli/contract_cmd.rs +++ b/crates/nargo/src/cli/contract_cmd.rs @@ -13,7 +13,7 @@ pub(crate) struct ContractCommand { } pub(crate) fn run(args: ContractCommand, config: NargoConfig) -> Result<(), CliError> { - let compiled_program = compile_circuit(config.program_dir.clone(), &args.compile_options)?; + let compiled_program = compile_circuit(&config.program_dir, &args.compile_options)?; let backend = crate::backends::ConcreteBackend; #[allow(deprecated)] diff --git a/crates/nargo/src/cli/execute_cmd.rs b/crates/nargo/src/cli/execute_cmd.rs index 6ebc3ece62a..de797d9973d 100644 --- a/crates/nargo/src/cli/execute_cmd.rs +++ b/crates/nargo/src/cli/execute_cmd.rs @@ -43,19 +43,15 @@ pub(crate) fn run(args: ExecuteCommand, config: NargoConfig) -> Result<(), CliEr Ok(()) } -fn execute_with_path>( - program_dir: P, +fn execute_with_path( + program_dir: &Path, compile_options: &CompileOptions, ) -> Result<(Option, WitnessMap), CliError> { - let compiled_program = compile_circuit(&program_dir, compile_options)?; + let compiled_program = compile_circuit(program_dir, compile_options)?; // Parse the initial witness values from Prover.toml - let (inputs_map, _) = read_inputs_from_file( - &program_dir, - PROVER_INPUT_FILE, - Format::Toml, - &compiled_program.abi, - )?; + let (inputs_map, _) = + read_inputs_from_file(program_dir, PROVER_INPUT_FILE, Format::Toml, &compiled_program.abi)?; let solved_witness = execute_program(&compiled_program, &inputs_map)?; diff --git a/crates/nargo/src/cli/test_cmd.rs b/crates/nargo/src/cli/test_cmd.rs index 65e997c53a8..01c21e1ac57 100644 --- a/crates/nargo/src/cli/test_cmd.rs +++ b/crates/nargo/src/cli/test_cmd.rs @@ -86,7 +86,7 @@ fn run_test( let backend = crate::backends::ConcreteBackend; let program = driver - .compile_no_check(config, Some(main)) + .compile_no_check(config, main) .map_err(|_| CliError::Generic(format!("Test '{test_name}' failed to compile")))?; let mut solved_witness = BTreeMap::new(); diff --git a/crates/nargo/tests/test_data/contracts/Nargo.toml b/crates/nargo/tests/test_data/contracts/Nargo.toml new file mode 100644 index 00000000000..e0b467ce5da --- /dev/null +++ b/crates/nargo/tests/test_data/contracts/Nargo.toml @@ -0,0 +1,5 @@ +[package] +authors = [""] +compiler_version = "0.1" + +[dependencies] \ No newline at end of file diff --git a/crates/nargo/tests/test_data/contracts/Prover.toml b/crates/nargo/tests/test_data/contracts/Prover.toml new file mode 100644 index 00000000000..97d5b1e0eed --- /dev/null +++ b/crates/nargo/tests/test_data/contracts/Prover.toml @@ -0,0 +1,2 @@ +x = 3 +y = 2 diff --git a/crates/nargo/tests/test_data/contracts/src/main.nr b/crates/nargo/tests/test_data/contracts/src/main.nr new file mode 100644 index 00000000000..092439ad93a --- /dev/null +++ b/crates/nargo/tests/test_data/contracts/src/main.nr @@ -0,0 +1,8 @@ +fn main(x : Field, y : pub Field) { + constrain Foo::double(x) == Foo::triple(y); +} + +contract Foo { + fn double(x: Field) -> Field { x * 2 } + fn triple(x: Field) -> Field { x * 3 } +} diff --git a/crates/noirc_driver/src/lib.rs b/crates/noirc_driver/src/lib.rs index 79620897412..246cb2c71b8 100644 --- a/crates/noirc_driver/src/lib.rs +++ b/crates/noirc_driver/src/lib.rs @@ -9,7 +9,7 @@ use noirc_abi::FunctionSignature; use noirc_errors::{reporter, ReportedError}; use noirc_evaluator::create_circuit; use noirc_frontend::graph::{CrateId, CrateName, CrateType, LOCAL_CRATE}; -use noirc_frontend::hir::def_map::CrateDefMap; +use noirc_frontend::hir::def_map::{Contract, CrateDefMap}; use noirc_frontend::hir::Context; use noirc_frontend::monomorphization::monomorphize; use noirc_frontend::node_interner::FuncId; @@ -23,7 +23,7 @@ pub struct Driver { language: Language, } -#[derive(Args, Clone, Debug, Default)] +#[derive(Args, Clone, Debug)] pub struct CompileOptions { /// Emit debug information for the intermediate SSA IR #[arg(short, long)] @@ -38,6 +38,12 @@ pub struct CompileOptions { pub show_output: bool, } +impl Default for CompileOptions { + fn default() -> Self { + Self { show_ssa: false, allow_warnings: false, show_output: true } + } +} + impl Driver { pub fn new(np_language: &Language) -> Self { let mut driver = Driver { context: Context::default(), language: np_language.clone() }; @@ -50,10 +56,7 @@ impl Driver { pub fn compile_file(root_file: PathBuf, np_language: acvm::Language) -> CompiledProgram { let mut driver = Driver::new(&np_language); driver.create_local_crate(root_file, CrateType::Binary); - - driver - .into_compiled_program(&CompileOptions::default()) - .unwrap_or_else(|_| std::process::exit(1)) + driver.compile_main(&CompileOptions::default()).unwrap_or_else(|_| std::process::exit(1)) } /// Compiles a file and returns true if compilation was successful @@ -158,12 +161,32 @@ impl Driver { Some(func_meta.into_function_signature(&self.context.def_interner)) } - pub fn into_compiled_program( - mut self, + /// Run the frontend to check the crate for errors then compile the main function if there were none + pub fn compile_main( + &mut self, options: &CompileOptions, ) -> Result { self.check_crate(options)?; - self.compile_no_check(options, None) + let main = self.main_function(); + self.compile_no_check(options, main) + } + + /// Returns the FuncId of the 'main' funciton. + /// - Expects check_crate to be called beforehand + /// - Panics if no main function is found + pub fn main_function(&self) -> FuncId { + // Find the local crate, one should always be present + let local_crate = self.context.def_map(LOCAL_CRATE).unwrap(); + + // Check the crate type + // We don't panic here to allow users to `evaluate` libraries which will do nothing + if self.context.crate_graph[LOCAL_CRATE].crate_type != CrateType::Binary { + println!("cannot compile crate into a program as the local crate is not a binary. For libraries, please use the check command"); + std::process::exit(1); + }; + + // All Binaries should have a main function + local_crate.main_function().expect("cannot compile a program with no main function") } /// Compile the current crate. Assumes self.check_crate is called beforehand! @@ -171,25 +194,8 @@ impl Driver { pub fn compile_no_check( &self, options: &CompileOptions, - // Optional override to provide a different `main` function to start execution - main_function: Option, + main_function: FuncId, ) -> Result { - // Find the local crate, one should always be present - let local_crate = self.context.def_map(LOCAL_CRATE).unwrap(); - - // If no override for the `main` function has been provided, attempt to find it. - let main_function = main_function.unwrap_or_else(|| { - // Check the crate type - // We don't panic here to allow users to `evaluate` libraries which will do nothing - if self.context.crate_graph[LOCAL_CRATE].crate_type != CrateType::Binary { - println!("cannot compile crate into a program as the local crate is not a binary. For libraries, please use the check command"); - std::process::exit(1); - }; - - // All Binaries should have a main function - local_crate.main_function().expect("cannot compile a program with no main function") - }); - let program = monomorphize(main_function, &self.context.def_interner); let np_language = self.language.clone(); @@ -228,6 +234,14 @@ impl Driver { .collect() } + /// Return a Vec of all `contract` declarations in the source code and the functions they contain + pub fn get_all_contracts(&self) -> Vec { + self.context + .def_map(LOCAL_CRATE) + .expect("The local crate should be analyzed already") + .get_all_contracts() + } + pub fn function_name(&self, id: FuncId) -> &str { self.context.def_interner.function_name(&id) } diff --git a/crates/noirc_driver/src/main.rs b/crates/noirc_driver/src/main.rs index 98a51780854..0d9127d04bd 100644 --- a/crates/noirc_driver/src/main.rs +++ b/crates/noirc_driver/src/main.rs @@ -18,6 +18,5 @@ fn main() { driver.add_dep(LOCAL_CRATE, crate_id1, "coo4"); driver.add_dep(LOCAL_CRATE, crate_id2, "coo3"); - let config = CompileOptions::default(); - driver.into_compiled_program(&config).ok(); + driver.compile_main(&CompileOptions::default()).ok(); } diff --git a/crates/noirc_frontend/src/hir/def_collector/dc_mod.rs b/crates/noirc_frontend/src/hir/def_collector/dc_mod.rs index 9433cd1519a..1a58decda99 100644 --- a/crates/noirc_frontend/src/hir/def_collector/dc_mod.rs +++ b/crates/noirc_frontend/src/hir/def_collector/dc_mod.rs @@ -161,7 +161,7 @@ impl<'a> ModCollector<'a> { let name = struct_definition.name.clone(); // Create the corresponding module for the struct namespace - let id = match self.push_child_module(&name, self.file_id, false, errors) { + let id = match self.push_child_module(&name, self.file_id, false, false, errors) { Some(local_id) => StructId(ModuleId { krate, local_id }), None => continue, }; @@ -195,7 +195,13 @@ impl<'a> ModCollector<'a> { errors: &mut Vec, ) { for submodule in submodules { - if let Some(child) = self.push_child_module(&submodule.name, file_id, true, errors) { + if let Some(child) = self.push_child_module( + &submodule.name, + file_id, + true, + submodule.is_contract, + errors, + ) { collect_defs( self.def_collector, submodule.contents, @@ -234,7 +240,9 @@ impl<'a> ModCollector<'a> { let ast = parse_file(&mut context.file_manager, child_file_id, errors); // Add module into def collector and get a ModuleId - if let Some(child_mod_id) = self.push_child_module(mod_name, child_file_id, true, errors) { + if let Some(child_mod_id) = + self.push_child_module(mod_name, child_file_id, true, false, errors) + { collect_defs( self.def_collector, ast, @@ -254,21 +262,15 @@ impl<'a> ModCollector<'a> { mod_name: &Ident, file_id: FileId, add_to_parent_scope: bool, + is_contract: bool, errors: &mut Vec, ) -> Option { - // Create a new default module - let module_id = self.def_collector.def_map.modules.insert(ModuleData::default()); + let parent = Some(self.module_id); + let new_module = ModuleData::new(parent, ModuleOrigin::File(file_id), is_contract); + let module_id = self.def_collector.def_map.modules.insert(new_module); let modules = &mut self.def_collector.def_map.modules; - // Update the child module to reference the parent - modules[module_id].parent = Some(self.module_id); - - // Update the origin of the child module - // Also note that the FileId is where this module is defined and not declared - // To find out where the module was declared, you need to check its parent - modules[module_id].origin = ModuleOrigin::File(file_id); - // Update the parent module to reference the child modules[self.module_id.0].children.insert(mod_name.clone(), LocalModuleId(module_id)); diff --git a/crates/noirc_frontend/src/hir/def_map/mod.rs b/crates/noirc_frontend/src/hir/def_map/mod.rs index be2a9feeb5f..7c0788050ce 100644 --- a/crates/noirc_frontend/src/hir/def_map/mod.rs +++ b/crates/noirc_frontend/src/hir/def_map/mod.rs @@ -72,10 +72,8 @@ impl CrateDefMap { // Allocate a default Module for the root, giving it a ModuleId let mut modules: Arena = Arena::default(); - let root = modules.insert(ModuleData::default()); - - // Set the origin of the root module - modules[root].origin = ModuleOrigin::CrateRoot(root_file_id); + let origin = ModuleOrigin::CrateRoot(root_file_id); + let root = modules.insert(ModuleData::new(None, origin, false)); let def_map = CrateDefMap { root: LocalModuleId(root), @@ -129,6 +127,60 @@ impl CrateDefMap { functions.filter(|id| interner.function_meta(id).attributes == Some(Attribute::Test)) }) } + + /// Go through all modules in this crate, find all `contract ... { ... }` declarations, + /// and collect them all into a Vec. + pub fn get_all_contracts(&self) -> Vec { + self.modules + .iter() + .filter_map(|(id, module)| { + if module.is_contract { + let functions = module + .scope + .values() + .values() + .filter_map(|(id, _)| id.as_function()) + .collect(); + + let name = self.get_module_path(id, module.parent); + Some(Contract { name, functions }) + } else { + None + } + }) + .collect() + } + + /// Find a child module's name by inspecting its parent. + /// Currently required as modules do not store their own names. + fn get_module_path(&self, child_id: Index, parent: Option) -> String { + if let Some(id) = parent { + let parent = &self.modules[id.0]; + let name = parent + .children + .iter() + .find(|(_, id)| id.0 == child_id) + .map(|(name, _)| &name.0.contents) + .expect("Child module was not a child of the given parent module"); + + let parent_name = self.get_module_path(id.0, parent.parent); + if parent_name.is_empty() { + name.to_string() + } else { + format!("{parent_name}.{name}") + } + } else { + String::new() + } + } +} + +/// A 'contract' in Noir source code with the given name and functions. +/// This is not an AST node, it is just a convenient form to return for CrateDefMap::get_all_contracts. +pub struct Contract { + /// To keep `name` semi-unique, it is prefixed with the names of parent modules via CrateDefMap::get_module_path + pub name: String, + pub functions: Vec, } /// Given a FileId, fetch the File, from the FileManager and parse it's content diff --git a/crates/noirc_frontend/src/hir/def_map/module_data.rs b/crates/noirc_frontend/src/hir/def_map/module_data.rs index 8f62f7581b1..d437a4d1b6c 100644 --- a/crates/noirc_frontend/src/hir/def_map/module_data.rs +++ b/crates/noirc_frontend/src/hir/def_map/module_data.rs @@ -8,13 +8,32 @@ use super::{ItemScope, LocalModuleId}; /// Contains the actual contents of a module: its parent (if one exists), /// children, and scope with all definitions defined within the scope. -#[derive(Default, Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq)] pub struct ModuleData { pub parent: Option, pub children: HashMap, pub scope: ItemScope, pub origin: ModuleOrigin, + + /// True if this module is a `contract Foo { ... }` module containing contract functions + pub is_contract: bool, +} + +impl ModuleData { + pub fn new( + parent: Option, + origin: ModuleOrigin, + is_contract: bool, + ) -> ModuleData { + ModuleData { + parent, + children: HashMap::new(), + scope: ItemScope::default(), + origin, + is_contract, + } + } } #[derive(Debug, PartialEq, Eq, Copy, Clone)] diff --git a/crates/noirc_frontend/src/lexer/token.rs b/crates/noirc_frontend/src/lexer/token.rs index 9213c7d5291..6cce4d351b4 100644 --- a/crates/noirc_frontend/src/lexer/token.rs +++ b/crates/noirc_frontend/src/lexer/token.rs @@ -418,6 +418,7 @@ pub enum Keyword { Char, CompTime, Constrain, + Contract, Crate, Dep, Else, @@ -447,6 +448,7 @@ impl fmt::Display for Keyword { Keyword::Char => write!(f, "char"), Keyword::CompTime => write!(f, "comptime"), Keyword::Constrain => write!(f, "constrain"), + Keyword::Contract => write!(f, "contract"), Keyword::Crate => write!(f, "crate"), Keyword::Dep => write!(f, "dep"), Keyword::Else => write!(f, "else"), @@ -479,6 +481,7 @@ impl Keyword { "char" => Keyword::Char, "comptime" => Keyword::CompTime, "constrain" => Keyword::Constrain, + "contract" => Keyword::Contract, "crate" => Keyword::Crate, "dep" => Keyword::Dep, "else" => Keyword::Else, diff --git a/crates/noirc_frontend/src/parser/mod.rs b/crates/noirc_frontend/src/parser/mod.rs index 72fb973fbf6..fa0903ee659 100644 --- a/crates/noirc_frontend/src/parser/mod.rs +++ b/crates/noirc_frontend/src/parser/mod.rs @@ -234,6 +234,7 @@ pub struct ParsedModule { pub struct SubModule { pub name: Ident, pub contents: ParsedModule, + pub is_contract: bool, } impl ParsedModule { diff --git a/crates/noirc_frontend/src/parser/parser.rs b/crates/noirc_frontend/src/parser/parser.rs index 708070652cc..9e767d3819b 100644 --- a/crates/noirc_frontend/src/parser/parser.rs +++ b/crates/noirc_frontend/src/parser/parser.rs @@ -101,7 +101,8 @@ fn top_level_statement( function_definition(false).map(TopLevelStatement::Function), struct_definition(), implementation(), - submodule(module_parser), + submodule(module_parser.clone()), + contract(module_parser), module_declaration().then_ignore(force(just(Token::Semicolon))), use_statement().then_ignore(force(just(Token::Semicolon))), global_declaration().then_ignore(force(just(Token::Semicolon))), @@ -128,7 +129,21 @@ fn submodule(module_parser: impl NoirParser) -> impl NoirParser) -> impl NoirParser { + keyword(Keyword::Contract) + .ignore_then(ident()) + .then_ignore(just(Token::LeftBrace)) + .then(module_parser) + .then_ignore(just(Token::RightBrace)) + .map(|(name, contents)| { + TopLevelStatement::SubModule(SubModule { name, contents, is_contract: true }) + }) } /// function_definition: attribute fn ident generics '(' function_parameters ')' function_return_type block