diff --git a/.gitignore b/.gitignore index 53a9838170..943e10b577 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ **/target /tmp/ +/temp/ **.idea/ *.DS_Store .vscode @@ -23,4 +24,4 @@ sccache*/ *.bat # environment -.env \ No newline at end of file +.env diff --git a/Cargo.lock b/Cargo.lock index 35cb30c7d2..366b7f3b31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1699,14 +1699,17 @@ dependencies = [ "indexmap 2.6.0", "leo-ast", "leo-compiler", + "leo-disassembler", "leo-errors", "leo-interpreter", "leo-package", "leo-retriever", "leo-span", "num-format", + "num_cpus", "rand", "rand_chacha", + "rayon", "rpassword", "rusty-hook", "self_update 0.41.0", diff --git a/Cargo.toml b/Cargo.toml index d28d45c21e..f88982caaf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -156,6 +156,9 @@ workspace = true [dependencies.leo-compiler] workspace = true +[dependencies.leo-disassembler] +workspace = true + [dependencies.leo-errors] workspace = true @@ -190,6 +193,9 @@ version = "0.15.7" [dependencies.indexmap] workspace = true +[dependencies.num_cpus] +version = "1.16.0" + [dependencies.rand] workspace = true @@ -229,6 +235,9 @@ features = [ "fmt" ] [dependencies.crossterm] version = "0.28.1" +[dependencies.rayon] +version = "1.10.0" + [dependencies.rpassword] version = "7.3.1" diff --git a/compiler/ast/src/functions/annotation.rs b/compiler/ast/src/functions/annotation.rs index 2cb07b54f4..13819aac35 100644 --- a/compiler/ast/src/functions/annotation.rs +++ b/compiler/ast/src/functions/annotation.rs @@ -18,6 +18,7 @@ use crate::{Identifier, Node, NodeID, simple_node_impl}; use leo_span::Span; +use indexmap::IndexMap; use serde::{Deserialize, Serialize}; use std::fmt; @@ -27,6 +28,8 @@ pub struct Annotation { // TODO: Consider using a symbol instead of an identifier. /// The name of the annotation. pub identifier: Identifier, + /// The data associated with the annotation. + pub data: IndexMap>, /// A span locating where the annotation occurred in the source. pub span: Span, /// The ID of the node. @@ -37,6 +40,19 @@ simple_node_impl!(Annotation); impl fmt::Display for Annotation { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "@{}", self.identifier) + let data = match self.data.is_empty() { + true => "".to_string(), + false => { + let mut string = String::new(); + for (key, value) in self.data.iter() { + match value { + None => string.push_str(&format!("{key},")), + Some(value) => string.push_str(&format!("{key} = \"{value}\",")), + } + } + format!("({string})") + } + }; + write!(f, "@{}{}", self.identifier, data) } } diff --git a/compiler/ast/src/lib.rs b/compiler/ast/src/lib.rs index 2d079a93c5..c89c566933 100644 --- a/compiler/ast/src/lib.rs +++ b/compiler/ast/src/lib.rs @@ -52,6 +52,9 @@ pub use self::program::*; pub mod statement; pub use self::statement::*; +pub mod test; +pub use self::test::*; + pub mod types; pub use self::types::*; @@ -70,6 +73,7 @@ use leo_errors::{AstError, Result}; /// /// The [`Ast`] type represents a Leo program as a series of recursive data types. /// These data types form a tree that begins from a [`Program`] type root. +// TODO: Clean up by removing the `Ast` type and renaming the exiting `Program` type to `Ast`. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct Ast { pub ast: Program, @@ -81,6 +85,15 @@ impl Ast { Self { ast: program } } + /// Combines the two ASTs into a single AST. + /// The ASTs are combined by extending the components of the first AST with the components of the second AST. + pub fn combine(&mut self, other: Self) { + let Program { imports, stubs, program_scopes } = other.ast; + self.ast.imports.extend(imports); + self.ast.stubs.extend(stubs); + self.ast.program_scopes.extend(program_scopes); + } + /// Returns a reference to the inner program AST representation. pub fn as_repr(&self) -> &Program { &self.ast diff --git a/compiler/ast/src/passes/reconstructor.rs b/compiler/ast/src/passes/reconstructor.rs index dea305c0f4..4371a0e12c 100644 --- a/compiler/ast/src/passes/reconstructor.rs +++ b/compiler/ast/src/passes/reconstructor.rs @@ -443,9 +443,6 @@ pub trait ProgramReconstructor: StatementReconstructor { fn reconstruct_program_scope(&mut self, input: ProgramScope) -> ProgramScope { ProgramScope { program_id: input.program_id, - structs: input.structs.into_iter().map(|(i, c)| (i, self.reconstruct_struct(c))).collect(), - mappings: input.mappings.into_iter().map(|(id, mapping)| (id, self.reconstruct_mapping(mapping))).collect(), - functions: input.functions.into_iter().map(|(i, f)| (i, self.reconstruct_function(f))).collect(), consts: input .consts .into_iter() @@ -454,6 +451,9 @@ pub trait ProgramReconstructor: StatementReconstructor { _ => unreachable!("`reconstruct_const` can only return `Statement::Const`"), }) .collect(), + structs: input.structs.into_iter().map(|(i, c)| (i, self.reconstruct_struct(c))).collect(), + mappings: input.mappings.into_iter().map(|(id, mapping)| (id, self.reconstruct_mapping(mapping))).collect(), + functions: input.functions.into_iter().map(|(i, f)| (i, self.reconstruct_function(f))).collect(), span: input.span, } } diff --git a/compiler/ast/src/passes/visitor.rs b/compiler/ast/src/passes/visitor.rs index 4c33c8f95d..d2842f980f 100644 --- a/compiler/ast/src/passes/visitor.rs +++ b/compiler/ast/src/passes/visitor.rs @@ -254,13 +254,10 @@ pub trait ProgramVisitor<'a>: StatementVisitor<'a> { } fn visit_program_scope(&mut self, input: &'a ProgramScope) { + input.consts.iter().for_each(|(_, c)| (self.visit_const(c))); input.structs.iter().for_each(|(_, c)| (self.visit_struct(c))); - input.mappings.iter().for_each(|(_, c)| (self.visit_mapping(c))); - input.functions.iter().for_each(|(_, c)| (self.visit_function(c))); - - input.consts.iter().for_each(|(_, c)| (self.visit_const(c))); } fn visit_stub(&mut self, _input: &'a Stub) {} diff --git a/compiler/ast/src/test/manifest.rs b/compiler/ast/src/test/manifest.rs new file mode 100644 index 0000000000..ba3a303459 --- /dev/null +++ b/compiler/ast/src/test/manifest.rs @@ -0,0 +1,56 @@ +// Copyright (C) 2019-2024 Aleo Systems Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use crate::ProgramId; +use snarkvm::prelude::{Network, PrivateKey}; + +use serde::{Deserialize, Serialize}; + +/// A manifest describing the tests to be run and their associated metadata. +#[derive(Debug, Serialize, Deserialize)] +#[serde(bound = "")] +pub struct TestManifest { + /// The program ID. + pub program_id: String, + /// The tests to be run. + pub tests: Vec>, +} + +impl TestManifest { + /// Create a new test manifest. + pub fn new(program_id: &ProgramId) -> Self { + Self { program_id: program_id.to_string(), tests: Vec::new() } + } + + /// Add a test to the manifest. + pub fn add_test(&mut self, test: TestMetadata) { + self.tests.push(test); + } +} + +/// Metadata associated with a test. +#[derive(Debug, Serialize, Deserialize)] +#[serde(bound = "")] +pub struct TestMetadata { + /// The name of the function. + pub function_name: String, + /// The private key to run the test with. + pub private_key: Option>, + /// The seed for the RNG. + pub seed: Option, + /// Whether or not the test is expected to fail. + pub should_fail: bool, +} diff --git a/compiler/ast/src/test/mod.rs b/compiler/ast/src/test/mod.rs new file mode 100644 index 0000000000..92b0cbb336 --- /dev/null +++ b/compiler/ast/src/test/mod.rs @@ -0,0 +1,18 @@ +// Copyright (C) 2019-2024 Aleo Systems Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +mod manifest; +pub use manifest::*; diff --git a/compiler/compiler/src/compiler.rs b/compiler/compiler/src/compiler.rs index de4076b04c..8a3f3a3fd9 100644 --- a/compiler/compiler/src/compiler.rs +++ b/compiler/compiler/src/compiler.rs @@ -21,7 +21,7 @@ use crate::CompilerOptions; pub use leo_ast::Ast; -use leo_ast::{NodeBuilder, Program, Stub}; +use leo_ast::{NodeBuilder, Program, Stub, TestManifest}; use leo_errors::{CompilerError, Result, emitter::Handler}; pub use leo_passes::SymbolTable; use leo_passes::*; @@ -30,22 +30,19 @@ use leo_span::{Symbol, source_map::FileName, symbol::with_session_globals}; use snarkvm::prelude::Network; use indexmap::{IndexMap, IndexSet}; -use sha2::{Digest, Sha256}; -use std::{fs, path::PathBuf}; +use std::path::PathBuf; /// The primary entry point of the Leo compiler. #[derive(Clone)] pub struct Compiler<'a, N: Network> { + /// A name used to identify the instance of the compiler. + pub name: String, /// The handler is used for error and warning emissions. handler: &'a Handler, - /// The path to the main leo file. - main_file_path: PathBuf, + /// The source files and their content. + sources: Vec<(FileName, String)>, /// The path to where the compiler outputs all generated files. output_directory: PathBuf, - /// The program name, - pub program_name: String, - /// The network name, - pub network: String, /// The AST for the program. pub ast: Ast, /// Options configuring compilation. @@ -65,25 +62,23 @@ pub struct Compiler<'a, N: Network> { impl<'a, N: Network> Compiler<'a, N> { /// Returns a new Leo compiler. pub fn new( - program_name: String, - network: String, + name: String, handler: &'a Handler, - main_file_path: PathBuf, + sources: Vec<(FileName, String)>, output_directory: PathBuf, - compiler_options: Option, + compiler_options: CompilerOptions, import_stubs: IndexMap, ) -> Self { let node_builder = NodeBuilder::default(); let assigner = Assigner::default(); let type_table = TypeTable::default(); Self { + name, handler, - main_file_path, + sources, output_directory, - program_name, - network, ast: Ast::new(Program::default()), - compiler_options: compiler_options.unwrap_or_default(), + compiler_options, node_builder, assigner, import_stubs, @@ -92,58 +87,52 @@ impl<'a, N: Network> Compiler<'a, N> { } } - /// Returns a SHA256 checksum of the program file. - pub fn checksum(&self) -> Result { - // Read in the main file as string - let unparsed_file = fs::read_to_string(&self.main_file_path) - .map_err(|e| CompilerError::file_read_error(self.main_file_path.clone(), e))?; - - // Hash the file contents - let mut hasher = Sha256::new(); - hasher.update(unparsed_file.as_bytes()); - let hash = hasher.finalize(); - - Ok(format!("{hash:x}")) + // TODO: Rethink build caching. + // /// Returns a SHA256 checksum of the program file. + // pub fn checksum(&self) -> Result { + // // Read in the main file as string + // let unparsed_file = fs::read_to_string(&self.main_file_path) + // .map_err(|e| CompilerError::file_read_error(self.main_file_path.clone(), e))?; + // + // // Hash the file contents + // let mut hasher = Sha256::new(); + // hasher.update(unparsed_file.as_bytes()); + // let hash = hasher.finalize(); + // + // Ok(format!("{hash:x}")) + // } + + /// Reset the compiler with new sources. + pub fn reset(&mut self, sources: Vec<(FileName, String)>) { + // Reset the sources and AST. + self.sources = sources; + self.ast = Ast::new(Program::default()); + // Reset the internal state. + self.node_builder = NodeBuilder::default(); + self.assigner = Assigner::default(); + self.type_table = TypeTable::default(); } - /// Parses and stores a program file content from a string, constructs a syntax tree, and generates a program. - pub fn parse_program_from_string(&mut self, program_string: &str, name: FileName) -> Result<()> { - // Register the source (`program_string`) in the source map. - let prg_sf = with_session_globals(|s| s.source_map.new_source(program_string, name)); - - // Use the parser to construct the abstract syntax tree (ast). - self.ast = leo_parser::parse_ast::(self.handler, &self.node_builder, &prg_sf.src, prg_sf.start_pos)?; - - // If the program is imported, then check that the name of its program scope matches the file name. - // Note that parsing enforces that there is exactly one program scope in a file. - // TODO: Clean up check. - let program_scope = self.ast.ast.program_scopes.values().next().unwrap(); - let program_scope_name = format!("{}", program_scope.program_id.name); - if program_scope_name != self.program_name { - return Err(CompilerError::program_scope_name_does_not_match( - program_scope_name, - self.program_name.clone(), - program_scope.program_id.name.span, - ) - .into()); + /// Parses and stores the source information, constructs the AST, and optionally outputs it. + pub fn parse(&mut self) -> Result<()> { + // Initialize the AST. + let mut ast = Ast::default(); + // Parse the sources. + for (name, program_string) in &self.sources { + // Register the source (`program_string`) in the source map. + let prg_sf = with_session_globals(|s| s.source_map.new_source(program_string, name.clone())); + // Use the parser to construct the abstract syntax tree (ast). + ast.combine(leo_parser::parse_ast::(self.handler, &self.node_builder, &prg_sf.src, prg_sf.start_pos)?); } - + // Store the AST. + self.ast = ast; + // Write the AST to a JSON file. if self.compiler_options.output.initial_ast { self.write_ast_to_json("initial_ast.json")?; } - Ok(()) } - /// Parses and stores the main program file, constructs a syntax tree, and generates a program. - pub fn parse_program(&mut self) -> Result<()> { - // Load the program file. - let program_string = fs::read_to_string(&self.main_file_path) - .map_err(|e| CompilerError::file_read_error(&self.main_file_path, e))?; - - self.parse_program_from_string(&program_string, FileName::Real(self.main_file_path.clone())) - } - /// Runs the symbol table pass. pub fn symbol_table_pass(&self) -> Result { let symbol_table = SymbolTableCreator::do_pass((&self.ast, self.handler))?; @@ -312,10 +301,38 @@ impl<'a, N: Network> Compiler<'a, N> { Ok((st, struct_graph, call_graph)) } + /// Generates the test manifest. + pub fn test_manifest_pass(&self) -> Result> { + let manifest = TestManifestGenerator::do_pass((&self.ast, self.handler))?; + Ok(manifest) + } + + /// Runs the test compiler stages. + pub fn test_compiler_stages(&mut self) -> Result<(SymbolTable, StructGraph, CallGraph, TestManifest)> { + let st = self.symbol_table_pass()?; + let (st, struct_graph, call_graph) = self.type_checker_pass(st)?; + + let test_manifest = self.test_manifest_pass()?; + + let st = self.loop_unrolling_pass(st)?; + + self.static_single_assignment_pass(&st)?; + + self.flattening_pass(&st)?; + + self.destructuring_pass()?; + + self.function_inlining_pass(&call_graph)?; + + self.dead_code_elimination_pass()?; + + Ok((st, struct_graph, call_graph, test_manifest)) + } + /// Returns a compiled Leo program. pub fn compile(&mut self) -> Result { // Parse the program. - self.parse_program()?; + self.parse()?; // Copy the dependencies specified in `program.json` into the AST. self.add_import_stubs()?; // Run the intermediate compiler stages. @@ -325,15 +342,28 @@ impl<'a, N: Network> Compiler<'a, N> { Ok(bytecode) } + /// Returns the compiled Leo tests. + pub fn compile_tests(&mut self) -> Result<(String, TestManifest)> { + // Parse the program. + self.parse()?; + // Copy the dependencies specified in `program.json` into the AST. + self.add_import_stubs()?; + // Run the intermediate compiler stages. + let (symbol_table, struct_graph, call_graph, test_manifest) = self.test_compiler_stages()?; + // Run code generation. + let bytecode = self.code_generation_pass(&symbol_table, &struct_graph, &call_graph)?; + Ok((bytecode, test_manifest)) + } + /// Writes the AST to a JSON file. fn write_ast_to_json(&self, file_suffix: &str) -> Result<()> { // Remove `Span`s if they are not enabled. if self.compiler_options.output.ast_spans_enabled { - self.ast.to_json_file(self.output_directory.clone(), &format!("{}.{file_suffix}", self.program_name))?; + self.ast.to_json_file(self.output_directory.clone(), &format!("{}.{file_suffix}", self.name))?; } else { self.ast.to_json_file_without_keys( self.output_directory.clone(), - &format!("{}.{file_suffix}", self.program_name), + &format!("{}.{file_suffix}", self.name), &["_span", "span"], )?; } @@ -344,12 +374,11 @@ impl<'a, N: Network> Compiler<'a, N> { fn write_symbol_table_to_json(&self, file_suffix: &str, symbol_table: &SymbolTable) -> Result<()> { // Remove `Span`s if they are not enabled. if self.compiler_options.output.symbol_table_spans_enabled { - symbol_table - .to_json_file(self.output_directory.clone(), &format!("{}.{file_suffix}", self.program_name))?; + symbol_table.to_json_file(self.output_directory.clone(), &format!("{}.{file_suffix}", self.name))?; } else { symbol_table.to_json_file_without_keys( self.output_directory.clone(), - &format!("{}.{file_suffix}", self.program_name), + &format!("{}.{file_suffix}", self.name), &["_span", "span"], )?; } @@ -375,7 +404,6 @@ impl<'a, N: Network> Compiler<'a, N> { } } else { return Err(CompilerError::imported_program_not_found( - self.program_name.clone(), *program_name, self.ast.ast.imports[program_name].1, ) diff --git a/compiler/compiler/src/options.rs b/compiler/compiler/src/options.rs index 6ed93c4309..263a0a1353 100644 --- a/compiler/compiler/src/options.rs +++ b/compiler/compiler/src/options.rs @@ -32,6 +32,8 @@ pub struct BuildOptions { pub conditional_block_max_depth: usize, /// Whether to disable type checking for nested conditionals. pub disable_conditional_branch_type_checking: bool, + /// If enabled builds all test programs. + pub build_tests: bool, } #[derive(Clone, Default)] diff --git a/compiler/compiler/tests/integration/compile.rs b/compiler/compiler/tests/integration/compile.rs index 33c53a8969..46c8a440d0 100644 --- a/compiler/compiler/tests/integration/compile.rs +++ b/compiler/compiler/tests/integration/compile.rs @@ -111,12 +111,11 @@ fn run_test(test: Test, handler: &Handler, buf: &BufferEmitter) -> Result Result(&program_name, &bytecode).map_err(|err| err.into()), + disassemble_from_str::(program_name, &bytecode).map_err(|err| err.into()), )?; - import_stubs.insert(Symbol::intern(&program_name), stub); + import_stubs.insert(Symbol::intern(program_name), stub); // Hash the ast files. let (initial_ast, unrolled_ast, ssa_ast, flattened_ast, destructured_ast, inlined_ast, dce_ast) = - hash_asts(&program_name); + hash_asts(program_name); // Hash the symbol tables. let (initial_symbol_table, type_checked_symbol_table, unrolled_symbol_table) = - hash_symbol_tables(&program_name); + hash_symbol_tables(program_name); // Clean up the output directory. if fs::read_dir("/tmp/output").is_ok() { diff --git a/compiler/compiler/tests/integration/execute.rs b/compiler/compiler/tests/integration/execute.rs index 2d7c92a75e..a8fd6b8d0f 100644 --- a/compiler/compiler/tests/integration/execute.rs +++ b/compiler/compiler/tests/integration/execute.rs @@ -140,12 +140,11 @@ fn run_test(test: Test, handler: &Handler, buf: &BufferEmitter) -> Result Result(&program_name, &bytecode).map_err(|err| err.into()), + disassemble_from_str::(program_name, &bytecode).map_err(|err| err.into()), )?; - import_stubs.insert(Symbol::intern(&program_name), stub); + import_stubs.insert(Symbol::intern(program_name), stub); // Hash the ast files. let (initial_ast, unrolled_ast, ssa_ast, flattened_ast, destructured_ast, inlined_ast, dce_ast) = - hash_asts(&program_name); + hash_asts(program_name); // Hash the symbol tables. let (initial_symbol_table, type_checked_symbol_table, unrolled_symbol_table) = - hash_symbol_tables(&program_name); + hash_symbol_tables(program_name); // Clean up the output directory. if fs::read_dir("/tmp/output").is_ok() { diff --git a/compiler/compiler/tests/integration/utilities/mod.rs b/compiler/compiler/tests/integration/utilities/mod.rs index b9ef01213b..7deaa302a7 100644 --- a/compiler/compiler/tests/integration/utilities/mod.rs +++ b/compiler/compiler/tests/integration/utilities/mod.rs @@ -104,6 +104,7 @@ pub fn get_build_options(test_config: &TestConfig) -> Vec { .expect("Expected value to be a boolean."), conditional_block_max_depth: 10, disable_conditional_branch_type_checking: false, + build_tests: true, } }) .collect() @@ -112,6 +113,7 @@ pub fn get_build_options(test_config: &TestConfig) -> Vec { dce_enabled: true, conditional_block_max_depth: 10, disable_conditional_branch_type_checking: false, + build_tests: true, }], } } @@ -152,43 +154,30 @@ pub fn setup_build_directory( } pub fn new_compiler( - program_name: String, + name: String, handler: &Handler, - main_file_path: PathBuf, - compiler_options: Option, + sources: Vec<(FileName, String)>, + compiler_options: CompilerOptions, import_stubs: IndexMap, ) -> Compiler<'_, CurrentNetwork> { let output_dir = PathBuf::from("/tmp/output/"); fs::create_dir_all(output_dir.clone()).unwrap(); - Compiler::new( - program_name, - String::from("aleo"), - handler, - main_file_path, - output_dir, - compiler_options, - import_stubs, - ) + Compiler::new(name, handler, sources, output_dir, compiler_options, import_stubs) } pub fn parse_program<'a>( - program_name: String, + name: String, handler: &'a Handler, program_string: &str, cwd: Option, - compiler_options: Option, + compiler_options: CompilerOptions, import_stubs: IndexMap, ) -> Result, LeoError> { - let mut compiler = new_compiler( - program_name, - handler, - cwd.clone().unwrap_or_else(|| "compiler-test".into()), - compiler_options, - import_stubs, - ); - let name = cwd.map_or_else(|| FileName::Custom("compiler-test".into()), FileName::Real); - compiler.parse_program_from_string(program_string, name)?; + let file_name = cwd.map_or_else(|| FileName::Custom("compiler-test".into()), FileName::Real); + let mut compiler = + new_compiler(name, handler, vec![(file_name, program_string.into())], compiler_options, import_stubs); + compiler.parse()?; CheckUniqueNodeIds::new().visit_program(&compiler.ast.ast); diff --git a/compiler/parser/src/parser/file.rs b/compiler/parser/src/parser/file.rs index 043848b879..0ea86d343c 100644 --- a/compiler/parser/src/parser/file.rs +++ b/compiler/parser/src/parser/file.rs @@ -58,7 +58,7 @@ impl ParserContext<'_, N> { Ok(Program { imports, stubs: IndexMap::new(), program_scopes }) } - fn unexpected_item(token: &SpannedToken, expected: &[Token]) -> ParserError { + pub(super) fn unexpected_item(token: &SpannedToken, expected: &[Token]) -> ParserError { ParserError::unexpected( &token.token, expected.iter().map(|x| format!("'{x}'")).collect::>().join(", "), @@ -317,15 +317,42 @@ impl ParserContext<'_, N> { // TODO: Verify that this check is sound. // Check that there is no whitespace or comments in between the `@` symbol and identifier. - match identifier.span.hi.0 - start.lo.0 > 1 + identifier.name.to_string().len() as u32 { - true => Err(ParserError::space_in_annotation(span).into()), - false => Ok(Annotation { identifier, span, id: self.node_builder.next_id() }), + if identifier.span.hi.0 - start.lo.0 > 1 + identifier.name.to_string().len() as u32 { + return Err(ParserError::space_in_annotation(span).into()); } + + // Optionally parse the data associated with the annotation. + // The data is a comma-separated sequence of identifiers or identifiers with associated strings. + // For example, `@test(should_fail, private_key = "foobar")` + let (data, span) = match &self.token.token { + Token::LeftParen => { + let (data, _, span) = self.parse_paren_comma_list(|p| { + let key = p.expect_identifier()?; + let value = if p.eat(&Token::Assign) { + match &p.token.token { + Token::StaticString(s) => { + let value = s.clone(); + p.expect(&Token::StaticString(value.clone()))?; + Some(value) + } + _ => return Err(ParserError::expected_string_literal_in_annotation(p.token.span).into()), + } + } else { + None + }; + Ok(Some((key, value))) + })?; + (data.into_iter().collect(), span) + } + _ => (Default::default(), span), + }; + + Ok(Annotation { identifier, data, span, id: self.node_builder.next_id() }) } /// Returns an [`(Identifier, Function)`] AST node if the next tokens represent a function name /// and function definition. - fn parse_function(&mut self) -> Result<(Symbol, Function)> { + pub(super) fn parse_function(&mut self) -> Result<(Symbol, Function)> { // TODO: Handle dangling annotations. // Parse annotations, if they exist. let mut annotations = Vec::new(); @@ -335,7 +362,7 @@ impl ParserContext<'_, N> { // Parse a potential async signifier. let (is_async, start_async) = if self.token.token == Token::Async { (true, self.expect(&Token::Async)?) } else { (false, Span::dummy()) }; - // Parse ` IDENT`, where `` is `function`, `transition`, or `inline`. + // Parse ` IDENT`, where `` is `function`, `transition`, `inline`, or `interpret`. let (variant, start) = match self.token.token.clone() { Token::Inline => (Variant::Inline, self.expect(&Token::Inline)?), Token::Function => { @@ -345,7 +372,7 @@ impl ParserContext<'_, N> { if is_async { Variant::AsyncTransition } else { Variant::Transition }, self.expect(&Token::Transition)?, ), - _ => self.unexpected("'function', 'transition', or 'inline'")?, + _ => self.unexpected("'function', 'transition', 'inline', or 'interpret'")?, }; let name = self.expect_identifier()?; diff --git a/compiler/passes/src/lib.rs b/compiler/passes/src/lib.rs index d77f14647d..66b456e59d 100644 --- a/compiler/passes/src/lib.rs +++ b/compiler/passes/src/lib.rs @@ -41,6 +41,9 @@ pub use function_inlining::*; pub mod loop_unrolling; pub use self::loop_unrolling::*; +pub mod test_manifest_generation; +pub use test_manifest_generation::*; + pub mod pass; pub use self::pass::*; diff --git a/compiler/passes/src/test_manifest_generation/generator.rs b/compiler/passes/src/test_manifest_generation/generator.rs new file mode 100644 index 0000000000..13b76378a3 --- /dev/null +++ b/compiler/passes/src/test_manifest_generation/generator.rs @@ -0,0 +1,153 @@ +// Copyright (C) 2019-2024 Aleo Systems Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use super::*; +use snarkvm::prelude::{Itertools, PrivateKey}; +use std::str::FromStr; + +use leo_ast::{ExpressionVisitor, Function, ProgramId, ProgramScope, StatementVisitor, TestManifest, TestMetadata}; +use leo_errors::TestError; +use leo_span::{Symbol, sym}; + +pub struct TestManifestGenerator<'a, N: Network> { + // The error handler. + handler: &'a Handler, + // The manifest we are currently generating. + pub manifest: Option>, +} + +impl<'a, N: Network> TestManifestGenerator<'a, N> { + /// Initial a new instance of the test manifest generator. + pub fn new(handler: &'a Handler) -> Self { + Self { handler, manifest: None } + } + + /// Initialize the manifest. + pub fn initialize_manifest(&mut self, program_id: &ProgramId) { + self.manifest = Some(TestManifest::new(program_id)); + } +} + +impl<'a, N: Network> ProgramVisitor<'a> for TestManifestGenerator<'a, N> { + fn visit_program_scope(&mut self, input: &'a ProgramScope) { + // Initialize a new manifest. + self.initialize_manifest(&input.program_id); + // Visit the functions in the program scope. + input.functions.iter().for_each(|(_, c)| (self.visit_function(c))); + } + + fn visit_function(&mut self, input: &'a Function) { + // Find all of the test annotations. + let test_annotations = input.annotations.iter().filter(|a| a.identifier.name == sym::test).collect_vec(); + + // Validate the number and usage of test annotations. + match test_annotations.len() { + 0 => return, + 1 => { + // Check that the function is a transition. + if !input.variant.is_transition() { + self.handler.emit_err(TestError::non_transition_test(input.span)); + } + } + _ => { + self.handler.emit_err(TestError::multiple_test_annotations(input.span)); + return; + } + } + + // Get the test annotation. + let test_annotation = test_annotations[0]; + + // Initialize the private key. + let mut private_key = None; + // Initialize the seed. + let mut seed = None; + // Initialize the should fail flag. + let mut should_fail = false; + + // Check the annotation body. + for (key, value) in test_annotation.data.iter() { + // Check that the key and associated value is valid. + if key.name == Symbol::intern("private_key") { + // Attempt to parse the value as a private key. + match value { + None => self.handler.emit_err(TestError::missing_annotation_value( + test_annotation.identifier, + key, + key.span, + )), + Some(string) => match PrivateKey::::from_str(string) { + Ok(pk) => private_key = Some(pk), + Err(err) => self.handler.emit_err(TestError::invalid_annotation_value( + test_annotation.identifier, + key, + string, + err, + key.span, + )), + }, + } + } else if key.name == Symbol::intern("seed") { + // Attempt to parse the value as a u64. + match value { + None => self.handler.emit_err(TestError::missing_annotation_value( + test_annotation.identifier, + key, + key.span, + )), + Some(string) => match string.parse::() { + Ok(s) => seed = Some(s), + Err(err) => self.handler.emit_err(TestError::invalid_annotation_value( + test_annotation.identifier, + key, + string, + err, + key.span, + )), + }, + } + } else if key.name == Symbol::intern("should_fail") { + // Check that there is no value associated with the key. + if let Some(string) = value { + self.handler.emit_err(TestError::unexpected_annotation_value( + test_annotation.identifier, + key, + string, + key.span, + )); + } + should_fail = true; + } else { + self.handler.emit_err(TestError::unknown_annotation_key(test_annotation.identifier, key, key.span)) + } + } + + // Add the test to the manifest. + self.manifest.as_mut().unwrap().add_test(TestMetadata { + function_name: input.identifier.to_string(), + private_key, + seed, + should_fail, + }); + } +} + +impl<'a, N: Network> StatementVisitor<'a> for TestManifestGenerator<'a, N> {} + +impl<'a, N: Network> ExpressionVisitor<'a> for TestManifestGenerator<'a, N> { + type AdditionalInput = (); + type Output = (); +} diff --git a/compiler/passes/src/test_manifest_generation/mod.rs b/compiler/passes/src/test_manifest_generation/mod.rs new file mode 100644 index 0000000000..d6133ee78f --- /dev/null +++ b/compiler/passes/src/test_manifest_generation/mod.rs @@ -0,0 +1,43 @@ +// Copyright (C) 2019-2024 Aleo Systems Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +mod generator; +pub use generator::TestManifestGenerator; + +use crate::Pass; + +use leo_ast::{Ast, ProgramVisitor, TestManifest}; +use leo_errors::{Result, emitter::Handler}; + +use snarkvm::prelude::Network; + +impl<'a, N: Network> Pass for TestManifestGenerator<'a, N> { + type Input = (&'a Ast, &'a Handler); + type Output = Result>; + + fn do_pass((ast, handler): Self::Input) -> Self::Output { + let mut visitor = TestManifestGenerator::::new(handler); + visitor.visit_program(ast.as_repr()); + + handler.last_err().map_err(|e| *e)?; + + // Get the generated manifest. + let Some(manifest) = visitor.manifest.take() else { + unreachable!("Every test program should have an associated manifest") + }; + Ok(manifest) + } +} diff --git a/compiler/passes/src/type_checking/check_program.rs b/compiler/passes/src/type_checking/check_program.rs index f06433bcb0..4328b2e890 100644 --- a/compiler/passes/src/type_checking/check_program.rs +++ b/compiler/passes/src/type_checking/check_program.rs @@ -237,10 +237,8 @@ impl<'a, N: Network> ProgramVisitor<'a> for TypeChecker<'a, N> { fn visit_function(&mut self, function: &'a Function) { // Check that the function's annotations are valid. - // Note that Leo does not natively support any specific annotations. for annotation in function.annotations.iter() { - // TODO: Change to compiler warning. - self.emit_err(TypeCheckerError::unknown_annotation(annotation, annotation.span)) + self.check_annotation(annotation); } // Set type checker variables for function variant details. diff --git a/compiler/passes/src/type_checking/checker.rs b/compiler/passes/src/type_checking/checker.rs index c2a6d9a869..147992db6c 100644 --- a/compiler/passes/src/type_checking/checker.rs +++ b/compiler/passes/src/type_checking/checker.rs @@ -26,7 +26,7 @@ use crate::{ use leo_ast::*; use leo_errors::{TypeCheckerError, TypeCheckerWarning, emitter::Handler}; -use leo_span::{Span, Symbol}; +use leo_span::{Span, Symbol, sym}; use snarkvm::console::network::Network; @@ -1207,8 +1207,15 @@ impl<'a, N: Network> TypeChecker<'a, N> { // Check that the function context matches. if self.scope_state.variant == Some(Variant::AsyncFunction) && !finalize_op { self.handler.emit_err(TypeCheckerError::invalid_operation_inside_finalize(name, span)) - } else if self.scope_state.variant != Some(Variant::AsyncFunction) && finalize_op { + } else if finalize_op && !matches!(self.scope_state.variant, Some(Variant::AsyncFunction)) { self.handler.emit_err(TypeCheckerError::invalid_operation_outside_finalize(name, span)) } } + + // Check if the annotation is valid. + pub(crate) fn check_annotation(&mut self, annotation: &Annotation) { + if annotation.identifier.name != sym::test { + self.emit_warning(TypeCheckerWarning::unknown_annotation(annotation.identifier, annotation.span)); + } + } } diff --git a/compiler/span/src/symbol.rs b/compiler/span/src/symbol.rs index bd22d4704b..aa25f7c9c3 100644 --- a/compiler/span/src/symbol.rs +++ b/compiler/span/src/symbol.rs @@ -236,6 +236,10 @@ symbols! { False: "false", True: "true", + // annotations + should_fail, + test, + // general keywords As: "as", assert, @@ -255,6 +259,7 @@ symbols! { increment, inline, input, + interpret, Let: "let", leo, main, diff --git a/errors/src/errors/cli/cli_errors.rs b/errors/src/errors/cli/cli_errors.rs index 710136e25d..0ad5d805ea 100644 --- a/errors/src/errors/cli/cli_errors.rs +++ b/errors/src/errors/cli/cli_errors.rs @@ -320,4 +320,18 @@ create_messages!( msg: format!("Failed to render table.\nError: {error}"), help: None, } + + @backtraced + general_cli_error { + args: (error: impl Display), + msg: format!("{error}"), + help: None, + } + + @backtraced + general_cli_error_with_help { + args: (error: impl Display, help: impl Display), + msg: format!("{error}"), + help: Some(format!("{help}")), + } ); diff --git a/errors/src/errors/compiler/compiler_errors.rs b/errors/src/errors/compiler/compiler_errors.rs index e92adfd80c..8590d3ede8 100644 --- a/errors/src/errors/compiler/compiler_errors.rs +++ b/errors/src/errors/compiler/compiler_errors.rs @@ -73,8 +73,8 @@ create_messages!( @formatted imported_program_not_found { - args: (main_program_name: impl Display, dependency_name: impl Display), - msg: format!("`{main_program_name}` imports `{dependency_name}.aleo`, but `{dependency_name}.aleo` is not found in program manifest. Use `leo add --help` for more information on how to add a dependency."), - help: None, + args: (dependency_name: impl Display), + msg: format!("`{dependency_name}.aleo` is not found in program manifest."), + help: Some("Use `leo add --help` for more information on how to add a dependency.".to_string()), } ); diff --git a/errors/src/errors/mod.rs b/errors/src/errors/mod.rs index ac602c91db..498de81669 100644 --- a/errors/src/errors/mod.rs +++ b/errors/src/errors/mod.rs @@ -52,6 +52,10 @@ pub use self::parser::*; pub mod static_analyzer; pub use self::static_analyzer::*; +/// Contains the Test error definitions. +pub mod test; +pub use self::test::*; + /// Contains the Type Checker error definitions. pub mod type_checker; pub use self::type_checker::*; @@ -84,6 +88,9 @@ pub enum LeoError { /// Represents a Static Analyzer Error in a Leo Error. #[error(transparent)] StaticAnalyzerError(#[from] StaticAnalyzerError), + /// Represents a Test Error in a Leo Error. + #[error(transparent)] + TestError(#[from] TestError), /// Represents a Type Checker Error in a Leo Error. #[error(transparent)] TypeCheckerError(#[from] TypeCheckerError), @@ -117,6 +124,7 @@ impl LeoError { ParserError(error) => error.error_code(), PackageError(error) => error.error_code(), StaticAnalyzerError(error) => error.error_code(), + TestError(error) => error.error_code(), TypeCheckerError(error) => error.error_code(), LoopUnrollerError(error) => error.error_code(), FlattenError(error) => error.error_code(), @@ -138,6 +146,7 @@ impl LeoError { ParserError(error) => error.exit_code(), PackageError(error) => error.exit_code(), StaticAnalyzerError(error) => error.exit_code(), + TestError(error) => error.exit_code(), TypeCheckerError(error) => error.exit_code(), LoopUnrollerError(error) => error.exit_code(), FlattenError(error) => error.exit_code(), diff --git a/errors/src/errors/package/package_errors.rs b/errors/src/errors/package/package_errors.rs index 7f1a12fa9a..d9cf21d14c 100644 --- a/errors/src/errors/package/package_errors.rs +++ b/errors/src/errors/package/package_errors.rs @@ -432,4 +432,11 @@ create_messages!( msg: format!("Failed to load leo project at path {path}"), help: Some("Make sure that the path is correct and that the project exists.".to_string()), } + + @backtraced + failed_to_create_test_directory { + args: (error: impl ErrorArg), + msg: format!("Failed to create test directory {error}."), + help: None, + } ); diff --git a/errors/src/errors/parser/parser_errors.rs b/errors/src/errors/parser/parser_errors.rs index 04a3707cfa..2f15a54a7c 100644 --- a/errors/src/errors/parser/parser_errors.rs +++ b/errors/src/errors/parser/parser_errors.rs @@ -372,4 +372,11 @@ create_messages!( msg: format!("Identifier {ident} is too long ({length} bytes; maximum is {max_length})"), help: None, } + + @formatted + expected_string_literal_in_annotation { + args: (), + msg: format!("Expected a string literal in annotation body"), + help: Some("The body of an annotation can either be an identifier or an identifier-string par. For example, `foo`, `foo = \"bar\"`".to_string()), + } ); diff --git a/errors/src/errors/test/mod.rs b/errors/src/errors/test/mod.rs new file mode 100644 index 0000000000..5e79bf4952 --- /dev/null +++ b/errors/src/errors/test/mod.rs @@ -0,0 +1,19 @@ +// Copyright (C) 2019-2024 Aleo Systems Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +/// This module contains the test error definitions. +pub mod test_error; +pub use self::test_error::*; diff --git a/errors/src/errors/test/test_error.rs b/errors/src/errors/test/test_error.rs new file mode 100644 index 0000000000..7ccd0c907a --- /dev/null +++ b/errors/src/errors/test/test_error.rs @@ -0,0 +1,74 @@ +// Copyright (C) 2019-2024 Aleo Systems Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use crate::create_messages; +use std::fmt::{Debug, Display}; + +create_messages!( + /// TestError enum that represents all the errors for the test framework + TestError, + code_mask: 8000i32, + code_prefix: "TST", + + @formatted + unknown_annotation_key { + args: (annotation: impl Display, key: impl Display), + msg: format!("Unknown key `{key}` in test annotation `{annotation}`."), + help: None, + } + + @formatted + missing_annotation_value { + args: (annotation: impl Display, key: impl Display), + msg: format!("Missing value for key `{key}` in test annotation `{annotation}`."), + help: None, + } + + @formatted + invalid_annotation_value { + args: (annotation: impl Display, key: impl Display, value: impl Display, error: impl Display), + msg: format!("Invalid value `{value}` for key `{key}` in test annotation `{annotation}`. Error: {error}"), + help: None, + } + + @formatted + unexpected_annotation_value { + args: (annotation: impl Display, key: impl Display, value: impl Display), + msg: format!("Unexpected value `{value}` for key `{key}` in test annotation `{annotation}`."), + help: None, + } + + @formatted + multiple_test_annotations { + args: (), + msg: format!("Multiple test annotations found, only one is allowed."), + help: None, + } + + @formatted + non_transition_test { + args: (), + msg: format!("A test annotation is only allowed on transition functions."), + help: None, + } + + @backtraced + default_error { + args: (error: impl Display), + msg: format!("Test Error: {error}"), + help: None, + } +); diff --git a/errors/src/errors/type_checker/type_checker_error.rs b/errors/src/errors/type_checker/type_checker_error.rs index decbb0fe36..ece6276749 100644 --- a/errors/src/errors/type_checker/type_checker_error.rs +++ b/errors/src/errors/type_checker/type_checker_error.rs @@ -269,7 +269,7 @@ create_messages!( help: Some("Remove the code in the loop body that always returns.".to_string()), } - // TODO: Consider emitting a warning instead of an error. + // TODO This error is unused. Remove it in a future version. @formatted unknown_annotation { args: (annotation: impl Display), diff --git a/errors/src/errors/type_checker/type_checker_warning.rs b/errors/src/errors/type_checker/type_checker_warning.rs index 1ac294cba3..288ea94ed1 100644 --- a/errors/src/errors/type_checker/type_checker_warning.rs +++ b/errors/src/errors/type_checker/type_checker_warning.rs @@ -52,4 +52,11 @@ create_messages!( msg: format!("The type checker has exceeded the max depth of nested conditional blocks: {max}."), help: Some("Re-run with a larger maximum depth using the `--conditional_block_max_depth` build option. Ex: `leo run main --conditional_block_max_depth 25`.".to_string()), } + + @formatted + unknown_annotation { + args: (annotation: impl Display), + msg: format!("Unknown annotation: `{annotation}`."), + help: None, + } ); diff --git a/leo/cli/cli.rs b/leo/cli/cli.rs index 1b290bfa6e..6f6b10596b 100644 --- a/leo/cli/cli.rs +++ b/leo/cli/cli.rs @@ -102,6 +102,11 @@ enum Commands { #[clap(flatten)] command: LeoClean, }, + #[clap(about = "Execute native, interpreted, and end-to-end tests.")] + Test { + #[clap(flatten)] + command: LeoTest, + }, #[clap(about = "Update the Leo CLI")] Update { #[clap(flatten)] @@ -151,6 +156,7 @@ pub fn run_with_args(cli: CLI) -> Result<()> { Commands::Run { command } => command.try_execute(context), Commands::Execute { command } => command.try_execute(context), Commands::Remove { command } => command.try_execute(context), + Commands::Test { command } => command.try_execute(context), Commands::Update { command } => command.try_execute(context), } } @@ -416,8 +422,8 @@ function external_nested_function: command: LeoAdd { name: "nested_example_layer_0".to_string(), local: None, + dev: false, network: NETWORK.to_string(), - clear: false, }, }, path: Some(project_directory.clone()), @@ -528,8 +534,8 @@ program child.aleo { command: LeoAdd { name: "parent".to_string(), local: Some(parent_directory.clone()), + dev: false, network: NETWORK.to_string(), - clear: false, }, }, path: Some(grandparent_directory.clone()), @@ -543,8 +549,8 @@ program child.aleo { command: LeoAdd { name: "child".to_string(), local: Some(child_directory.clone()), + dev: false, network: NETWORK.to_string(), - clear: false, }, }, path: Some(grandparent_directory.clone()), @@ -558,8 +564,8 @@ program child.aleo { command: LeoAdd { name: "child".to_string(), local: Some(child_directory.clone()), + dev: false, network: NETWORK.to_string(), - clear: false, }, }, path: Some(parent_directory.clone()), @@ -700,8 +706,8 @@ program outer.aleo { command: LeoAdd { name: "inner_1".to_string(), local: Some(inner_1_directory.clone()), + dev: false, network: NETWORK.to_string(), - clear: false, }, }, path: Some(outer_directory.clone()), @@ -715,8 +721,8 @@ program outer.aleo { command: LeoAdd { name: "inner_2".to_string(), local: Some(inner_2_directory.clone()), + dev: false, network: NETWORK.to_string(), - clear: false, }, }, path: Some(outer_directory.clone()), @@ -887,8 +893,8 @@ program outer_2.aleo { command: LeoAdd { name: "inner_1".to_string(), local: Some(inner_1_directory.clone()), + dev: false, network: NETWORK.to_string(), - clear: false, }, }, path: Some(outer_directory.clone()), @@ -902,8 +908,8 @@ program outer_2.aleo { command: LeoAdd { name: "inner_2".to_string(), local: Some(inner_2_directory.clone()), + dev: false, network: NETWORK.to_string(), - clear: false, }, }, path: Some(outer_directory.clone()), diff --git a/leo/cli/commands/add.rs b/leo/cli/commands/add.rs index f37604fee0..4c95e66637 100755 --- a/leo/cli/commands/add.rs +++ b/leo/cli/commands/add.rs @@ -28,11 +28,11 @@ pub struct LeoAdd { #[clap(short = 'l', long, help = "Path to local dependency")] pub(crate) local: Option, + #[clap(short = 'd', long, help = "Whether the dependency is a dev dependency", default_value = "false")] + pub(crate) dev: bool, + #[clap(short = 'n', long, help = "Name of the network to use", default_value = "testnet")] pub(crate) network: String, - - #[clap(short = 'c', long, help = "Clear all previous dependencies.", default_value = "false")] - pub(crate) clear: bool, } impl Command for LeoAdd { @@ -66,65 +66,64 @@ impl Command for LeoAdd { name => return Err(PackageError::invalid_file_name_dependency(name).into()), }; - // Add dependency section to manifest if it doesn't exist. - let mut dependencies = match (self.clear, manifest.dependencies()) { - (false, Some(ref dependencies)) => dependencies - .iter() - .filter_map(|dependency| { - // Overwrite old dependencies of the same name. - if dependency.name() == &name { - let msg = match (dependency.path(), dependency.network()) { - (Some(local_path), _) => { - format!("local dependency at path `{}`", local_path.to_str().unwrap().replace('\"', "")) - } - (_, Some(network)) => { - format!("network dependency from `{}`", network) - } - _ => "git dependency".to_string(), - }; - tracing::warn!("⚠️ Program `{name}` already exists as a {msg}. Overwriting."); - None - } else if self.local.is_some() && &self.local == dependency.path() { - // Overwrite old dependencies at the same local path. - tracing::warn!( - "⚠️ Path `{}` already exists as the location for local dependency `{}`. Overwriting.", - self.local.clone().unwrap().to_str().unwrap().replace('\"', ""), - dependency.name() - ); - None - } else { - Some(dependency.clone()) - } - }) - .collect(), - _ => Vec::new(), - }; + // Destructure the manifest. + let Manifest { program, version, description, license, dependencies, dev_dependencies } = manifest; - // Add new dependency to the manifest. - dependencies.push(match self.local { - Some(local_path) => { - tracing::info!( - "✅ Added local dependency to program `{name}` at path `{}`.", - local_path.to_str().unwrap().replace('\"', "") - ); - Dependency::new(name, Location::Local, None, Some(local_path)) - } - None => { - tracing::info!("✅ Added network dependency to program `{name}` from network `{}`.", self.network); - Dependency::new(name, Location::Network, Some(NetworkName::try_from(self.network.as_str())?), None) - } - }); + // Add the dependency to the appropriate section. + let (dependencies, dev_dependencies) = if self.dev { + (dependencies, add_dependency(dev_dependencies, name, self.local, self.network)?) + } else { + (add_dependency(dependencies, name, self.local, self.network)?, dev_dependencies) + }; // Update the manifest file. let new_manifest = Manifest::new( - manifest.program(), - manifest.version(), - manifest.description(), - manifest.license(), - Some(dependencies), + program.as_str(), + version.as_str(), + description.as_str(), + license.as_str(), + dependencies, + dev_dependencies, ); new_manifest.write_to_dir(&path)?; Ok(()) } } + +// A helper function to add a dependency to either the `dependencies` or `dev_dependencies` section of the manifest. +fn add_dependency( + dependencies: Option>, + name: String, + location: Option, + network: String, +) -> Result>> { + // Check if the dependency already exists, returning the original list if it does. + let mut dependencies = if let Some(dependencies) = dependencies { + if dependencies.iter().any(|dependency| dependency.name() == &name) { + tracing::warn!( + "⚠️ Program `{name}` already exists as a dependency. If you wish to update it, explicitly remove it using `leo remove` and add it again." + ); + return Ok(Some(dependencies)); + } + dependencies + } else { + Vec::new() + }; + // Add the new dependency to the list. + dependencies.push(match location { + Some(local_path) => { + tracing::info!( + "✅ Added local dependency to program `{name}` at path `{}`.", + local_path.to_str().unwrap().replace('\"', "") + ); + Dependency::new(name, Location::Local, None, Some(local_path)) + } + None => { + tracing::info!("✅ Added network dependency to program `{name}` from network `{network}`."); + Dependency::new(name, Location::Network, Some(NetworkName::try_from(network.as_str())?), None) + } + }); + // Return the updated list of dependencies. + Ok(Some(dependencies)) +} diff --git a/leo/cli/commands/build.rs b/leo/cli/commands/build.rs index 588ca33a63..cc8acff8a5 100644 --- a/leo/cli/commands/build.rs +++ b/leo/cli/commands/build.rs @@ -29,7 +29,9 @@ use snarkvm::{ }; use indexmap::IndexMap; -use snarkvm::prelude::CanaryV0; +use leo_package::tst::TestDirectory; +use leo_span::source_map::FileName; +use snarkvm::prelude::{CanaryV0, Itertools, Program}; use std::{ io::Write, path::{Path, PathBuf}, @@ -43,6 +45,7 @@ impl From for CompilerOptions { dce_enabled: options.enable_dce, conditional_block_max_depth: options.conditional_block_max_depth, disable_conditional_branch_type_checking: options.disable_conditional_branch_type_checking, + build_tests: options.build_tests, }, output: OutputOptions { symbol_table_spans_enabled: options.enable_symbol_table_spans, @@ -141,11 +144,18 @@ fn handle_build(command: &LeoBuild, context: Context) -> Result<(command: &LeoBuild, context: Context) -> Result<::try_from(format!("{}.aleo", dependency)) - .map_err(|_| UtilError::snarkvm_error_building_program_id(Default::default()))?, - &local_outputs_directory, - &local_build_directory, - &handler, - command.options.clone(), - stubs.clone(), - )?; - } + // Compile the sources. + compile_leo_files::( + dependency.to_string(), + local_source_files, + &local_outputs_directory, + &local_build_directory, + &handler, + command.options.clone(), + stubs.clone(), + )?; } // Writes `leo.lock` as well as caches objects (when target is an intermediate dependency) @@ -179,39 +186,44 @@ fn handle_build(command: &LeoBuild, context: Context) -> Result<::open(&build_directory).map_err(CliError::failed_to_execute_build)?; + let package = Package::::open(&build_directory).map_err(CliError::failed_to_execute_build)?; + + // Add the main program as a stub. + main_stubs.insert(main_sym, leo_disassembler::disassemble(package.program())); + // If the `build_tests` flag is set, compile the tests. + if command.options.build_tests { + // Compile the tests. + compile_tests::(main_sym.to_string(), &package_path, &handler, command.options.clone(), main_stubs.clone())?; + } Ok(()) } -/// Compiles a Leo file in the `src/` directory. +/// Compiles Leo files in the `src/` directory. #[allow(clippy::too_many_arguments)] -fn compile_leo_file( - file_path: PathBuf, - program_id: &ProgramID, +fn compile_leo_files( + name: String, + local_source_files: Vec, outputs: &Path, build: &Path, handler: &Handler, options: BuildOptions, stubs: IndexMap, ) -> Result<()> { - // Construct program name from the program_id found in `package.json`. - let program_name = program_id.name().to_string(); + // Read the files and collect it into sources. + let mut sources = Vec::with_capacity(local_source_files.len()); + for file_path in local_source_files.iter() { + let file_content = std::fs::read_to_string(file_path.clone()) + .map_err(|_| CliError::general_cli_error("Failed to read source files."))?; // Read the file content. + sources.push((FileName::Real(file_path.clone()), file_content)); + } // Create the path to the Aleo file. let mut aleo_file_path = build.to_path_buf(); - aleo_file_path.push(format!("main.{}", program_id.network())); + aleo_file_path.push("main.aleo"); // Create a new instance of the Leo compiler. - let mut compiler = Compiler::::new( - program_name.clone(), - program_id.network().to_string(), - handler, - file_path.clone(), - outputs.to_path_buf(), - Some(options.into()), - stubs, - ); + let mut compiler = Compiler::::new(name.clone(), handler, sources, outputs.to_path_buf(), options.into(), stubs); // Compile the Leo program into Aleo instructions. let instructions = compiler.compile()?; @@ -222,6 +234,108 @@ fn compile_leo_file( .write_all(instructions.as_bytes()) .map_err(CliError::failed_to_load_instructions)?; - tracing::info!("✅ Compiled '{program_name}.aleo' into Aleo instructions"); + tracing::info!("✅ Compiled sources for '{name}'"); + Ok(()) +} + +/// Compiles test files in the `tests/` directory. +#[allow(clippy::too_many_arguments)] +fn compile_tests( + name: String, + package_path: &Path, + handler: &Handler, + options: BuildOptions, + stubs: IndexMap, +) -> Result<()> { + // Get the files in `/tests` directory. + let test_files = TestDirectory::files(package_path)?; + + // Create a subdirectory for the built tests. + let build_dir = BuildDirectory::open(package_path)?; + let test_dir = build_dir.join("tests"); + std::fs::create_dir_all(&test_dir) + .map_err(|_| CliError::general_cli_error("Failed to create directory for the built tests."))?; + + // Get the program ID and programs in the built package. + let mut programs = Vec::new(); + // Read the main program. + let package = Package::open(build_dir.as_path())?; + programs.push(package.program().clone()); + // Read the imported programs if they exist. + let entries = std::fs::read_dir(package.imports_directory()) + .map_or(vec![], |entries| entries.into_iter().flatten().collect_vec()); + for entry in entries { + // Read the file if it is a `.aleo` file. + let path = entry.path(); + if path.is_file() && path.extension().map_or(false, |ext| ext == "aleo") { + // Read the file. + let program = std::fs::read_to_string(path.clone()) + .map_err(|_| CliError::general_cli_error("Failed to read the built package."))?; + // Parse the program. + let program = Program::::from_str(&program)?; + // Push the program. + programs.push(program); + } + } + + // Construct the compiler. + let mut compiler = Compiler::::new( + "tests".to_string(), + handler, + vec![], + PathBuf::from("build/tests"), + options.into(), + stubs.clone(), + ); + + // Read and compile the test files individually. + // For each test file, we create a new package. + // This is because tests files themselves are valid Leo programs that can be deployed and executed. + // A test program with the name `foo.aleo` will be compiled to package at `build/tests/foo.aleo`. + for file_path in test_files { + // Read the test file. + let file_content = std::fs::read_to_string(&file_path) + .map_err(|_| CliError::general_cli_error("Failed to read test file."))?; + + // Reset the compiler with the test file content. + compiler.reset(vec![(FileName::Real(file_path.clone()), file_content)]); + + // Compile the test file. + let (output, test_manifest) = compiler.compile_tests()?; + // Parse the program. + let program = Program::::from_str(&output)?; + // Get the program ID. + let program_id = program.id(); + + // Initialize the test package path. + let test_package_name = program_id.name().to_string(); + let test_package_path = test_dir.join(test_package_name); + + // Initialize a new package. + Package::create(&test_package_path, program_id)?; + + // Write the program to the `main.aleo` file in the test package. + let main_file_path = test_package_path.join("main.aleo"); + std::fs::write(&main_file_path, output) + .map_err(|_| CliError::general_cli_error("Failed to write test file."))?; + + // Write the programs to the imports in the test package. + let imports_dir = test_package_path.join("imports"); + std::fs::create_dir_all(&imports_dir) + .map_err(|_| CliError::general_cli_error("Failed to write test dependencies."))?; + for program in programs.iter() { + let import_path = imports_dir.join(program.id().to_string()); + std::fs::write(import_path, program.to_string()) + .map_err(|_| CliError::general_cli_error("Failed to write test dependency file."))?; + } + + // Write the test manifest to `manifest.json` in the test package. + let manifest_file_path = test_package_path.join("manifest.json"); + let manifest_json = serde_json::to_string_pretty(&test_manifest) + .map_err(|_| CliError::general_cli_error("Failed to create test manifest"))?; + std::fs::write(&manifest_file_path, manifest_json) + .map_err(|_| CliError::general_cli_error("Failed to write test manifest"))?; + } + tracing::info!("✅ Compiled tests for '{name}'"); Ok(()) } diff --git a/leo/cli/commands/mod.rs b/leo/cli/commands/mod.rs index e9f4a60515..2d563bf73f 100644 --- a/leo/cli/commands/mod.rs +++ b/leo/cli/commands/mod.rs @@ -53,23 +53,30 @@ pub use remove::LeoRemove; pub mod run; pub use run::LeoRun; +pub mod test; +pub use test::LeoTest; + pub mod update; pub use update::LeoUpdate; use super::*; -use crate::cli::helpers::context::*; +use crate::cli::{helpers::context::*, query::QueryCommands}; + use leo_errors::{CliError, PackageError, Result, emitter::Handler}; use leo_package::{build::*, outputs::OutputsDirectory, package::*}; -use snarkvm::prelude::{Address, Ciphertext, Plaintext, PrivateKey, Record, ViewKey, block::Transaction}; +use leo_retriever::NetworkName; + +use snarkvm::{ + circuit::{Aleo, AleoCanaryV0, AleoTestnetV0, AleoV0}, + console::network::Network, + prelude::{Address, Ciphertext, Plaintext, PrivateKey, Record, ViewKey, block::Transaction}, +}; use clap::Parser; use colored::Colorize; use std::str::FromStr; use tracing::span::Span; -use crate::cli::query::QueryCommands; -use snarkvm::console::network::Network; - /// Base trait for the Leo CLI, see methods and their documentation for details. pub trait Command { /// If the current command requires running another command beforehand @@ -176,6 +183,8 @@ pub struct BuildOptions { pub conditional_block_max_depth: usize, #[clap(long, help = "Disable type checking of nested conditional branches in finalize scope.")] pub disable_conditional_branch_type_checking: bool, + #[clap(long, help = "Build the test programs as well.", default_value = "false")] + pub build_tests: bool, } impl Default for BuildOptions { @@ -201,6 +210,7 @@ impl Default for BuildOptions { enable_dce_ast_snapshot: false, conditional_block_max_depth: 10, disable_conditional_branch_type_checking: false, + build_tests: true, } } } diff --git a/leo/cli/commands/remove.rs b/leo/cli/commands/remove.rs index 4807991971..3b8e1501b0 100644 --- a/leo/cli/commands/remove.rs +++ b/leo/cli/commands/remove.rs @@ -28,6 +28,9 @@ pub struct LeoRemove { )] pub(crate) name: Option, + #[clap(short = 'd', long, help = "Whether the dependency is a dev dependency", default_value = "false")] + pub(crate) dev: bool, + #[clap(long, help = "Clear all previous dependencies.", default_value = "false")] pub(crate) all: bool, } @@ -54,58 +57,49 @@ impl Command for LeoRemove { let manifest: Manifest = serde_json::from_str(&program_data) .map_err(|err| PackageError::failed_to_deserialize_manifest_file(path.to_str().unwrap(), err))?; - let dependencies: Vec = if !self.all { - // Note that this unwrap is safe since `name` is required if `all` is `false`. - let name: String = self.name.unwrap().clone(); - - let mut found_match = false; - let dep = match manifest.dependencies() { - Some(ref dependencies) => dependencies - .iter() - .filter_map(|dependency| { - if dependency.name() == &name { - found_match = true; - let msg = match (dependency.path(), dependency.network()) { - (Some(local_path), _) => format!( - "local dependency to `{}` from path `{}`", - name, - local_path.to_str().unwrap().replace('\"', "") - ), - (_, Some(network)) => { - format!("network dependency to `{}` from network `{}`", name, network) - } - _ => format!("git dependency to `{name}`"), - }; - tracing::warn!("✅ Successfully removed the {msg}."); - None - } else { - Some(dependency.clone()) - } - }) - .collect(), - _ => Vec::new(), - }; - - // Throw error if no match is found. - if !found_match { - return Err(PackageError::dependency_not_found(name).into()); - } + // Destructure the manifest. + let Manifest { program, version, description, license, dependencies, dev_dependencies } = manifest; - dep + // Add the dependency to the appropriate section. + let (dependencies, dev_dependencies) = if self.all { + if self.dev { (Some(Vec::new()), dev_dependencies) } else { (dependencies, Some(Vec::new())) } } else { - Vec::new() + // Note that this unwrap is safe since `name` is required if `all` is `false`. + let name = self.name.unwrap(); + if self.dev { + (dependencies, remove_dependency(dev_dependencies, name)?) + } else { + (remove_dependency(dependencies, name)?, dev_dependencies) + } }; // Update the manifest file. let new_manifest = Manifest::new( - manifest.program(), - manifest.version(), - manifest.description(), - manifest.license(), - Some(dependencies), + program.as_str(), + version.as_str(), + description.as_str(), + license.as_str(), + dependencies, + dev_dependencies, ); new_manifest.write_to_dir(&path)?; Ok(()) } } + +// A helper function to remove a dependency from either the `dependencies` or `dev_dependencies` section of the manifest. +fn remove_dependency(dependencies: Option>, name: String) -> Result>> { + // Remove the dependency from the list, returning an error if it was not found. + match dependencies { + None => Err(PackageError::dependency_not_found(name).into()), + Some(mut dependencies) => { + if let Some(index) = dependencies.iter().position(|dep| dep.name() == &name) { + dependencies.remove(index); + Ok(Some(dependencies)) + } else { + Err(PackageError::dependency_not_found(name).into()) + } + } + } +} diff --git a/leo/cli/commands/test/mod.rs b/leo/cli/commands/test/mod.rs new file mode 100644 index 0000000000..c15484a3ac --- /dev/null +++ b/leo/cli/commands/test/mod.rs @@ -0,0 +1,307 @@ +// Copyright (C) 2019-2024 Aleo Systems Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +mod result; +use result::*; + +mod utilities; +use utilities::*; + +use super::*; + +use leo_ast::TestManifest; +use leo_errors::TestError; + +use snarkvm::prelude::{Itertools, Program, Value, execution_cost_v1, execution_cost_v2}; + +use rand::{Rng, SeedableRng, rngs::OsRng}; +use rand_chacha::ChaChaRng; +use rayon::{ThreadPoolBuilder, prelude::*}; + +/// Build, Prove and Run Leo program with inputs +#[derive(Parser, Debug)] +pub struct LeoTest { + #[clap(name = "FILTER", help = "If specified, only run tests containing this string in their names.")] + filter: Option, + #[clap(long, help = "Compile, but don't run the tests", default_value = "false")] + no_run: bool, + #[clap(long, help = "Run all tests regardless of failure.", default_value = "false")] + no_fail_fast: bool, + #[clap(short, long, help = "Number of parallel jobs, defaults to the number of CPUs.")] + jobs: Option, + #[clap(flatten)] + compiler_options: BuildOptions, +} + +impl Command for LeoTest { + type Input = ::Output; + type Output = (); + + fn log_span(&self) -> Span { + tracing::span!(tracing::Level::INFO, "Leo") + } + + fn prelude(&self, context: Context) -> Result { + // Set `build_tests` to `true` to ensure that the tests are built. + let mut options = self.compiler_options.clone(); + options.build_tests = true; + (LeoBuild { options }).execute(context) + } + + fn apply(self, context: Context, _input: Self::Input) -> Result { + // Parse the network. + let network = NetworkName::try_from(context.get_network(&self.compiler_options.network)?)?; + match network { + NetworkName::TestnetV0 => handle_test::(self, context), + NetworkName::MainnetV0 => { + #[cfg(feature = "only_testnet")] + panic!("Mainnet chosen with only_testnet feature"); + #[cfg(not(feature = "only_testnet"))] + return handle_test::(self, context); + } + NetworkName::CanaryV0 => { + #[cfg(feature = "only_testnet")] + panic!("Canary chosen with only_testnet feature"); + #[cfg(not(feature = "only_testnet"))] + return handle_test::(self, context); + } + } + } +} + +// A helper function to handle the `test` command. +fn handle_test(command: LeoTest, context: Context) -> Result<::Output> { + // Select the number of jobs, defaulting to the number of CPUs. + let num_cpus = num_cpus::get(); + let jobs = command.jobs.unwrap_or(num_cpus); + + // Initialize the Rayon thread pool. + ThreadPoolBuilder::new().num_threads(jobs).build_global().map_err(TestError::default_error)?; + + // Get the individual test directories within the build directory at `/build/tests` + let package_path = context.dir()?; + let build_directory = BuildDirectory::open(&package_path)?; + let tests_directory = build_directory.join("tests"); + let test_directories = std::fs::read_dir(tests_directory) + .map_err(TestError::default_error)? + .flat_map(|dir_entry| dir_entry.map(|dir_entry| dir_entry.path())) + .collect_vec(); + + println!("Running {} tests...", test_directories.len()); + + // For each test package within the tests directory: + // - Open the manifest. + // - Initialize the VM. + // - Run each test within the manifest sequentially. + let results: Vec<_> = test_directories + .into_par_iter() + .map(|path| -> String { + // The default bug message. + const BUG_MESSAGE: &str = + "This is a bug, please file an issue at https://github.com/ProvableHQ/leo/issues/new?template=0_bug.md"; + // Load the `manifest.json` within the test directory. + let manifest_path = path.join("manifest.json"); + let manifest_json = match std::fs::read_to_string(&manifest_path) { + Ok(manifest_json) => manifest_json, + Err(e) => return format!("Failed to read manifest.json: {e}. {BUG_MESSAGE}"), + }; + let manifest: TestManifest = match serde_json::from_str(&manifest_json) { + Ok(manifest) => manifest, + Err(e) => return format!("Failed to parse manifest.json: {e}. {BUG_MESSAGE}"), + }; + + // Get the programs in the package in the following order: + // - If the `imports` directory exists, load the programs from the `imports` directory. + // - Load the main program from the package. + let mut program_paths = Vec::new(); + let imports_directory = path.join("imports"); + if let Ok(dir) = std::fs::read_dir(&imports_directory) { + if let Ok(paths) = + dir.map(|dir_entry| dir_entry.map(|dir_entry| dir_entry.path())).collect::, _>>() + { + program_paths.extend(paths); + } + }; + program_paths.push(path.join("main.aleo")); + + // Read and parse the programs. + let mut programs = Vec::with_capacity(program_paths.len()); + for path in program_paths { + let program_string = match std::fs::read_to_string(&path) { + Ok(program_string) => program_string, + Err(e) => return format!("Failed to read program: {e}. {BUG_MESSAGE}"), + }; + let program = match Program::::from_str(&program_string) { + Ok(program) => program, + Err(e) => return format!("Failed to parse program: {e}. {BUG_MESSAGE}"), + }; + programs.push(program); + } + + // Initialize the VM. + let (vm, genesis_private_key) = match initialize_vm(programs) { + Ok((vm, genesis_private_key)) => (vm, genesis_private_key), + Err(e) => return format!("Failed to initialize VM: {e}. {BUG_MESSAGE}"), + }; + + // Initialize the results object. + let mut results = TestResults::new(manifest.program_id.clone()); + + // Run each test within the manifest. + for test in manifest.tests { + // Get the full test name. + let test_name = format!("{}/{}", manifest.program_id, test.function_name); + + // If a filter is specified, skip the test if it does not contain the filter. + if let Some(filter) = &command.filter { + if !test_name.contains(filter) { + results.skip(test_name); + continue; + } + } + + // Get the seed if specified, otherwise use a random seed. + let seed = match test.seed { + Some(seed) => seed, + None => OsRng.gen(), + }; + + // Initialize the RNG. + let rng = &mut ChaChaRng::seed_from_u64(seed); + + // Use the private key if provided, otherwise initialize one using the RNG. + let private_key = match test.private_key { + Some(private_key) => private_key, + None => match PrivateKey::new(rng) { + Ok(private_key) => private_key, + Err(e) => { + results + .add_result(test_name, format!("Failed to generate private key: {e}. {BUG_MESSAGE}")); + continue; + } + }, + }; + + // Determine whether or not the function should fail. + let should_fail = test.should_fail; + + // Execute the function. + let inputs: Vec> = Vec::new(); + let authorization = match vm.process().read().authorize::( + &private_key, + &manifest.program_id, + &test.function_name, + inputs.iter(), + rng, + ) { + Ok(authorization) => authorization, + Err(e) => { + results.add_result(test_name, format!("Failed to authorize: {e}. {BUG_MESSAGE}")); + continue; + } + }; + let Transaction::Execute(_, execution, _) = + (match vm.execute_authorization(authorization, None, None, rng) { + Ok(transaction) => transaction, + Err(e) => { + // TODO (@d0cd) A failure here may not be a bug. + results.add_result(test_name, format!("Failed to execute: {e}. {BUG_MESSAGE}")); + continue; + } + }) + else { + unreachable!("VM::execute_authorization always produces an execution") + }; + let block_height = vm.block_store().current_block_height(); + let result = match block_height < A::Network::CONSENSUS_V2_HEIGHT { + true => execution_cost_v1(&vm.process().read(), &execution), + false => execution_cost_v2(&vm.process().read(), &execution), + }; + let base_fee_in_microcredits = match result { + Ok((total, _)) => total, + Err(e) => { + results.add_result(test_name, format!("Failed to get execution cost: {e}. {BUG_MESSAGE}")); + continue; + } + }; + let execution_id = match execution.to_execution_id() { + Ok(execution_id) => execution_id, + Err(e) => { + results.add_result(test_name, format!("Failed to get execution ID: {e}. {BUG_MESSAGE}")); + continue; + } + }; + let fee_authorization = + match vm.authorize_fee_public(&genesis_private_key, base_fee_in_microcredits, 0, execution_id, rng) + { + Ok(fee_authorization) => fee_authorization, + Err(e) => { + results.add_result(test_name, format!("Failed to authorize fee: {e}. {BUG_MESSAGE}")); + continue; + } + }; + let fee = match vm.execute_fee_authorization(fee_authorization, None, rng) { + Ok(transaction) => transaction, + Err(e) => { + results + .add_result(test_name, format!("Failed to execute fee authorization: {e}. {BUG_MESSAGE}")); + continue; + } + }; + let transaction = match Transaction::from_execution(execution, Some(fee)) { + Ok(transaction) => transaction, + Err(e) => { + results.add_result(test_name, format!("Failed to construct transaction: {e}. {BUG_MESSAGE}")); + continue; + } + }; + let is_verified = vm.check_transaction(&transaction, None, rng).is_ok(); + let (block, is_accepted) = match construct_next_block(&vm, &genesis_private_key, transaction, rng) { + Ok(block) => block, + Err(e) => { + results.add_result(test_name, format!("Failed to create block: {e}. {BUG_MESSAGE}")); + continue; + } + }; + + if let Err(e) = vm.add_next_block(&block) { + results.add_result(test_name, format!("Failed to add block: {e}. {BUG_MESSAGE}")); + continue; + } + + // Construct the result. + match (is_verified & is_accepted, should_fail) { + (true, true) => results.add_result(test_name, " ❌ Test passed but should have failed".to_string()), + (false, false) => { + results.add_result(test_name, " ❌ Test failed but should have passed".to_string()) + } + (true, false) => results.add_result(test_name, " ✅ Test passed as expected".to_string()), + (false, true) => results.add_result(test_name, " ✅ Test failed as expected".to_string()), + } + } + + // Return the results as a string. + results.to_string() + }) + .collect(); + + // Print the results. + for result in results { + println!("{result}"); + } + + Ok(()) +} diff --git a/leo/cli/commands/test/result.rs b/leo/cli/commands/test/result.rs new file mode 100644 index 0000000000..7d95cf2e18 --- /dev/null +++ b/leo/cli/commands/test/result.rs @@ -0,0 +1,55 @@ +// Copyright (C) 2019-2024 Aleo Systems Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +/// The results of running a test program. +pub struct TestResults { + /// The name of the test program. + program_name: String, + /// The ran test functions and the associated result. + results: Vec<(String, String)>, + /// The skipped tests. + skipped: Vec, +} + +impl TestResults { + /// Initialize a new `TestResults` object. + pub fn new(program_name: String) -> Self { + Self { program_name, results: Default::default(), skipped: Default::default() } + } + + /// Add a function name and result to the results. + pub fn add_result(&mut self, function: String, result: String) { + self.results.push((function, result)) + } + + /// Add a skipped test to the results. + pub fn skip(&mut self, function: String) { + self.skipped.push(function) + } +} + +impl std::fmt::Display for TestResults { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Test results for program '{}':", self.program_name)?; + for (function, result) in &self.results { + writeln!(f, " Ran '{function}' | {result}")?; + } + for function in &self.skipped { + writeln!(f, " Skipped '{function}'")?; + } + Ok(()) + } +} diff --git a/leo/cli/commands/test/utilities.rs b/leo/cli/commands/test/utilities.rs new file mode 100644 index 0000000000..71f4b43a61 --- /dev/null +++ b/leo/cli/commands/test/utilities.rs @@ -0,0 +1,167 @@ +// Copyright (C) 2019-2024 Aleo Systems Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use super::*; + +use snarkvm::{ + ledger::store::helpers::memory::ConsensusMemory, + prelude::{ + Block, + CryptoRng, + Field, + Header, + Metadata, + Program, + Result, + VM, + Zero, + bail, + ensure, + store::{ConsensusStorage, ConsensusStore}, + }, + synthesizer::program::FinalizeGlobalState, +}; + +const GENESIS_PRIVATE_KEY: &str = "APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH"; + +pub(super) fn initialize_vm( + programs: Vec>, +) -> Result<(VM>, PrivateKey)> { + // Initialize an RNG with a fixed seed. + let rng = &mut ChaChaRng::seed_from_u64(123456789); + // Initialize the genesis private key. + let genesis_private_key = PrivateKey::::from_str(GENESIS_PRIVATE_KEY).unwrap(); + // Initialize the VM. + let vm = VM::>::from(ConsensusStore::>::open(None)?)?; + // Initialize the genesis block. + let genesis = vm.genesis_beacon(&genesis_private_key, rng)?; + // Add the genesis block to the VM. + vm.add_next_block(&genesis)?; + + // Deploy the programs to the VM. + for program in &programs { + // Create the deployment. + let deployment = vm.deploy(&genesis_private_key, program, None, 0, None, rng)?; + // Create a new block and check that the transaction was accepted. + let (block, is_accepted) = construct_next_block(&vm, &genesis_private_key, deployment, rng)?; + ensure!(is_accepted, "Failed to deploy the program"); + // Add the block to the VM. + vm.add_next_block(&block)?; + } + + Ok((vm, genesis_private_key)) +} + +// A helper function that takes a single transaction and creates a new block. +// The function also tells whether the transaction was accepted or rejected. +#[allow(clippy::too_many_arguments)] +pub(super) fn construct_next_block, R: Rng + CryptoRng>( + vm: &VM, + private_key: &PrivateKey, + transaction: Transaction, + rng: &mut R, +) -> Result<(Block, bool)> { + // Speculate on the transaction. + let time_since_last_block = N::BLOCK_TIME as i64; + let (ratifications, transactions, aborted_transaction_ids, ratified_finalize_operations) = vm.speculate( + construct_finalize_global_state(&vm)?, + time_since_last_block, + Some(0u64), + vec![], + &None.into(), + [transaction].iter(), + rng, + )?; + let is_accepted = match transactions.iter().next() { + Some(confirmed_transaction) => confirmed_transaction.is_accepted(), + None => false, + }; + + // Get the most recent block. + let block_hash = vm.block_store().get_block_hash(vm.block_store().max_height().unwrap()).unwrap().unwrap(); + let previous_block = vm.block_store().get_block(&block_hash).unwrap().unwrap(); + + // Construct the metadata associated with the block. + let metadata = Metadata::new( + N::ID, + previous_block.round() + 1, + previous_block.height() + 1, + 0, + 0, + N::GENESIS_COINBASE_TARGET, + N::GENESIS_PROOF_TARGET, + previous_block.last_coinbase_target(), + previous_block.last_coinbase_timestamp(), + previous_block.timestamp().saturating_add(time_since_last_block), + )?; + // Construct the block header. + let header = Header::from( + vm.block_store().current_state_root(), + transactions.to_transactions_root().unwrap(), + transactions.to_finalize_root(ratified_finalize_operations).unwrap(), + ratifications.to_ratifications_root().unwrap(), + Field::zero(), + Field::zero(), + metadata, + )?; + + // Construct the new block. + Ok(( + Block::new_beacon( + private_key, + previous_block.hash(), + header, + ratifications, + None.into(), + vec![], + transactions, + aborted_transaction_ids, + rng, + )?, + is_accepted, + )) +} + +// A helper function to construct the `FinalizeGlobalState` from the current `VM` state. +fn construct_finalize_global_state>(vm: &VM) -> Result { + // Retrieve the latest block. + let block_height = match vm.block_store().max_height() { + Some(height) => height, + None => bail!("Failed to retrieve the latest block height"), + }; + let latest_block_hash = match vm.block_store().get_block_hash(block_height)? { + Some(hash) => hash, + None => bail!("Failed to retrieve the latest block hash"), + }; + let latest_block = match vm.block_store().get_block(&latest_block_hash)? { + Some(block) => block, + None => bail!("Failed to retrieve the latest block"), + }; + // Retrieve the latest round. + let latest_round = latest_block.round(); + // Retrieve the latest height. + let latest_height = latest_block.height(); + // Retrieve the latest cumulative weight. + let latest_cumulative_weight = latest_block.cumulative_weight(); + + // Compute the next round number./ + let next_round = latest_round.saturating_add(1); + // Compute the next height. + let next_height = latest_height.saturating_add(1); + + // Construct the finalize state. + FinalizeGlobalState::new::(next_round, next_height, latest_cumulative_weight, 0u128, latest_block.hash()) +} diff --git a/leo/package/src/lib.rs b/leo/package/src/lib.rs index 2d99ded342..eeeffd0410 100644 --- a/leo/package/src/lib.rs +++ b/leo/package/src/lib.rs @@ -24,6 +24,7 @@ pub mod outputs; pub mod package; pub mod root; pub mod source; +pub mod tst; use leo_errors::{PackageError, Result}; diff --git a/leo/package/src/package.rs b/leo/package/src/package.rs index 04a74b2b11..747bf2ba0b 100644 --- a/leo/package/src/package.rs +++ b/leo/package/src/package.rs @@ -18,9 +18,11 @@ use crate::{ TEST_PRIVATE_KEY, root::{Env, Gitignore}, source::{MainFile, SourceDirectory}, + tst::TestDirectory, }; use leo_errors::{PackageError, Result}; +use crate::tst::DefaultTestFile; use leo_retriever::{Manifest, NetworkName}; use serde::Deserialize; use snarkvm::prelude::{Network, PrivateKey}; @@ -171,6 +173,12 @@ impl Package { .into()); } + // Create the test directory. + TestDirectory::create(&path)?; + + // Create the default test file. + DefaultTestFile::write_to(&path)?; + Ok(()) } } diff --git a/leo/package/src/tst/default.rs b/leo/package/src/tst/default.rs new file mode 100644 index 0000000000..c228cc0c64 --- /dev/null +++ b/leo/package/src/tst/default.rs @@ -0,0 +1,54 @@ +// Copyright (C) 2019-2024 Aleo Systems Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +//! The default file provided when invoking `leo new` to create a new package. + +use crate::tst::directory::TEST_DIRECTORY_NAME; +use leo_errors::{PackageError, Result}; + +use std::{borrow::Cow, fs::File, io::Write, path::Path}; + +pub static DEFAULT_TEST_FILENAME: &str = "test.leo"; + +pub struct DefaultTestFile; + +impl DefaultTestFile { + pub fn write_to(path: &Path) -> Result<()> { + let mut path = Cow::from(path); + if path.is_dir() { + if !path.ends_with(TEST_DIRECTORY_NAME) { + path.to_mut().push(TEST_DIRECTORY_NAME); + } + path.to_mut().push(DEFAULT_TEST_FILENAME); + } + + let mut file = File::create(&path).map_err(PackageError::io_error_main_file)?; + Ok(file.write_all(Self::template().as_bytes()).map_err(PackageError::io_error_main_file)?) + } + + fn template() -> String { + r#"// A Leo test file. +// To learn more about testing your program, see the documentation at https://docs.leo-lang.org +program test.aleo { + @test + transition test_helloworld() {{ + assert_eq(1u32 + 2u32, 3u32); + }} +} +"# + .to_string() + } +} diff --git a/leo/package/src/tst/directory.rs b/leo/package/src/tst/directory.rs new file mode 100644 index 0000000000..6780f64894 --- /dev/null +++ b/leo/package/src/tst/directory.rs @@ -0,0 +1,57 @@ +// Copyright (C) 2019-2024 Aleo Systems Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use crate::parse_file_paths; + +use leo_errors::{PackageError, Result}; + +use std::{ + borrow::Cow, + fs, + path::{Path, PathBuf}, +}; + +pub static TEST_DIRECTORY_NAME: &str = "tests/"; + +pub struct TestDirectory; + +impl TestDirectory { + /// Creates a directory at the provided path with the default directory name. + pub fn create(path: &Path) -> Result<()> { + let mut path = Cow::from(path); + if path.is_dir() && !path.ends_with(TEST_DIRECTORY_NAME) { + path.to_mut().push(TEST_DIRECTORY_NAME); + } + + fs::create_dir_all(&path).map_err(PackageError::failed_to_create_test_directory)?; + Ok(()) + } + + /// Returns a list of files in the test directory. + pub fn files(path: &Path) -> Result> { + let mut path = Cow::from(path); + if path.is_dir() && !path.ends_with(TEST_DIRECTORY_NAME) { + path.to_mut().push(TEST_DIRECTORY_NAME); + } + + let directory = fs::read_dir(&path).map_err(|err| PackageError::failed_to_read_file(path.display(), err))?; + let mut file_paths = Vec::new(); + + parse_file_paths(directory, &mut file_paths)?; + + Ok(file_paths) + } +} diff --git a/leo/package/src/tst/mod.rs b/leo/package/src/tst/mod.rs new file mode 100644 index 0000000000..cfd64f8a8d --- /dev/null +++ b/leo/package/src/tst/mod.rs @@ -0,0 +1,21 @@ +// Copyright (C) 2019-2024 Aleo Systems Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +pub mod default; +pub use default::*; + +pub mod directory; +pub use directory::*; diff --git a/tests/expectations/compiler/function/annotated_function.out b/tests/expectations/compiler/function/annotated_function.out new file mode 100644 index 0000000000..9a2505435f --- /dev/null +++ b/tests/expectations/compiler/function/annotated_function.out @@ -0,0 +1,13 @@ +namespace = "Compile" +expectation = "Pass" +outputs = [[{ compile = [{ initial_symbol_table = "690edb74eb02c5ac9e717bfb51933668a2b530f7e803ba5666880d23f28d5bec", type_checked_symbol_table = "e7e865d4fe1f786bea9fa28c5cff41a8c29ce00da69306b522dd79eac50d4c78", unrolled_symbol_table = "e7e865d4fe1f786bea9fa28c5cff41a8c29ce00da69306b522dd79eac50d4c78", initial_ast = "97b8563a49a209737aa82195d8fe84257cdd16ac2b32133de91b43de7bb557b1", unrolled_ast = "97b8563a49a209737aa82195d8fe84257cdd16ac2b32133de91b43de7bb557b1", ssa_ast = "4e6c70e32a0fd6f3fc362568ba2f365b198be164922c9276b48a54513a9a0501", flattened_ast = "5ecbeaa566e8bd200515944b8f73945bff67eace488df97e8d0f477f2255bcd7", destructured_ast = "f0ae5e19798aaa5777a17d477f6eb0b2d9ccd1067c807c4348e1ba0a0f147dd5", inlined_ast = "f0ae5e19798aaa5777a17d477f6eb0b2d9ccd1067c807c4348e1ba0a0f147dd5", dce_ast = "f0ae5e19798aaa5777a17d477f6eb0b2d9ccd1067c807c4348e1ba0a0f147dd5", bytecode = """ +program test.aleo; + +function test: + is.eq 1u32 1u32 into r0; + assert.eq r0 true; + +function test_other: + is.eq 1u32 1u32 into r0; + assert.eq r0 true; +""", errors = "", warnings = "" }] }]] diff --git a/tests/expectations/compiler/function/annotated_function_fail.out b/tests/expectations/compiler/function/annotated_function_fail.out index 7c9b020e13..967040f3ea 100644 --- a/tests/expectations/compiler/function/annotated_function_fail.out +++ b/tests/expectations/compiler/function/annotated_function_fail.out @@ -1,21 +1,36 @@ namespace = "Compile" -expectation = "Fail" -outputs = [""" -Error [ETYC0372027]: Unknown annotation: `@test`. - --> compiler-test:4:5 +expectation = "Pass" +outputs = [[{ compile = [{ initial_symbol_table = "4886e187c6b46dbeeb507a7f5188c89f415516c2ff7781ad5af48ea13fca665b", type_checked_symbol_table = "214bdcdf17793e3098343e1aa5993244ad520847278f8864ce7ce0bf4e3bf8ad", unrolled_symbol_table = "214bdcdf17793e3098343e1aa5993244ad520847278f8864ce7ce0bf4e3bf8ad", initial_ast = "05ac2357efbe8d0e00d30f8b7ee33cb625070471ab6f513d2613cb8baeaeff39", unrolled_ast = "05ac2357efbe8d0e00d30f8b7ee33cb625070471ab6f513d2613cb8baeaeff39", ssa_ast = "2f8548323d07c917964e711b10e5198796ac601eb3c75aae61009a600a437220", flattened_ast = "67707c544bfd8a551287d9bf6d4d869b4817d767003c44735ec27ff33d75cf8d", destructured_ast = "3c3c87cf1069c8691bfe57d696be55e2716021cb03e11f8345d3bafb5b23c99b", inlined_ast = "3c3c87cf1069c8691bfe57d696be55e2716021cb03e11f8345d3bafb5b23c99b", dce_ast = "3c3c87cf1069c8691bfe57d696be55e2716021cb03e11f8345d3bafb5b23c99b", bytecode = """ +program test.aleo; + +function test: + is.eq 1u32 1u32 into r0; + assert.eq r0 true; + +closure foo: + input r0 as u8; + input r1 as u8; + add r0 r1 into r2; + output r2 as u8; + +closure bar: + input r0 as u8; + input r1 as u8; + mul r0 r1 into r2; + output r2 as u8; +""", errors = "", warnings = """ +Warning [WTYC0372005]: Unknown key `foo` in annotation `test`. + --> compiler-test:16:11 | - 4 | @test - | ^^^^^ -Error [ETYC0372027]: Unknown annotation: `@program`. - --> compiler-test:9:5 + 16 | @test(foo) + | ^^^ +Warning [WTYC0372004]: Unknown annotation: `foo`. + --> compiler-test:6:5 | - 9 | @program - | ^^^^^^^^ -Error [ETYC0372083]: A program must have at least one transition function. - --> compiler-test:1:1 + 6 | @foo + | ^^^^ +Warning [WTYC0372004]: Unknown annotation: `program`. + --> compiler-test:11:5 | - 1 | - 2 | - 3 | program test.aleo { - | ^^^^^^^^^^^^ -"""] + 11 | @program + | ^^^^^^^^""" }] }]] diff --git a/tests/expectations/parser/functions/annotated_arg_not_ident_fail.out b/tests/expectations/parser/functions/annotated_arg_not_ident_fail.out deleted file mode 100644 index 87527ac2de..0000000000 --- a/tests/expectations/parser/functions/annotated_arg_not_ident_fail.out +++ /dev/null @@ -1,8 +0,0 @@ -namespace = "Parse" -expectation = "Fail" -outputs = [""" -Error [EPAR0370005]: expected 'function', 'transition', or 'inline' -- found '(' - --> test:4:9 - | - 4 | @foo(?, bar, ?) - | ^"""] diff --git a/tests/test-framework/benches/leo_compiler.rs b/tests/test-framework/benches/leo_compiler.rs index e18755c946..72d1dc6f26 100644 --- a/tests/test-framework/benches/leo_compiler.rs +++ b/tests/test-framework/benches/leo_compiler.rs @@ -86,15 +86,15 @@ struct Sample { fn new_compiler(handler: &Handler) -> Compiler<'_, CurrentNetwork> { Compiler::new( String::from("test"), - String::from("aleo"), handler, + vec![], PathBuf::from(String::new()), - PathBuf::from(String::new()), - Some(CompilerOptions { + CompilerOptions { build: BuildOptions { dce_enabled: true, conditional_block_max_depth: 10, disable_conditional_branch_type_checking: false, + build_tests: true, }, output: OutputOptions { symbol_table_spans_enabled: false, @@ -110,7 +110,7 @@ fn new_compiler(handler: &Handler) -> Compiler<'_, CurrentNetwork> { inlined_ast: false, dce_ast: false, }, - }), + }, IndexMap::new(), ) } @@ -166,7 +166,8 @@ impl Sample { ) { self.bencher(c, mode, |mut compiler| { let (input, name) = self.data(); - compiler.parse_program_from_string(input, name).expect("Failed to parse program"); + compiler.reset(vec![(name, input.to_string())]); + compiler.parse().expect("Failed to parse program"); logic(compiler) }); } @@ -174,8 +175,9 @@ impl Sample { fn bench_parse(&self, c: &mut Criterion) { self.bencher(c, "parse", |mut compiler| { let (input, name) = self.data(); + compiler.reset(vec![(name, input.to_string())]); let start = Instant::now(); - let out = compiler.parse_program_from_string(input, name); + let out = compiler.parse(); let time = start.elapsed(); out.expect("Failed to parse program"); time @@ -318,8 +320,9 @@ impl Sample { fn bench_full(&self, c: &mut Criterion) { self.bencher(c, "full", |mut compiler| { let (input, name) = self.data(); + compiler.reset(vec![(name, input.to_string())]); let start = Instant::now(); - compiler.parse_program_from_string(input, name).expect("Failed to parse program"); + compiler.parse().expect("Failed to parse program"); let symbol_table = compiler.symbol_table_pass().expect("failed to generate symbol table"); let (symbol_table, struct_graph, call_graph) = compiler.type_checker_pass(symbol_table).expect("failed to run type check pass"); diff --git a/tests/tests/compiler/function/annotated_function.leo b/tests/tests/compiler/function/annotated_function.leo new file mode 100644 index 0000000000..6bddeb9ec4 --- /dev/null +++ b/tests/tests/compiler/function/annotated_function.leo @@ -0,0 +1,20 @@ +/* +namespace = "Compile" +expectation = "Pass" +*/ + +program test.aleo { + @test + transition test() { + assert(1u32 == 1u32); + } + + @test( + private_key = "APrivateKey1zkp9wLdmfcM57QFL3ZEzgfZwCWV52nM24ckmLSmTQcp64FL", + batch = "0", + seed = "1234" + ) + transition test_other() { + assert(1u32 == 1u32); + } +} diff --git a/tests/tests/compiler/function/annotated_function_fail.leo b/tests/tests/compiler/function/annotated_function_fail.leo index 2eb1425e09..5e09b4d8c5 100644 --- a/tests/tests/compiler/function/annotated_function_fail.leo +++ b/tests/tests/compiler/function/annotated_function_fail.leo @@ -1,10 +1,12 @@ /* namespace = "Compile" -expectation = "Fail" +expectation = "Pass" */ +// This test should pass, but produce warnings about unrecognized and malformed annotations. + program test.aleo { - @test + @foo function foo(a: u8, b: u8) -> u8 { return a + b; } @@ -12,4 +14,10 @@ program test.aleo { @program function bar(a: u8, b: u8) -> u8 { return a * b; - }} + } + + @test(foo) + transition test() { + assert(1u32 == 1u32); + } +} diff --git a/utils/disassembler/src/lib.rs b/utils/disassembler/src/lib.rs index b97752ddce..72444bf4c3 100644 --- a/utils/disassembler/src/lib.rs +++ b/utils/disassembler/src/lib.rs @@ -26,11 +26,11 @@ use snarkvm::{ use std::str::FromStr; pub fn disassemble, Command: CommandTrait>( - program: ProgramCore, + program: &ProgramCore, ) -> Stub { let program_id = ProgramId::from(program.id()); Stub { - imports: program.imports().into_iter().map(|(id, _)| ProgramId::from(id)).collect(), + imports: program.imports().iter().map(|(id, _)| ProgramId::from(id)).collect(), stub_id: program_id, consts: Vec::new(), structs: [ @@ -48,7 +48,7 @@ pub fn disassemble, Command: Comman .concat(), mappings: program .mappings() - .into_iter() + .iter() .map(|(id, m)| (Identifier::from(id).name, Mapping::from_snarkvm(m))) .collect(), functions: [ @@ -88,7 +88,7 @@ pub fn disassemble, Command: Comman pub fn disassemble_from_str(name: &str, program: &str) -> Result { match Program::::from_str(program) { - Ok(p) => Ok(disassemble(p)), + Ok(p) => Ok(disassemble(&p)), Err(_) => Err(UtilError::snarkvm_parsing_error(name, Default::default())), } } @@ -109,7 +109,7 @@ mod tests { let program = Program::::credits(); match program { Ok(p) => { - let disassembled = disassemble(p); + let disassembled = disassemble(&p); println!("{}", disassembled); } Err(e) => { diff --git a/utils/retriever/src/program_context/manifest.rs b/utils/retriever/src/program_context/manifest.rs index d4fb93ba8d..fc5a2aa788 100644 --- a/utils/retriever/src/program_context/manifest.rs +++ b/utils/retriever/src/program_context/manifest.rs @@ -22,11 +22,18 @@ use std::path::Path; // Struct representation of program's `program.json` specification #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Manifest { - program: String, - version: String, - description: String, - license: String, - dependencies: Option>, + /// The name of the program + pub program: String, + /// The version of the program. + pub version: String, + /// The description of the program. + pub description: String, + /// The license of the program. + pub license: String, + /// The dependencies of the program. + pub dependencies: Option>, + /// The dev dependencies of the program. These dependencies are only available during testing. + pub dev_dependencies: Option>, } impl Manifest { @@ -36,6 +43,7 @@ impl Manifest { description: &str, license: &str, dependencies: Option>, + dev_dependencies: Option>, ) -> Self { Self { program: program.to_owned(), @@ -43,6 +51,7 @@ impl Manifest { description: description.to_owned(), license: license.to_owned(), dependencies, + dev_dependencies, } } @@ -53,6 +62,7 @@ impl Manifest { description: "".to_owned(), license: "MIT".to_owned(), dependencies: None, + dev_dependencies: None, } } @@ -76,6 +86,10 @@ impl Manifest { &self.dependencies } + pub fn dev_dependencies(&self) -> &Option> { + &self.dev_dependencies + } + pub fn write_to_dir(&self, path: &Path) -> Result<(), PackageError> { // Serialize the manifest to a JSON string. let contents = serde_json::to_string_pretty(&self) diff --git a/utils/retriever/src/retriever/mod.rs b/utils/retriever/src/retriever/mod.rs index fdd29a1436..78b06f1422 100644 --- a/utils/retriever/src/retriever/mod.rs +++ b/utils/retriever/src/retriever/mod.rs @@ -35,6 +35,14 @@ use std::{ }; use ureq::AgentBuilder; +// TODO: The retriever does too many things. +// It should only be responsible for pulling dependencies and managing the global cache. +// Otherwise, the aggregated files should be passed on to whatever context needs them. +// The cache should also be made optional and configurable. +// The definition of manifest and lock files should be handled by the build system. +// As part of this refactor, the package system should be improved with better build caching and organization. +// The current system uses snarkVM's which doesn't fit well with Leo's needs. + // Retriever is responsible for retrieving external programs pub struct Retriever { name: Symbol, @@ -291,40 +299,33 @@ impl Retriever { // Creates the stub of the program, caches it, and writes the local `leo.lock` file pub fn process_local(&mut self, name: Symbol, recursive: bool) -> Result<(), UtilError> { let cur_context = self.contexts.get_mut(&name).unwrap(); - // Don't need to disassemble the main file - if name != self.name { - // Disassemble the program - let compiled_path = cur_context.compiled_file_path(); - if !compiled_path.exists() { - return Err(UtilError::build_file_does_not_exist(compiled_path.to_str().unwrap(), Default::default())); - } - let mut file = File::open(compiled_path).unwrap_or_else(|_| { - panic!("Failed to open file {}", cur_context.compiled_file_path().to_str().unwrap()) - }); - let mut content = String::new(); - file.read_to_string(&mut content).map_err(|err| { - UtilError::util_file_io_error( - format!("Could not read {}", cur_context.compiled_file_path().to_str().unwrap()), - err, - Default::default(), - ) - })?; + // Disassemble the program + let compiled_path = cur_context.compiled_file_path(); + if !compiled_path.exists() { + return Err(UtilError::build_file_does_not_exist(compiled_path.to_str().unwrap(), Default::default())); + } + let mut file = File::open(compiled_path) + .unwrap_or_else(|_| panic!("Failed to open file {}", cur_context.compiled_file_path().to_str().unwrap())); + let mut content = String::new(); + file.read_to_string(&mut content).map_err(|err| { + UtilError::util_file_io_error( + format!("Could not read {}", cur_context.compiled_file_path().to_str().unwrap()), + err, + Default::default(), + ) + })?; - // Cache the disassembled stub - let stub: Stub = disassemble_from_str::(&name.to_string(), &content)?; - if cur_context.add_stub(stub.clone()) { - Err(UtilError::duplicate_dependency_name_error(stub.stub_id.name.name, Default::default()))?; - } + // Cache the disassembled stub + let stub: Stub = disassemble_from_str::(&name.to_string(), &content)?; + if cur_context.add_stub(stub.clone()) { + Err(UtilError::duplicate_dependency_name_error(stub.stub_id.name.name, Default::default()))?; + } - // Cache the hash - cur_context.add_checksum(); + // Cache the hash + cur_context.add_checksum(); - // Only write lock file when recursive building - if recursive { - self.write_lock_file(&name)?; - } - } else { - // Write lock file + // Only write lock file when recursive building or when building the top-level program. + if recursive || name == self.name { self.write_lock_file(&name)?; } @@ -416,11 +417,16 @@ fn retrieve_local(name: &String, path: &PathBuf) -> Result, Util ))?; } - let dependencies = match program_data.dependencies() { + let mut dependencies = match program_data.dependencies() { Some(deps) => deps.clone(), None => Vec::new(), }; + // Add the dev dependencies, if they exist. + if let Some(deps) = program_data.dev_dependencies() { + dependencies.extend(deps.clone()) + } + Ok(dependencies) }