From 99089d9bbf5d59e26e956b8d31e3cbd752a5d423 Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Fri, 15 Nov 2024 08:59:53 -0800 Subject: [PATCH 01/23] Attributes and symbols in preparation for the test framework. --- compiler/ast/src/functions/mod.rs | 1 + compiler/ast/src/functions/variant.rs | 1 + compiler/ast/src/stub/function_stub.rs | 1 + compiler/compiler/src/compiler.rs | 1 + compiler/parser/src/parser/file.rs | 7 ++++--- compiler/parser/src/tokenizer/lexer.rs | 1 + compiler/parser/src/tokenizer/token.rs | 4 ++++ .../passes/src/code_generation/visit_program.rs | 2 +- .../src/function_inlining/inline_expression.rs | 8 +++++--- .../passes/src/type_checking/check_expressions.rs | 2 +- compiler/passes/src/type_checking/check_program.rs | 14 +++++++++++--- compiler/passes/src/type_checking/checker.rs | 8 +++++++- compiler/passes/src/type_checking/mod.rs | 6 +++--- compiler/span/src/symbol.rs | 6 ++++++ .../src/errors/type_checker/type_checker_error.rs | 7 +++++++ .../functions/annotated_arg_not_ident_fail.out | 8 -------- 16 files changed, 54 insertions(+), 23 deletions(-) delete mode 100644 tests/expectations/parser/functions/annotated_arg_not_ident_fail.out diff --git a/compiler/ast/src/functions/mod.rs b/compiler/ast/src/functions/mod.rs index f0b9f9622d..75583561bb 100644 --- a/compiler/ast/src/functions/mod.rs +++ b/compiler/ast/src/functions/mod.rs @@ -104,6 +104,7 @@ impl Function { Variant::Inline => write!(f, "inline ")?, Variant::Function | Variant::AsyncFunction => write!(f, "function ")?, Variant::Transition | Variant::AsyncTransition => write!(f, "transition ")?, + Variant::Interpret => write!(f, "interpret")?, } write!(f, "{}", self.identifier)?; diff --git a/compiler/ast/src/functions/variant.rs b/compiler/ast/src/functions/variant.rs index eb2851456f..7cc5876a18 100644 --- a/compiler/ast/src/functions/variant.rs +++ b/compiler/ast/src/functions/variant.rs @@ -29,6 +29,7 @@ pub enum Variant { Transition, AsyncTransition, AsyncFunction, + Interpret, } impl Variant { diff --git a/compiler/ast/src/stub/function_stub.rs b/compiler/ast/src/stub/function_stub.rs index c62524f179..0d1b58bd2c 100644 --- a/compiler/ast/src/stub/function_stub.rs +++ b/compiler/ast/src/stub/function_stub.rs @@ -114,6 +114,7 @@ impl FunctionStub { Variant::Inline => write!(f, "inline ")?, Variant::Function | Variant::AsyncFunction => write!(f, "function ")?, Variant::Transition | Variant::AsyncTransition => write!(f, "transition ")?, + Variant::Interpret => write!(f, "interpret")?, } write!(f, "{}", self.identifier)?; diff --git a/compiler/compiler/src/compiler.rs b/compiler/compiler/src/compiler.rs index 006762dec8..1cbe9d8b41 100644 --- a/compiler/compiler/src/compiler.rs +++ b/compiler/compiler/src/compiler.rs @@ -162,6 +162,7 @@ impl<'a, N: Network> Compiler<'a, N> { &self.type_table, self.compiler_options.build.conditional_block_max_depth, self.compiler_options.build.disable_conditional_branch_type_checking, + false, // is_test ))?; if self.compiler_options.output.type_checked_symbol_table { self.write_symbol_table_to_json("type_checked_symbol_table.json", &symbol_table)?; diff --git a/compiler/parser/src/parser/file.rs b/compiler/parser/src/parser/file.rs index 1390c114d1..2c2ff395b0 100644 --- a/compiler/parser/src/parser/file.rs +++ b/compiler/parser/src/parser/file.rs @@ -134,7 +134,7 @@ impl ParserContext<'_, N> { let (id, mapping) = self.parse_mapping()?; mappings.push((id, mapping)); } - Token::At | Token::Async | Token::Function | Token::Transition | Token::Inline => { + Token::At | Token::Async | Token::Function | Token::Transition | Token::Inline | Token::Interpret => { let (id, function) = self.parse_function()?; // Partition into transitions and functions so that we don't have to sort later. @@ -334,8 +334,9 @@ 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::Interpret => (Variant::Interpret, self.expect(&Token::Interpret)?), Token::Inline => (Variant::Inline, self.expect(&Token::Inline)?), Token::Function => { (if is_async { Variant::AsyncFunction } else { Variant::Function }, self.expect(&Token::Function)?) @@ -344,7 +345,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/parser/src/tokenizer/lexer.rs b/compiler/parser/src/tokenizer/lexer.rs index 51e31412be..44dd8f5f0a 100644 --- a/compiler/parser/src/tokenizer/lexer.rs +++ b/compiler/parser/src/tokenizer/lexer.rs @@ -431,6 +431,7 @@ impl Token { "import" => Token::Import, "in" => Token::In, "inline" => Token::Inline, + "interpret" => Token::Interpret, "let" => Token::Let, "leo" => Token::Leo, "mapping" => Token::Mapping, diff --git a/compiler/parser/src/tokenizer/token.rs b/compiler/parser/src/tokenizer/token.rs index d6499e8962..aca7c187de 100644 --- a/compiler/parser/src/tokenizer/token.rs +++ b/compiler/parser/src/tokenizer/token.rs @@ -156,6 +156,7 @@ pub enum Token { Import, In, Inline, + Interpret, Let, Mapping, Network, @@ -205,6 +206,7 @@ pub const KEYWORD_TOKENS: &[Token] = &[ Token::Import, Token::In, Token::Inline, + Token::Interpret, Token::Let, Token::Mapping, Token::Network, @@ -262,6 +264,7 @@ impl Token { Token::Import => sym::import, Token::In => sym::In, Token::Inline => sym::inline, + Token::Interpret => sym::interpret, Token::Let => sym::Let, Token::Leo => sym::leo, Token::Mapping => sym::mapping, @@ -396,6 +399,7 @@ impl fmt::Display for Token { Import => write!(f, "import"), In => write!(f, "in"), Inline => write!(f, "inline"), + Interpret => write!(f, "interpret"), Let => write!(f, "let"), Mapping => write!(f, "mapping"), Network => write!(f, "network"), diff --git a/compiler/passes/src/code_generation/visit_program.rs b/compiler/passes/src/code_generation/visit_program.rs index 6d08b1e270..0fd39357a0 100644 --- a/compiler/passes/src/code_generation/visit_program.rs +++ b/compiler/passes/src/code_generation/visit_program.rs @@ -186,7 +186,7 @@ impl<'a> CodeGenerator<'a> { Variant::Transition | Variant::AsyncTransition => format!("\nfunction {}:\n", function.identifier), Variant::Function => format!("\nclosure {}:\n", function.identifier), Variant::AsyncFunction => format!("\nfinalize {}:\n", self.finalize_caller.unwrap()), - Variant::Inline => return String::new(), + Variant::Inline | Variant::Interpret => return String::new(), }; // Construct and append the input declarations of the function. diff --git a/compiler/passes/src/function_inlining/inline_expression.rs b/compiler/passes/src/function_inlining/inline_expression.rs index 4d627af9f0..89357caa2d 100644 --- a/compiler/passes/src/function_inlining/inline_expression.rs +++ b/compiler/passes/src/function_inlining/inline_expression.rs @@ -102,9 +102,11 @@ impl ExpressionReconstructor for FunctionInliner<'_> { (result, inlined_statements) } - Variant::Function | Variant::AsyncFunction | Variant::Transition | Variant::AsyncTransition => { - (Expression::Call(input), Default::default()) - } + Variant::Function + | Variant::AsyncFunction + | Variant::Transition + | Variant::AsyncTransition + | Variant::Interpret => (Expression::Call(input), Default::default()), } } } diff --git a/compiler/passes/src/type_checking/check_expressions.rs b/compiler/passes/src/type_checking/check_expressions.rs index 60bb0511f3..097f7130ce 100644 --- a/compiler/passes/src/type_checking/check_expressions.rs +++ b/compiler/passes/src/type_checking/check_expressions.rs @@ -96,7 +96,7 @@ impl<'a, N: Network> ExpressionVisitor<'a> for TypeChecker<'a, N> { // Check core struct name and function. if let Some(core_instruction) = self.get_core_function_call(&access.variant, &access.name) { // Check that operation is not restricted to finalize blocks. - if self.scope_state.variant != Some(Variant::AsyncFunction) + if !matches!(self.scope_state.variant, Some(Variant::AsyncFunction) | Some(Variant::Interpret)) && core_instruction.is_finalize_command() { self.emit_err(TypeCheckerError::operation_must_be_in_finalize_block(input.span())); diff --git a/compiler/passes/src/type_checking/check_program.rs b/compiler/passes/src/type_checking/check_program.rs index 342c4a91e4..32cbac3674 100644 --- a/compiler/passes/src/type_checking/check_program.rs +++ b/compiler/passes/src/type_checking/check_program.rs @@ -229,10 +229,18 @@ 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. + let valid_annotations = [sym::should_fail, sym::native_test, sym::interpreted_test]; for annotation in function.annotations.iter() { - // TODO: Change to compiler warning. - self.emit_err(TypeCheckerError::unknown_annotation(annotation, annotation.span)) + // All Leo annotations currently apply only to test code. + if !self.is_test || !valid_annotations.contains(&annotation.identifier.name) { + // TODO: Change to compiler warning. + self.emit_err(TypeCheckerError::unknown_annotation(annotation, annotation.span)); + } + } + + // `interpret` can only be used for tests. + if !self.is_test && function.variant == Variant::Interpret { + self.emit_err(TypeCheckerError::interpret_outside_test(function.span)); } // 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 dfcd0afc6d..21b6f7ac79 100644 --- a/compiler/passes/src/type_checking/checker.rs +++ b/compiler/passes/src/type_checking/checker.rs @@ -53,6 +53,8 @@ pub struct TypeChecker<'a, N: Network> { pub(crate) async_function_input_types: IndexMap>, /// The set of used composites. pub(crate) used_structs: IndexSet, + /// Are we compiling tests? + pub(crate) is_test: bool, // Allows the type checker to be generic over the network. phantom: PhantomData, } @@ -109,6 +111,7 @@ impl<'a, N: Network> TypeChecker<'a, N> { handler: &'a Handler, max_depth: usize, disabled: bool, + is_test: bool, ) -> Self { let struct_names = symbol_table.structs.keys().map(|loc| loc.name).collect(); let function_names = symbol_table.functions.keys().map(|loc| loc.name).collect(); @@ -124,6 +127,7 @@ impl<'a, N: Network> TypeChecker<'a, N> { await_checker: AwaitChecker::new(max_depth, !disabled), async_function_input_types: IndexMap::new(), used_structs: IndexSet::new(), + is_test, phantom: Default::default(), } } @@ -1375,7 +1379,9 @@ 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) | Some(Variant::Interpret)) + { self.handler.emit_err(TypeCheckerError::invalid_operation_outside_finalize(name, span)) } } diff --git a/compiler/passes/src/type_checking/mod.rs b/compiler/passes/src/type_checking/mod.rs index d06179100d..6fde23185b 100644 --- a/compiler/passes/src/type_checking/mod.rs +++ b/compiler/passes/src/type_checking/mod.rs @@ -36,11 +36,11 @@ use leo_errors::{Result, emitter::Handler}; use snarkvm::prelude::Network; impl<'a, N: Network> Pass for TypeChecker<'a, N> { - type Input = (&'a Ast, &'a Handler, SymbolTable, &'a TypeTable, usize, bool); + type Input = (&'a Ast, &'a Handler, SymbolTable, &'a TypeTable, usize, bool, bool); type Output = Result<(SymbolTable, StructGraph, CallGraph)>; - fn do_pass((ast, handler, st, tt, max_depth, await_checking): Self::Input) -> Self::Output { - let mut visitor = TypeChecker::::new(st, tt, handler, max_depth, await_checking); + fn do_pass((ast, handler, st, tt, max_depth, await_checking, is_test): Self::Input) -> Self::Output { + let mut visitor = TypeChecker::::new(st, tt, handler, max_depth, await_checking, is_test); visitor.visit_program(ast.as_repr()); handler.last_err().map_err(|e| *e)?; diff --git a/compiler/span/src/symbol.rs b/compiler/span/src/symbol.rs index 48ae35d370..b1c845aa34 100644 --- a/compiler/span/src/symbol.rs +++ b/compiler/span/src/symbol.rs @@ -231,6 +231,11 @@ symbols! { False: "false", True: "true", + // annotations + should_fail, + native_test, + interpreted_test, + // general keywords As: "as", assert, @@ -250,6 +255,7 @@ symbols! { increment, inline, input, + interpret, Let: "let", leo, main, diff --git a/errors/src/errors/type_checker/type_checker_error.rs b/errors/src/errors/type_checker/type_checker_error.rs index f7248b7399..14989fe86e 100644 --- a/errors/src/errors/type_checker/type_checker_error.rs +++ b/errors/src/errors/type_checker/type_checker_error.rs @@ -894,4 +894,11 @@ create_messages!( msg: format!("Cannot define a function with no parameters."), help: None, } + + @formatted + interpret_outside_test { + args: (), + msg: "Cannot define an `interpret` function outside of tests.".to_string(), + help: None, + } ); 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, ?) - | ^"""] From 77d646ceef5bb7a8ac425bb98a8d572c84c0bb9e Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:33:00 -0800 Subject: [PATCH 02/23] Add scaffolding for leo test --- leo/cli/cli.rs | 6 ++++ leo/cli/commands/mod.rs | 17 ++++++--- leo/cli/commands/test.rs | 78 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 leo/cli/commands/test.rs diff --git a/leo/cli/cli.rs b/leo/cli/cli.rs index 3cf4fff3f6..ea0ec14bdb 100644 --- a/leo/cli/cli.rs +++ b/leo/cli/cli.rs @@ -97,6 +97,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)] @@ -140,6 +145,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), } } diff --git a/leo/cli/commands/mod.rs b/leo/cli/commands/mod.rs index d22cd8683c..daef962731 100644 --- a/leo/cli/commands/mod.rs +++ b/leo/cli/commands/mod.rs @@ -50,23 +50,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 diff --git a/leo/cli/commands/test.rs b/leo/cli/commands/test.rs new file mode 100644 index 0000000000..0d1dfce8e1 --- /dev/null +++ b/leo/cli/commands/test.rs @@ -0,0 +1,78 @@ +// 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::*; + +/// Build, Prove and Run Leo program with inputs +#[derive(Parser, Debug)] +pub struct LeoTest { + #[clap(name = "TESTNAME", help = "If specified, only run tests containing this string in their names.")] + name: 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, the maximum is the number of CPUs.")] + jobs: Option, + #[clap(long, help = "Skip running the native tests.", default_value = "false")] + skip_native: bool, + // TODO: The default should eventually be `false`. + #[clap(long, help = "Skip running the interpreted tests.", default_value = "true")] + skip_interpreted: bool, + // TODO: The default should eventually be `false`. + #[clap(long, help = "Skip running the end-to-end tests.", default_value = "true")] + skip_end_to_end: bool, + #[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 { + (LeoBuild { options: self.compiler_options.clone() }).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> { + todo!() +} From 7c4f9bd6e9bef75acee73ac01660c583f22fd451 Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:52:03 -0800 Subject: [PATCH 03/23] Add dev dependencies to CLI and Manifest --- Cargo.lock | 1 + Cargo.toml | 3 + leo/cli/cli.rs | 16 +-- leo/cli/commands/add.rs | 109 +++++++++--------- leo/cli/commands/remove.rs | 80 ++++++------- leo/cli/commands/test.rs | 7 +- .../retriever/src/program_context/manifest.rs | 24 +++- 7 files changed, 128 insertions(+), 112 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bd88612d94..f8097591d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1581,6 +1581,7 @@ dependencies = [ "leo-retriever", "leo-span", "num-format", + "num_cpus", "rand", "rand_chacha", "rpassword", diff --git a/Cargo.toml b/Cargo.toml index 6ac213531d..9f5217bedf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -173,6 +173,9 @@ version = "0.15.7" [dependencies.indexmap] workspace = true +[dependencies.num_cpus] +version = "1.16.0" + [dependencies.rand] workspace = true diff --git a/leo/cli/cli.rs b/leo/cli/cli.rs index ea0ec14bdb..2dc01cabed 100644 --- a/leo/cli/cli.rs +++ b/leo/cli/cli.rs @@ -410,8 +410,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()), @@ -522,8 +522,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()), @@ -537,8 +537,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()), @@ -552,8 +552,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()), @@ -694,8 +694,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()), @@ -709,8 +709,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()), @@ -881,8 +881,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()), @@ -896,8 +896,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/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.rs b/leo/cli/commands/test.rs index 0d1dfce8e1..a98650c006 100644 --- a/leo/cli/commands/test.rs +++ b/leo/cli/commands/test.rs @@ -74,5 +74,10 @@ impl Command for LeoTest { // A helper function to handle the `test` command. fn handle_test(command: LeoTest, context: Context) -> Result<::Output> { - todo!() + // Select the number of jobs, defaulting to the number of CPUs. + // If the number exceeds the number of CPUs, it is clamped to the number of CPUs. + let num_cpus = num_cpus::get(); + let jobs = command.jobs.unwrap_or(num_cpus).min(num_cpus); + + Ok(()) } 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) From 53abcdcb5db1023ba2f95ad9773aaa7b21c8839a Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Mon, 18 Nov 2024 17:08:14 -0800 Subject: [PATCH 04/23] Add build_tests flag to compiler options --- compiler/compiler/src/options.rs | 2 ++ compiler/compiler/tests/integration/compile.rs | 1 + compiler/compiler/tests/integration/execute.rs | 1 + leo/cli/commands/build.rs | 1 + leo/cli/commands/mod.rs | 3 +++ leo/cli/commands/test.rs | 5 ++++- tests/test-framework/benches/leo_compiler.rs | 1 + 7 files changed, 13 insertions(+), 1 deletion(-) diff --git a/compiler/compiler/src/options.rs b/compiler/compiler/src/options.rs index 6ed93c4309..61d69fa8e2 100644 --- a/compiler/compiler/src/options.rs +++ b/compiler/compiler/src/options.rs @@ -60,4 +60,6 @@ pub struct OutputOptions { pub inlined_ast: bool, /// If enabled writes the AST after dead code elimination. pub dce_ast: bool, + /// If enabled builds all test programs. + pub build_tests: bool, } diff --git a/compiler/compiler/tests/integration/compile.rs b/compiler/compiler/tests/integration/compile.rs index 33c53a8969..ca2fc34f72 100644 --- a/compiler/compiler/tests/integration/compile.rs +++ b/compiler/compiler/tests/integration/compile.rs @@ -84,6 +84,7 @@ fn run_test(test: Test, handler: &Handler, buf: &BufferEmitter) -> Result Result for CompilerOptions { destructured_ast: options.enable_destructured_ast_snapshot, inlined_ast: options.enable_inlined_ast_snapshot, dce_ast: options.enable_dce_ast_snapshot, + build_tests: options.build_tests, }, }; if options.enable_all_ast_snapshots { diff --git a/leo/cli/commands/mod.rs b/leo/cli/commands/mod.rs index daef962731..9f8531b848 100644 --- a/leo/cli/commands/mod.rs +++ b/leo/cli/commands/mod.rs @@ -180,6 +180,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 { @@ -205,6 +207,7 @@ impl Default for BuildOptions { enable_dce_ast_snapshot: false, conditional_block_max_depth: 10, disable_conditional_branch_type_checking: false, + build_tests: false, } } } diff --git a/leo/cli/commands/test.rs b/leo/cli/commands/test.rs index a98650c006..5c78701c17 100644 --- a/leo/cli/commands/test.rs +++ b/leo/cli/commands/test.rs @@ -48,7 +48,10 @@ impl Command for LeoTest { } fn prelude(&self, context: Context) -> Result { - (LeoBuild { options: self.compiler_options.clone() }).execute(context) + // 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 { diff --git a/tests/test-framework/benches/leo_compiler.rs b/tests/test-framework/benches/leo_compiler.rs index e18755c946..fe1d7a99ee 100644 --- a/tests/test-framework/benches/leo_compiler.rs +++ b/tests/test-framework/benches/leo_compiler.rs @@ -109,6 +109,7 @@ fn new_compiler(handler: &Handler) -> Compiler<'_, CurrentNetwork> { destructured_ast: false, inlined_ast: false, dce_ast: false, + build_tests: false, }, }), IndexMap::new(), From 7a19c8384ed584d530e3276f212e2466585edbf8 Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Mon, 18 Nov 2024 19:25:14 -0800 Subject: [PATCH 05/23] WIP --- compiler/compiler/src/compiler.rs | 2 +- compiler/passes/src/type_checking/check_program.rs | 4 ++-- compiler/passes/src/type_checking/checker.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/compiler/compiler/src/compiler.rs b/compiler/compiler/src/compiler.rs index 1cbe9d8b41..a80bfb2a36 100644 --- a/compiler/compiler/src/compiler.rs +++ b/compiler/compiler/src/compiler.rs @@ -162,7 +162,7 @@ impl<'a, N: Network> Compiler<'a, N> { &self.type_table, self.compiler_options.build.conditional_block_max_depth, self.compiler_options.build.disable_conditional_branch_type_checking, - false, // is_test + self.compiler_options.output.build_tests, ))?; if self.compiler_options.output.type_checked_symbol_table { self.write_symbol_table_to_json("type_checked_symbol_table.json", &symbol_table)?; diff --git a/compiler/passes/src/type_checking/check_program.rs b/compiler/passes/src/type_checking/check_program.rs index 32cbac3674..5ff804a0db 100644 --- a/compiler/passes/src/type_checking/check_program.rs +++ b/compiler/passes/src/type_checking/check_program.rs @@ -232,14 +232,14 @@ impl<'a, N: Network> ProgramVisitor<'a> for TypeChecker<'a, N> { let valid_annotations = [sym::should_fail, sym::native_test, sym::interpreted_test]; for annotation in function.annotations.iter() { // All Leo annotations currently apply only to test code. - if !self.is_test || !valid_annotations.contains(&annotation.identifier.name) { + if !self.build_tests || !valid_annotations.contains(&annotation.identifier.name) { // TODO: Change to compiler warning. self.emit_err(TypeCheckerError::unknown_annotation(annotation, annotation.span)); } } // `interpret` can only be used for tests. - if !self.is_test && function.variant == Variant::Interpret { + if !self.build_tests && function.variant == Variant::Interpret { self.emit_err(TypeCheckerError::interpret_outside_test(function.span)); } diff --git a/compiler/passes/src/type_checking/checker.rs b/compiler/passes/src/type_checking/checker.rs index 21b6f7ac79..f8b85b1ae3 100644 --- a/compiler/passes/src/type_checking/checker.rs +++ b/compiler/passes/src/type_checking/checker.rs @@ -54,7 +54,7 @@ pub struct TypeChecker<'a, N: Network> { /// The set of used composites. pub(crate) used_structs: IndexSet, /// Are we compiling tests? - pub(crate) is_test: bool, + pub(crate) build_tests: bool, // Allows the type checker to be generic over the network. phantom: PhantomData, } @@ -127,7 +127,7 @@ impl<'a, N: Network> TypeChecker<'a, N> { await_checker: AwaitChecker::new(max_depth, !disabled), async_function_input_types: IndexMap::new(), used_structs: IndexSet::new(), - is_test, + build_tests: is_test, phantom: Default::default(), } } From 1ea1c7daaa3886a1cd7763ae014645ec49b1479a Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Fri, 22 Nov 2024 18:44:08 -0800 Subject: [PATCH 06/23] Add test to AST --- compiler/ast/src/lib.rs | 3 ++ compiler/ast/src/tst/mod.rs | 55 +++++++++++++++++++++++++++++++++++++ examples | 2 +- 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 compiler/ast/src/tst/mod.rs diff --git a/compiler/ast/src/lib.rs b/compiler/ast/src/lib.rs index 2d079a93c5..0894d8be15 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 tst; +pub use self::tst::*; + pub mod types; pub use self::types::*; diff --git a/compiler/ast/src/tst/mod.rs b/compiler/ast/src/tst/mod.rs new file mode 100644 index 0000000000..00c961bc2e --- /dev/null +++ b/compiler/ast/src/tst/mod.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 . + +//! A Leo test consists of struct, record, function, transition, and mapping definitions. +//! Anything that can be defined within a program scope can be defined in a test. + +use crate::{Composite, ConstDeclaration, Function, Mapping, ProgramId, Stub}; + +use leo_span::{Span, Symbol}; +use serde::{Deserialize, Serialize}; +use std::fmt; + +/// An abstract syntax tree for a Leo test. +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct Test { + /// The constant definitions + pub consts: Vec<(Symbol, ConstDeclaration)>, + /// A vector of struct/record definitions. + pub structs: Vec<(Symbol, Composite)>, + /// A vector of mapping definitions. + pub mappings: Vec<(Symbol, Mapping)>, + /// A vector of function definitions. + pub functions: Vec<(Symbol, Function)>, +} + +impl fmt::Display for Test { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for (_, const_) in self.consts.iter() { + writeln!(f, " {const_}")?; + } + for (_, struct_) in self.structs.iter() { + writeln!(f, " {struct_}")?; + } + for (_, mapping) in self.mappings.iter() { + writeln!(f, " {mapping}")?; + } + for (_, function) in self.functions.iter() { + writeln!(f, " {function}")?; + } + Ok(()) + } +} diff --git a/examples b/examples index b75bb77851..155f8410fb 160000 --- a/examples +++ b/examples @@ -1 +1 @@ -Subproject commit b75bb77851667f6ece0af4cacfc27715aa7186aa +Subproject commit 155f8410fb199c971b50950882a74ff593f8f5d9 From dc604536a6865ea80e6de7edcea52fc82a32a003 Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Fri, 22 Nov 2024 18:51:41 -0800 Subject: [PATCH 07/23] Add test visitors --- compiler/ast/src/passes/consumer.rs | 7 +++++++ compiler/ast/src/passes/reconstructor.rs | 25 +++++++++++++++++++++--- compiler/ast/src/passes/visitor.rs | 13 ++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/compiler/ast/src/passes/consumer.rs b/compiler/ast/src/passes/consumer.rs index 3d8edea460..c5b5b91e59 100644 --- a/compiler/ast/src/passes/consumer.rs +++ b/compiler/ast/src/passes/consumer.rs @@ -153,3 +153,10 @@ pub trait ProgramConsumer { type Output; fn consume_program(&mut self, input: Program) -> Self::Output; } + +/// A `Consumer` for a `Test` +pub trait TestConsumer { + type Output; + + fn consume_test(&mut self, input: Test) -> Self::Output; +} diff --git a/compiler/ast/src/passes/reconstructor.rs b/compiler/ast/src/passes/reconstructor.rs index dea305c0f4..e83caac554 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, } } @@ -488,3 +488,22 @@ pub trait ProgramReconstructor: StatementReconstructor { input } } + +/// The `Reconstructor` for a `Test` +pub trait TestReconstructor: ProgramReconstructor { + fn reconstruct_test(&mut self, input: Test) -> Test { + Test { + consts: input + .consts + .into_iter() + .map(|(i, c)| match self.reconstruct_const(c) { + (Statement::Const(declaration), _) => (i, declaration), + _ => 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(|(i, m)| (i, self.reconstruct_mapping(m))).collect(), + functions: input.functions.into_iter().map(|(i, f)| (i, self.reconstruct_function(f))).collect(), + } + } +} diff --git a/compiler/ast/src/passes/visitor.rs b/compiler/ast/src/passes/visitor.rs index eb1f36a87a..d3ee864e4a 100644 --- a/compiler/ast/src/passes/visitor.rs +++ b/compiler/ast/src/passes/visitor.rs @@ -254,3 +254,16 @@ pub trait ProgramVisitor<'a>: StatementVisitor<'a> { fn visit_struct_stub(&mut self, _input: &'a Composite) {} } + +/// The `Visitor` for a `Test` +pub trait TestVisitor<'a>: ProgramVisitor<'a> { + fn visit_test(&mut self, input: &'a Test) { + 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))); + } +} From 520f1f8aee782af0423fffc22d93eebb7bc4ff3c Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Sat, 23 Nov 2024 13:36:00 -0800 Subject: [PATCH 08/23] Initialize tests directory on leo new --- errors/src/errors/package/package_errors.rs | 7 +++ leo/package/src/lib.rs | 1 + leo/package/src/package.rs | 8 +++ leo/package/src/tst/default.rs | 57 ++++++++++++++++++++ leo/package/src/tst/directory.rs | 58 +++++++++++++++++++++ leo/package/src/tst/mod.rs | 21 ++++++++ 6 files changed, 152 insertions(+) create mode 100644 leo/package/src/tst/default.rs create mode 100644 leo/package/src/tst/directory.rs create mode 100644 leo/package/src/tst/mod.rs 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/leo/package/src/lib.rs b/leo/package/src/lib.rs index 6d069fdfee..bc5b55bf9b 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 63bae5fa79..371848f208 100644 --- a/leo/package/src/package.rs +++ b/leo/package/src/package.rs @@ -17,9 +17,11 @@ use crate::{ 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}; @@ -174,6 +176,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..6dc943050c --- /dev/null +++ b/leo/package/src/tst/default.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 . + +//! 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 serde::Deserialize; +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 { + format!( + r#"// A default Leo test file. +// To learn more about testing your program, see the documentation at https://docs.leo-lang.org + +@native_test +@interpreted_test +transition test_helloworld() {{ + let result: u32 = helloworld.aleo/main(1u32, 2u32) + assert_eq!(result, 3u32) +}} +"#, + ) + } +} diff --git a/leo/package/src/tst/directory.rs b/leo/package/src/tst/directory.rs new file mode 100644 index 0000000000..03e4b1da5c --- /dev/null +++ b/leo/package/src/tst/directory.rs @@ -0,0 +1,58 @@ +// 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 crate::source::MAIN_FILENAME; +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::*; From caa436640fbaddd43aaf740ffba2cb2bcd0737a4 Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Sat, 23 Nov 2024 13:45:32 -0800 Subject: [PATCH 09/23] Add parser for Tests --- compiler/parser/src/parser/file.rs | 4 +- compiler/parser/src/parser/mod.rs | 1 + compiler/parser/src/parser/test.rs | 69 ++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 compiler/parser/src/parser/test.rs diff --git a/compiler/parser/src/parser/file.rs b/compiler/parser/src/parser/file.rs index 2c2ff395b0..d20dd8b494 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(", "), @@ -324,7 +324,7 @@ impl ParserContext<'_, N> { /// 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(); diff --git a/compiler/parser/src/parser/mod.rs b/compiler/parser/src/parser/mod.rs index f39da3a305..5cadb6f9b7 100644 --- a/compiler/parser/src/parser/mod.rs +++ b/compiler/parser/src/parser/mod.rs @@ -36,6 +36,7 @@ pub(super) use context::ParserContext; mod expression; mod file; mod statement; +mod test; pub(super) mod type_; /// Creates a new program from a given file path and source code text. diff --git a/compiler/parser/src/parser/test.rs b/compiler/parser/src/parser/test.rs new file mode 100644 index 0000000000..13520fea6b --- /dev/null +++ b/compiler/parser/src/parser/test.rs @@ -0,0 +1,69 @@ +// 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 leo_errors::{ParserError, Result}; + +impl ParserContext<'_, N> { + /// Parses a test file. + fn parse_test(&mut self) -> Result { + // Initialize storage for the components of the test file + let mut consts: Vec<(Symbol, ConstDeclaration)> = Vec::new(); + let mut functions = Vec::new(); + let mut structs: Vec<(Symbol, Composite)> = Vec::new(); + let mut mappings: Vec<(Symbol, Mapping)> = Vec::new(); + // Parse the components of the test file. + while self.has_next() { + match &self.token.token { + Token::Const => { + let declaration = self.parse_const_declaration_statement()?; + consts.push((Symbol::intern(&declaration.place.to_string()), declaration)); + } + Token::Struct | Token::Record => { + let (id, struct_) = self.parse_struct()?; + structs.push((id, struct_)); + } + Token::Mapping => { + let (id, mapping) = self.parse_mapping()?; + mappings.push((id, mapping)); + } + Token::At | Token::Async | Token::Function | Token::Transition | Token::Inline | Token::Interpret => { + let (id, function) = self.parse_function()?; + functions.push((id, function)); + } + _ => { + return Err(Self::unexpected_item(&self.token, &[ + Token::Const, + Token::Struct, + Token::Record, + Token::Mapping, + Token::At, + Token::Async, + Token::Function, + Token::Transition, + Token::Inline, + ]) + .into()); + } + } + } + + Ok(Test { consts, functions, structs, mappings }) + } +} + +use leo_span::{Symbol, sym}; From 7ea23ca78ba04408fada7e90d733ec0ab4e6c062 Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Tue, 26 Nov 2024 09:38:16 -0800 Subject: [PATCH 10/23] WIP --- Cargo.lock | 1 + Cargo.toml | 3 + compiler/ast/src/program/mod.rs | 2 + compiler/compiler/src/compiler.rs | 18 ------ .../tests/integration/utilities/mod.rs | 1 - leo/cli/commands/build.rs | 46 ++++++++++++++- tests/test-framework/benches/leo_compiler.rs | 1 - utils/disassembler/src/lib.rs | 8 +-- utils/retriever/src/retriever/mod.rs | 56 +++++++++---------- 9 files changed, 79 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f8097591d7..171542092c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1576,6 +1576,7 @@ dependencies = [ "indexmap 2.6.0", "leo-ast", "leo-compiler", + "leo-disassembler", "leo-errors", "leo-package", "leo-retriever", diff --git a/Cargo.toml b/Cargo.toml index 9f5217bedf..e137f321b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -142,6 +142,9 @@ workspace = true [dependencies.leo-compiler] workspace = true +[dependencies.leo-disassembler] +workspace = true + [dependencies.leo-errors] workspace = true diff --git a/compiler/ast/src/program/mod.rs b/compiler/ast/src/program/mod.rs index 083aa6072a..6cf62d9a9a 100644 --- a/compiler/ast/src/program/mod.rs +++ b/compiler/ast/src/program/mod.rs @@ -38,6 +38,8 @@ pub struct Program { pub stubs: IndexMap, /// A map from program names to program scopes. pub program_scopes: IndexMap, + /// The program tests. + pub tests: Vec, } impl fmt::Display for Program { diff --git a/compiler/compiler/src/compiler.rs b/compiler/compiler/src/compiler.rs index a80bfb2a36..fb21402b3b 100644 --- a/compiler/compiler/src/compiler.rs +++ b/compiler/compiler/src/compiler.rs @@ -44,8 +44,6 @@ pub struct Compiler<'a, N: Network> { 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. @@ -66,7 +64,6 @@ impl<'a, N: Network> Compiler<'a, N> { /// Returns a new Leo compiler. pub fn new( program_name: String, - network: String, handler: &'a Handler, main_file_path: PathBuf, output_directory: PathBuf, @@ -81,7 +78,6 @@ impl<'a, N: Network> Compiler<'a, N> { main_file_path, output_directory, program_name, - network, ast: Ast::new(Program::default()), compiler_options: compiler_options.unwrap_or_default(), node_builder, @@ -114,20 +110,6 @@ impl<'a, N: Network> Compiler<'a, N> { // 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()); - } - if self.compiler_options.output.initial_ast { self.write_ast_to_json("initial_ast.json")?; } diff --git a/compiler/compiler/tests/integration/utilities/mod.rs b/compiler/compiler/tests/integration/utilities/mod.rs index 449aa0b1bb..f50d8c446f 100644 --- a/compiler/compiler/tests/integration/utilities/mod.rs +++ b/compiler/compiler/tests/integration/utilities/mod.rs @@ -163,7 +163,6 @@ pub fn new_compiler( Compiler::new( program_name, - String::from("aleo"), handler, main_file_path, output_dir, diff --git a/leo/cli/commands/build.rs b/leo/cli/commands/build.rs index aa4d8273f5..9fdd2657d7 100644 --- a/leo/cli/commands/build.rs +++ b/leo/cli/commands/build.rs @@ -35,6 +35,7 @@ use std::{ path::{Path, PathBuf}, str::FromStr, }; +use leo_package::tst::TestDirectory; impl From for CompilerOptions { fn from(options: BuildOptions) -> Self { @@ -142,11 +143,18 @@ fn handle_build(command: &LeoBuild, context: Context) -> Result<(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( + &package_path, + &program_id, + &handler, + command.options.clone(), + main_stubs.clone(), + )?; + } Ok(()) } @@ -206,7 +229,6 @@ fn compile_leo_file( // 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(), @@ -226,3 +248,23 @@ fn compile_leo_file( tracing::info!("✅ Compiled '{program_name}.aleo' into Aleo instructions"); Ok(()) } + +/// Compiles test files in the `tests/` directory. +#[allow(clippy::too_many_arguments)] +fn compile_tests( + package_path: &Path, + program_id: &ProgramID, + handler: &Handler, + options: BuildOptions, + stubs: IndexMap, +) -> Result<()> { + // Get the files in `/tests` directory. + let test_dir = TestDirectory::files(package_path)?; + + for test_file in test_dir { + // Compile the test file. + + } + Ok(()) +} + diff --git a/tests/test-framework/benches/leo_compiler.rs b/tests/test-framework/benches/leo_compiler.rs index fe1d7a99ee..4334898882 100644 --- a/tests/test-framework/benches/leo_compiler.rs +++ b/tests/test-framework/benches/leo_compiler.rs @@ -86,7 +86,6 @@ struct Sample { fn new_compiler(handler: &Handler) -> Compiler<'_, CurrentNetwork> { Compiler::new( String::from("test"), - String::from("aleo"), handler, PathBuf::from(String::new()), PathBuf::from(String::new()), diff --git a/utils/disassembler/src/lib.rs b/utils/disassembler/src/lib.rs index b97752ddce..10d05c25f8 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())), } } diff --git a/utils/retriever/src/retriever/mod.rs b/utils/retriever/src/retriever/mod.rs index fdd29a1436..67e3d57c7f 100644 --- a/utils/retriever/src/retriever/mod.rs +++ b/utils/retriever/src/retriever/mod.rs @@ -291,40 +291,34 @@ 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)?; } From 17c8ad30a22a6e864186569806e6229c7bc31606 Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Tue, 26 Nov 2024 15:29:40 -0800 Subject: [PATCH 11/23] WIP --- compiler/ast/src/passes/consumer.rs | 12 ++--- compiler/ast/src/passes/reconstructor.rs | 4 +- compiler/ast/src/passes/visitor.rs | 14 ++---- compiler/ast/src/program/mod.rs | 14 ++++-- .../tests/integration/utilities/mod.rs | 9 +--- compiler/parser/src/parser/file.rs | 2 +- .../rename_program.rs | 16 +++++++ errors/src/errors/cli/cli_errors.rs | 14 ++++++ leo/cli/commands/build.rs | 44 +++++++++++++------ utils/retriever/src/retriever/mod.rs | 5 +-- 10 files changed, 84 insertions(+), 50 deletions(-) diff --git a/compiler/ast/src/passes/consumer.rs b/compiler/ast/src/passes/consumer.rs index c5b5b91e59..5f7053ee50 100644 --- a/compiler/ast/src/passes/consumer.rs +++ b/compiler/ast/src/passes/consumer.rs @@ -148,15 +148,15 @@ pub trait ProgramScopeConsumer { fn consume_program_scope(&mut self, input: ProgramScope) -> Self::Output; } -/// A Consumer trait for the program represented by the AST. -pub trait ProgramConsumer { - type Output; - fn consume_program(&mut self, input: Program) -> Self::Output; -} - /// A `Consumer` for a `Test` pub trait TestConsumer { type Output; fn consume_test(&mut self, input: Test) -> Self::Output; } + +/// A Consumer trait for the program represented by the AST. +pub trait ProgramConsumer { + type Output; + fn consume_program(&mut self, input: Program) -> Self::Output; +} diff --git a/compiler/ast/src/passes/reconstructor.rs b/compiler/ast/src/passes/reconstructor.rs index e83caac554..23f5118696 100644 --- a/compiler/ast/src/passes/reconstructor.rs +++ b/compiler/ast/src/passes/reconstructor.rs @@ -425,6 +425,7 @@ pub trait ProgramReconstructor: StatementReconstructor { .into_iter() .map(|(id, scope)| (id, self.reconstruct_program_scope(scope))) .collect(), + tests: input.tests.into_iter().map(|(id, test)| (id, self.reconstruct_test(test))).collect(), } } @@ -487,10 +488,7 @@ pub trait ProgramReconstructor: StatementReconstructor { fn reconstruct_mapping(&mut self, input: Mapping) -> Mapping { input } -} -/// The `Reconstructor` for a `Test` -pub trait TestReconstructor: ProgramReconstructor { fn reconstruct_test(&mut self, input: Test) -> Test { Test { consts: input diff --git a/compiler/ast/src/passes/visitor.rs b/compiler/ast/src/passes/visitor.rs index d3ee864e4a..a98a4b063c 100644 --- a/compiler/ast/src/passes/visitor.rs +++ b/compiler/ast/src/passes/visitor.rs @@ -224,16 +224,14 @@ pub trait ProgramVisitor<'a>: StatementVisitor<'a> { input.imports.values().for_each(|import| self.visit_import(&import.0)); input.stubs.values().for_each(|stub| self.visit_stub(stub)); input.program_scopes.values().for_each(|scope| self.visit_program_scope(scope)); + input.tests.values().for_each(|test| self.visit_test(test)); } 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) {} @@ -253,17 +251,11 @@ pub trait ProgramVisitor<'a>: StatementVisitor<'a> { fn visit_function_stub(&mut self, _input: &'a FunctionStub) {} fn visit_struct_stub(&mut self, _input: &'a Composite) {} -} -/// The `Visitor` for a `Test` -pub trait TestVisitor<'a>: ProgramVisitor<'a> { fn visit_test(&mut self, input: &'a Test) { + 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))); } } diff --git a/compiler/ast/src/program/mod.rs b/compiler/ast/src/program/mod.rs index 6cf62d9a9a..e0e71fa6f0 100644 --- a/compiler/ast/src/program/mod.rs +++ b/compiler/ast/src/program/mod.rs @@ -24,7 +24,7 @@ pub use program_scope::*; use leo_span::{Span, Symbol}; -use crate::Stub; +use crate::{Stub, Test}; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; use std::fmt; @@ -38,8 +38,9 @@ pub struct Program { pub stubs: IndexMap, /// A map from program names to program scopes. pub program_scopes: IndexMap, - /// The program tests. - pub tests: Vec, + /// A map from test file names to test defintions. + // TODO: This is a temporary way to store tests in the AST, without requiring an overhaul of the compiler. + pub tests: IndexMap, } impl fmt::Display for Program { @@ -62,6 +63,11 @@ impl fmt::Display for Program { impl Default for Program { /// Constructs an empty program node. fn default() -> Self { - Self { imports: IndexMap::new(), stubs: IndexMap::new(), program_scopes: IndexMap::new() } + Self { + imports: IndexMap::new(), + stubs: IndexMap::new(), + program_scopes: IndexMap::new(), + tests: IndexMap::new(), + } } } diff --git a/compiler/compiler/tests/integration/utilities/mod.rs b/compiler/compiler/tests/integration/utilities/mod.rs index f50d8c446f..4056b9a769 100644 --- a/compiler/compiler/tests/integration/utilities/mod.rs +++ b/compiler/compiler/tests/integration/utilities/mod.rs @@ -161,14 +161,7 @@ pub fn new_compiler( let output_dir = PathBuf::from("/tmp/output/"); fs::create_dir_all(output_dir.clone()).unwrap(); - Compiler::new( - program_name, - handler, - main_file_path, - output_dir, - compiler_options, - import_stubs, - ) + Compiler::new(program_name, handler, main_file_path, output_dir, compiler_options, import_stubs) } pub fn parse_program<'a>( diff --git a/compiler/parser/src/parser/file.rs b/compiler/parser/src/parser/file.rs index d20dd8b494..ff8b1b06a3 100644 --- a/compiler/parser/src/parser/file.rs +++ b/compiler/parser/src/parser/file.rs @@ -55,7 +55,7 @@ impl ParserContext<'_, N> { return Err(ParserError::missing_program_scope(self.token.span).into()); } - Ok(Program { imports, stubs: IndexMap::new(), program_scopes }) + Ok(Program { imports, stubs: IndexMap::new(), program_scopes, tests: IndexMap::new() }) } pub(super) fn unexpected_item(token: &SpannedToken, expected: &[Token]) -> ParserError { diff --git a/compiler/passes/src/static_single_assignment/rename_program.rs b/compiler/passes/src/static_single_assignment/rename_program.rs index 1a8dce86e1..4e21d0d6c4 100644 --- a/compiler/passes/src/static_single_assignment/rename_program.rs +++ b/compiler/passes/src/static_single_assignment/rename_program.rs @@ -28,6 +28,8 @@ use leo_ast::{ ProgramScopeConsumer, StatementConsumer, StructConsumer, + Test, + TestConsumer, }; use leo_span::{Symbol, sym}; @@ -109,6 +111,19 @@ impl ProgramScopeConsumer for StaticSingleAssigner<'_> { } } +impl TestConsumer for StaticSingleAssigner<'_> { + type Output = Test; + + fn consume_test(&mut self, input: Test) -> Self::Output { + Test { + structs: input.structs.into_iter().map(|(i, s)| (i, self.consume_struct(s))).collect(), + mappings: input.mappings, + functions: input.functions.into_iter().map(|(i, f)| (i, self.consume_function(f))).collect(), + consts: input.consts, + } + } +} + impl ProgramConsumer for StaticSingleAssigner<'_> { type Output = Program; @@ -125,6 +140,7 @@ impl ProgramConsumer for StaticSingleAssigner<'_> { .into_iter() .map(|(name, scope)| (name, self.consume_program_scope(scope))) .collect(), + tests: input.tests.into_iter().map(|(name, test)| (name, self.consume_test(test))).collect(), } } } 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/leo/cli/commands/build.rs b/leo/cli/commands/build.rs index 9fdd2657d7..38618c75ca 100644 --- a/leo/cli/commands/build.rs +++ b/leo/cli/commands/build.rs @@ -29,13 +29,13 @@ use snarkvm::{ }; use indexmap::IndexMap; +use leo_package::tst::TestDirectory; use snarkvm::prelude::CanaryV0; use std::{ io::Write, path::{Path, PathBuf}, str::FromStr, }; -use leo_package::tst::TestDirectory; impl From for CompilerOptions { fn from(options: BuildOptions) -> Self { @@ -196,15 +196,8 @@ fn handle_build(command: &LeoBuild, context: Context) -> Result<( #[allow(clippy::too_many_arguments)] fn compile_tests( package_path: &Path, - program_id: &ProgramID, handler: &Handler, options: BuildOptions, stubs: IndexMap, ) -> Result<()> { // Get the files in `/tests` directory. - let test_dir = TestDirectory::files(package_path)?; + let test_files = TestDirectory::files(package_path)?; - for test_file in test_dir { - // Compile the test file. + // Create a subdirectory for the tests. + let build_dir = BuildDirectory::open(package_path)?; + let test_dir = build_dir.join("tests"); + std::fs::create_dir_all(&test_dir) + .map_err(|e| CliError::general_cli_error(format!("Failed to create `build/tests` directory: {e}")))?; + for test_file in test_files { + // Compile the test file. + let compiler = Compiler::::new( + program_name.clone(), + handler, + file_path.clone(), + outputs.to_path_buf(), + Some(options.into()), + stubs.clone(), + ); + + let test_programs = compiler.compile_tests()?; + + // Write the test programs to the test directory. + for (test_name, test_program) in test_programs { + let mut test_file_path = test_dir.clone(); + test_file_path.push(format!("{}.aleo.test", test_name)); + std::fs::File::create(&test_file_path) + .map_err(|e| CliError::general_cli_error(format!("Failed to create test file: {e}")))? + .write_all(test_program.as_bytes()) + .map_err(|e| CliError::general_cli_error(format!("Failed to write test program: {e}")))?; + } } Ok(()) } - diff --git a/utils/retriever/src/retriever/mod.rs b/utils/retriever/src/retriever/mod.rs index 67e3d57c7f..30eb1239f4 100644 --- a/utils/retriever/src/retriever/mod.rs +++ b/utils/retriever/src/retriever/mod.rs @@ -296,9 +296,8 @@ impl Retriever { 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 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( From c6e49ad3976aafc96df626da7718d51343f05c89 Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Tue, 26 Nov 2024 18:57:31 -0800 Subject: [PATCH 12/23] Fix build pathway for tests --- compiler/ast/src/lib.rs | 11 ++ compiler/ast/src/tst/mod.rs | 4 +- compiler/compiler/src/compiler.rs | 116 ++++++++++------- .../compiler/tests/integration/compile.rs | 11 +- .../compiler/tests/integration/execute.rs | 11 +- .../tests/integration/utilities/mod.rs | 25 ++-- compiler/parser/src/parser/test.rs | 5 +- errors/src/errors/compiler/compiler_errors.rs | 6 +- leo/cli/commands/build.rs | 121 ++++++++++-------- leo/package/src/tst/default.rs | 8 +- leo/package/src/tst/directory.rs | 1 - tests/test-framework/benches/leo_compiler.rs | 15 ++- utils/disassembler/src/lib.rs | 2 +- 13 files changed, 183 insertions(+), 153 deletions(-) diff --git a/compiler/ast/src/lib.rs b/compiler/ast/src/lib.rs index 0894d8be15..26724d5ecb 100644 --- a/compiler/ast/src/lib.rs +++ b/compiler/ast/src/lib.rs @@ -73,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, @@ -84,6 +85,16 @@ 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, tests } = other.ast; + self.ast.imports.extend(imports); + self.ast.stubs.extend(stubs); + self.ast.program_scopes.extend(program_scopes); + self.ast.tests.extend(tests); + } + /// Returns a reference to the inner program AST representation. pub fn as_repr(&self) -> &Program { &self.ast diff --git a/compiler/ast/src/tst/mod.rs b/compiler/ast/src/tst/mod.rs index 00c961bc2e..71c57b1d3c 100644 --- a/compiler/ast/src/tst/mod.rs +++ b/compiler/ast/src/tst/mod.rs @@ -17,9 +17,9 @@ //! A Leo test consists of struct, record, function, transition, and mapping definitions. //! Anything that can be defined within a program scope can be defined in a test. -use crate::{Composite, ConstDeclaration, Function, Mapping, ProgramId, Stub}; +use crate::{Composite, ConstDeclaration, Function, Mapping}; -use leo_span::{Span, Symbol}; +use leo_span::Symbol; use serde::{Deserialize, Serialize}; use std::fmt; diff --git a/compiler/compiler/src/compiler.rs b/compiler/compiler/src/compiler.rs index fb21402b3b..f25a5013a6 100644 --- a/compiler/compiler/src/compiler.rs +++ b/compiler/compiler/src/compiler.rs @@ -30,20 +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 AST for the program. pub ast: Ast, /// Options configuring compilation. @@ -63,23 +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, + 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, ast: Ast::new(Program::default()), - compiler_options: compiler_options.unwrap_or_default(), + compiler_options, node_builder, assigner, import_stubs, @@ -88,44 +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)?; - + /// 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))?; @@ -289,7 +296,20 @@ impl<'a, N: Network> Compiler<'a, N> { /// 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. + let (symbol_table, struct_graph, call_graph) = self.compiler_stages()?; + // Run code generation. + let bytecode = self.code_generation_pass(&symbol_table, &struct_graph, &call_graph)?; + Ok(bytecode) + } + + /// Returns the compiled Leo tests. + pub fn compile_tests(&mut self) -> Result { + // Parse the program. + self.parse()?; // Copy the dependencies specified in `program.json` into the AST. self.add_import_stubs()?; // Run the intermediate compiler stages. @@ -303,11 +323,11 @@ impl<'a, N: Network> Compiler<'a, N> { 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"], )?; } @@ -318,12 +338,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"], )?; } @@ -349,7 +368,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/tests/integration/compile.rs b/compiler/compiler/tests/integration/compile.rs index ca2fc34f72..7a6c55ee8f 100644 --- a/compiler/compiler/tests/integration/compile.rs +++ b/compiler/compiler/tests/integration/compile.rs @@ -112,12 +112,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 f20ddf2ea3..d330ebb7dd 100644 --- a/compiler/compiler/tests/integration/execute.rs +++ b/compiler/compiler/tests/integration/execute.rs @@ -141,12 +141,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 4056b9a769..1216529f42 100644 --- a/compiler/compiler/tests/integration/utilities/mod.rs +++ b/compiler/compiler/tests/integration/utilities/mod.rs @@ -152,35 +152,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, 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/test.rs b/compiler/parser/src/parser/test.rs index 13520fea6b..6d493ebd96 100644 --- a/compiler/parser/src/parser/test.rs +++ b/compiler/parser/src/parser/test.rs @@ -16,7 +16,8 @@ use super::*; -use leo_errors::{ParserError, Result}; +use leo_errors::Result; +use leo_span::Symbol; impl ParserContext<'_, N> { /// Parses a test file. @@ -65,5 +66,3 @@ impl ParserContext<'_, N> { Ok(Test { consts, functions, structs, mappings }) } } - -use leo_span::{Symbol, sym}; 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/leo/cli/commands/build.rs b/leo/cli/commands/build.rs index 38618c75ca..6372afde0c 100644 --- a/leo/cli/commands/build.rs +++ b/leo/cli/commands/build.rs @@ -30,6 +30,7 @@ use snarkvm::{ use indexmap::IndexMap; use leo_package::tst::TestDirectory; +use leo_span::source_map::FileName; use snarkvm::prelude::CanaryV0; use std::{ io::Write, @@ -168,19 +169,16 @@ fn handle_build(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) @@ -196,38 +194,37 @@ fn handle_build(command: &LeoBuild, context: Context) -> Result<(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(|e| { + CliError::general_cli_error(format!("Failed to read source file '{:?}': {e}", file_path.as_path())) + })?; // 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(), - 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()?; @@ -238,13 +235,14 @@ 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, @@ -253,34 +251,45 @@ fn compile_tests( // Get the files in `/tests` directory. let test_files = TestDirectory::files(package_path)?; - // Create a subdirectory for the tests. - let build_dir = BuildDirectory::open(package_path)?; - let test_dir = build_dir.join("tests"); - std::fs::create_dir_all(&test_dir) - .map_err(|e| CliError::general_cli_error(format!("Failed to create `build/tests` directory: {e}")))?; + // 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 file_path in test_files { + // Read the test file. + let file_content = std::fs::read_to_string(&file_path).map_err(|e| { + CliError::general_cli_error(format!( + "Failed to read test file '{:?}': {e}", + file_path.clone().into_os_string() + )) + })?; + + // Reset the compiler with the test file content. + compiler.reset(vec![(FileName::Real(file_path.clone()), file_content)]); - for test_file in test_files { // Compile the test file. - let compiler = Compiler::::new( - program_name.clone(), - handler, - file_path.clone(), - outputs.to_path_buf(), - Some(options.into()), - stubs.clone(), - ); - - let test_programs = compiler.compile_tests()?; - - // Write the test programs to the test directory. - for (test_name, test_program) in test_programs { - let mut test_file_path = test_dir.clone(); - test_file_path.push(format!("{}.aleo.test", test_name)); - std::fs::File::create(&test_file_path) - .map_err(|e| CliError::general_cli_error(format!("Failed to create test file: {e}")))? - .write_all(test_program.as_bytes()) - .map_err(|e| CliError::general_cli_error(format!("Failed to write test program: {e}")))?; - } + let output = compiler.compile_tests()?; + + // Create a subdirectory for the test. + let build_dir = BuildDirectory::open(package_path)?; + let test_dir = build_dir.join("tests"); + std::fs::create_dir_all(&test_dir) + .map_err(|e| CliError::general_cli_error(format!("Failed to create `build/tests` directory: {e}")))?; + + // Write the outputs. + let test_file_name = file_path.file_name().unwrap().to_str().unwrap(); + let test_file_path = test_dir.join(test_file_name); + std::fs::write(&test_file_path, output).map_err(|e| { + CliError::general_cli_error(format!("Failed to write test file '{:?}': {e}", test_file_path)) + })?; } + tracing::info!("✅ Compiled tests for '{name}'"); Ok(()) } diff --git a/leo/package/src/tst/default.rs b/leo/package/src/tst/default.rs index 6dc943050c..01770a2175 100644 --- a/leo/package/src/tst/default.rs +++ b/leo/package/src/tst/default.rs @@ -19,7 +19,6 @@ use crate::tst::directory::TEST_DIRECTORY_NAME; use leo_errors::{PackageError, Result}; -use serde::Deserialize; use std::{borrow::Cow, fs::File, io::Write, path::Path}; pub static DEFAULT_TEST_FILENAME: &str = "test.leo"; @@ -41,8 +40,7 @@ impl DefaultTestFile { } fn template() -> String { - format!( - r#"// A default Leo test file. + r#"// A default Leo test file. // To learn more about testing your program, see the documentation at https://docs.leo-lang.org @native_test @@ -51,7 +49,7 @@ transition test_helloworld() {{ let result: u32 = helloworld.aleo/main(1u32, 2u32) assert_eq!(result, 3u32) }} -"#, - ) +"# + .to_string() } } diff --git a/leo/package/src/tst/directory.rs b/leo/package/src/tst/directory.rs index 03e4b1da5c..6780f64894 100644 --- a/leo/package/src/tst/directory.rs +++ b/leo/package/src/tst/directory.rs @@ -18,7 +18,6 @@ use crate::parse_file_paths; use leo_errors::{PackageError, Result}; -use crate::source::MAIN_FILENAME; use std::{ borrow::Cow, fs, diff --git a/tests/test-framework/benches/leo_compiler.rs b/tests/test-framework/benches/leo_compiler.rs index 4334898882..4b9c6fa736 100644 --- a/tests/test-framework/benches/leo_compiler.rs +++ b/tests/test-framework/benches/leo_compiler.rs @@ -87,9 +87,9 @@ fn new_compiler(handler: &Handler) -> Compiler<'_, CurrentNetwork> { Compiler::new( String::from("test"), handler, + vec![], PathBuf::from(String::new()), - PathBuf::from(String::new()), - Some(CompilerOptions { + CompilerOptions { build: BuildOptions { dce_enabled: true, conditional_block_max_depth: 10, @@ -110,7 +110,7 @@ fn new_compiler(handler: &Handler) -> Compiler<'_, CurrentNetwork> { dce_ast: false, build_tests: 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/utils/disassembler/src/lib.rs b/utils/disassembler/src/lib.rs index 10d05c25f8..72444bf4c3 100644 --- a/utils/disassembler/src/lib.rs +++ b/utils/disassembler/src/lib.rs @@ -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) => { From ff4e46f0401209b747dbe72738b40057f6c2399f Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Tue, 26 Nov 2024 19:42:06 -0800 Subject: [PATCH 13/23] Test flow up to loop unrolling working --- compiler/ast/src/passes/reconstructor.rs | 2 +- compiler/ast/src/passes/visitor.rs | 2 +- compiler/ast/src/program/mod.rs | 9 +- compiler/compiler/src/compiler.rs | 193 +++++++++++++++++- compiler/parser/src/lib.rs | 15 +- compiler/parser/src/parser/file.rs | 2 +- compiler/parser/src/parser/mod.rs | 12 ++ compiler/parser/src/parser/test.rs | 2 +- .../rename_program.rs | 2 +- leo/package/src/tst/default.rs | 4 +- 10 files changed, 225 insertions(+), 18 deletions(-) diff --git a/compiler/ast/src/passes/reconstructor.rs b/compiler/ast/src/passes/reconstructor.rs index 23f5118696..1f6a2692fd 100644 --- a/compiler/ast/src/passes/reconstructor.rs +++ b/compiler/ast/src/passes/reconstructor.rs @@ -425,7 +425,7 @@ pub trait ProgramReconstructor: StatementReconstructor { .into_iter() .map(|(id, scope)| (id, self.reconstruct_program_scope(scope))) .collect(), - tests: input.tests.into_iter().map(|(id, test)| (id, self.reconstruct_test(test))).collect(), + tests: input.tests.into_iter().map(|test| self.reconstruct_test(test)).collect(), } } diff --git a/compiler/ast/src/passes/visitor.rs b/compiler/ast/src/passes/visitor.rs index a98a4b063c..412841331b 100644 --- a/compiler/ast/src/passes/visitor.rs +++ b/compiler/ast/src/passes/visitor.rs @@ -224,7 +224,7 @@ pub trait ProgramVisitor<'a>: StatementVisitor<'a> { input.imports.values().for_each(|import| self.visit_import(&import.0)); input.stubs.values().for_each(|stub| self.visit_stub(stub)); input.program_scopes.values().for_each(|scope| self.visit_program_scope(scope)); - input.tests.values().for_each(|test| self.visit_test(test)); + input.tests.iter().for_each(|test| self.visit_test(test)); } fn visit_program_scope(&mut self, input: &'a ProgramScope) { diff --git a/compiler/ast/src/program/mod.rs b/compiler/ast/src/program/mod.rs index e0e71fa6f0..176d35b08e 100644 --- a/compiler/ast/src/program/mod.rs +++ b/compiler/ast/src/program/mod.rs @@ -40,7 +40,7 @@ pub struct Program { pub program_scopes: IndexMap, /// A map from test file names to test defintions. // TODO: This is a temporary way to store tests in the AST, without requiring an overhaul of the compiler. - pub tests: IndexMap, + pub tests: Vec, } impl fmt::Display for Program { @@ -63,11 +63,6 @@ impl fmt::Display for Program { impl Default for Program { /// Constructs an empty program node. fn default() -> Self { - Self { - imports: IndexMap::new(), - stubs: IndexMap::new(), - program_scopes: IndexMap::new(), - tests: IndexMap::new(), - } + Self { imports: IndexMap::new(), stubs: IndexMap::new(), program_scopes: IndexMap::new(), tests: Vec::new() } } } diff --git a/compiler/compiler/src/compiler.rs b/compiler/compiler/src/compiler.rs index f25a5013a6..8ebea25e7d 100644 --- a/compiler/compiler/src/compiler.rs +++ b/compiler/compiler/src/compiler.rs @@ -133,6 +133,31 @@ impl<'a, N: Network> Compiler<'a, N> { Ok(()) } + /// Parses and stores the test source , constructs the AST, and optionally outputs it. + pub fn parse_test(&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_test_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(()) + } + /// Runs the symbol table pass. pub fn symbol_table_pass(&self) -> Result { let symbol_table = SymbolTableCreator::do_pass((&self.ast, self.handler))?; @@ -293,6 +318,168 @@ impl<'a, N: Network> Compiler<'a, N> { Ok((st, struct_graph, call_graph)) } + /// Runs the test symbol table pass. + pub fn test_symbol_table_pass(&self) -> Result { + let symbol_table = SymbolTableCreator::do_pass((&self.ast, self.handler))?; + if self.compiler_options.output.initial_symbol_table { + self.write_symbol_table_to_json("initial_symbol_table.json", &symbol_table)?; + } + Ok(symbol_table) + } + + /// Runs the test type checker pass. + pub fn test_type_checker_pass( + &'a self, + symbol_table: SymbolTable, + ) -> Result<(SymbolTable, StructGraph, CallGraph)> { + let (symbol_table, struct_graph, call_graph) = TypeChecker::::do_pass(( + &self.ast, + self.handler, + symbol_table, + &self.type_table, + self.compiler_options.build.conditional_block_max_depth, + self.compiler_options.build.disable_conditional_branch_type_checking, + self.compiler_options.output.build_tests, + ))?; + if self.compiler_options.output.type_checked_symbol_table { + self.write_symbol_table_to_json("type_checked_symbol_table.json", &symbol_table)?; + } + Ok((symbol_table, struct_graph, call_graph)) + } + + /// Runs the test loop unrolling pass. + pub fn test_loop_unrolling_pass(&mut self, symbol_table: SymbolTable) -> Result { + let (ast, symbol_table) = Unroller::do_pass(( + std::mem::take(&mut self.ast), + self.handler, + &self.node_builder, + symbol_table, + &self.type_table, + ))?; + self.ast = ast; + + if self.compiler_options.output.unrolled_ast { + self.write_ast_to_json("unrolled_ast.json")?; + } + + if self.compiler_options.output.unrolled_symbol_table { + self.write_symbol_table_to_json("unrolled_symbol_table.json", &symbol_table)?; + } + + Ok(symbol_table) + } + + /// Runs the test static single assignment pass. + pub fn test_static_single_assignment_pass(&mut self, symbol_table: &SymbolTable) -> Result<()> { + self.ast = StaticSingleAssigner::do_pass(( + std::mem::take(&mut self.ast), + &self.node_builder, + &self.assigner, + symbol_table, + &self.type_table, + ))?; + + if self.compiler_options.output.ssa_ast { + self.write_ast_to_json("ssa_ast.json")?; + } + + Ok(()) + } + + /// Runs the test flattening pass. + pub fn test_flattening_pass(&mut self, symbol_table: &SymbolTable) -> Result<()> { + self.ast = Flattener::do_pass(( + std::mem::take(&mut self.ast), + symbol_table, + &self.type_table, + &self.node_builder, + &self.assigner, + ))?; + + if self.compiler_options.output.flattened_ast { + self.write_ast_to_json("flattened_ast.json")?; + } + + Ok(()) + } + + /// Runs the test destructuring pass. + pub fn test_destructuring_pass(&mut self) -> Result<()> { + self.ast = Destructurer::do_pass(( + std::mem::take(&mut self.ast), + &self.type_table, + &self.node_builder, + &self.assigner, + ))?; + + if self.compiler_options.output.destructured_ast { + self.write_ast_to_json("destructured_ast.json")?; + } + + Ok(()) + } + + /// Runs the test function inlining pass. + pub fn test_function_inlining_pass(&mut self, call_graph: &CallGraph) -> Result<()> { + let ast = FunctionInliner::do_pass(( + std::mem::take(&mut self.ast), + &self.node_builder, + call_graph, + &self.assigner, + &self.type_table, + ))?; + self.ast = ast; + + if self.compiler_options.output.inlined_ast { + self.write_ast_to_json("inlined_ast.json")?; + } + + Ok(()) + } + + /// Runs the test dead code elimination pass. + pub fn test_dead_code_elimination_pass(&mut self) -> Result<()> { + if self.compiler_options.build.dce_enabled { + self.ast = DeadCodeEliminator::do_pass((std::mem::take(&mut self.ast), &self.node_builder))?; + } + + if self.compiler_options.output.dce_ast { + self.write_ast_to_json("dce_ast.json")?; + } + + Ok(()) + } + + /// Runs the test code generation pass. + pub fn test_code_generation_pass( + &mut self, + symbol_table: &SymbolTable, + struct_graph: &StructGraph, + call_graph: &CallGraph, + ) -> Result { + CodeGenerator::do_pass((&self.ast, symbol_table, &self.type_table, struct_graph, call_graph, &self.ast.ast)) + } + + /// Runs the test compiler stages. + pub fn test_compiler_stages(&mut self) -> Result<(SymbolTable, StructGraph, CallGraph)> { + let st = self.test_symbol_table_pass()?; + let (st, struct_graph, call_graph) = self.test_type_checker_pass(st)?; + + let st = self.test_loop_unrolling_pass(st)?; + + self.test_static_single_assignment_pass(&st)?; + + self.test_flattening_pass(&st)?; + + self.test_destructuring_pass()?; + + self.test_function_inlining_pass(&call_graph)?; + + self.test_dead_code_elimination_pass()?; + + Ok((st, struct_graph, call_graph)) + } + /// Returns a compiled Leo program. pub fn compile(&mut self) -> Result { // Parse the program. @@ -309,13 +496,13 @@ impl<'a, N: Network> Compiler<'a, N> { /// Returns the compiled Leo tests. pub fn compile_tests(&mut self) -> Result { // Parse the program. - self.parse()?; + self.parse_test()?; // 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) = self.compiler_stages()?; + let (symbol_table, struct_graph, call_graph) = self.test_compiler_stages()?; // Run code generation. - let bytecode = self.code_generation_pass(&symbol_table, &struct_graph, &call_graph)?; + let bytecode = self.test_code_generation_pass(&symbol_table, &struct_graph, &call_graph)?; Ok(bytecode) } diff --git a/compiler/parser/src/lib.rs b/compiler/parser/src/lib.rs index 82fff54479..d6e6f7687d 100644 --- a/compiler/parser/src/lib.rs +++ b/compiler/parser/src/lib.rs @@ -31,7 +31,7 @@ pub(crate) use tokenizer::*; pub mod parser; pub use parser::*; -use leo_ast::{Ast, NodeBuilder}; +use leo_ast::{Ast, NodeBuilder, Test}; use leo_errors::{Result, emitter::Handler}; use snarkvm::prelude::Network; @@ -48,3 +48,16 @@ pub fn parse_ast( ) -> Result { Ok(Ast::new(parse::(handler, node_builder, source, start_pos)?)) } + +/// Creates a new test AST from a given file path and source code text. +pub fn parse_test_ast( + handler: &Handler, + node_builder: &NodeBuilder, + source: &str, + start_pos: BytePos, +) -> Result { + let test = parse_test::(handler, node_builder, source, start_pos)?; + let mut program = leo_ast::Program::default(); + program.tests.push(test); + Ok(Ast::new(program)) +} diff --git a/compiler/parser/src/parser/file.rs b/compiler/parser/src/parser/file.rs index ff8b1b06a3..0f52e8f4a7 100644 --- a/compiler/parser/src/parser/file.rs +++ b/compiler/parser/src/parser/file.rs @@ -55,7 +55,7 @@ impl ParserContext<'_, N> { return Err(ParserError::missing_program_scope(self.token.span).into()); } - Ok(Program { imports, stubs: IndexMap::new(), program_scopes, tests: IndexMap::new() }) + Ok(Program { imports, stubs: IndexMap::new(), program_scopes, tests: Vec::new() }) } pub(super) fn unexpected_item(token: &SpannedToken, expected: &[Token]) -> ParserError { diff --git a/compiler/parser/src/parser/mod.rs b/compiler/parser/src/parser/mod.rs index 5cadb6f9b7..e4c261f4d3 100644 --- a/compiler/parser/src/parser/mod.rs +++ b/compiler/parser/src/parser/mod.rs @@ -50,3 +50,15 @@ pub fn parse( tokens.parse_program() } + +/// Creates a new test from a given file path and source code text. +pub fn parse_test( + handler: &Handler, + node_builder: &NodeBuilder, + source: &str, + start_pos: BytePos, +) -> Result { + let mut tokens = ParserContext::::new(handler, node_builder, crate::tokenize(source, start_pos)?); + + tokens.parse_test() +} diff --git a/compiler/parser/src/parser/test.rs b/compiler/parser/src/parser/test.rs index 6d493ebd96..dd28efcd31 100644 --- a/compiler/parser/src/parser/test.rs +++ b/compiler/parser/src/parser/test.rs @@ -21,7 +21,7 @@ use leo_span::Symbol; impl ParserContext<'_, N> { /// Parses a test file. - fn parse_test(&mut self) -> Result { + pub fn parse_test(&mut self) -> Result { // Initialize storage for the components of the test file let mut consts: Vec<(Symbol, ConstDeclaration)> = Vec::new(); let mut functions = Vec::new(); diff --git a/compiler/passes/src/static_single_assignment/rename_program.rs b/compiler/passes/src/static_single_assignment/rename_program.rs index 4e21d0d6c4..321d1273f7 100644 --- a/compiler/passes/src/static_single_assignment/rename_program.rs +++ b/compiler/passes/src/static_single_assignment/rename_program.rs @@ -140,7 +140,7 @@ impl ProgramConsumer for StaticSingleAssigner<'_> { .into_iter() .map(|(name, scope)| (name, self.consume_program_scope(scope))) .collect(), - tests: input.tests.into_iter().map(|(name, test)| (name, self.consume_test(test))).collect(), + tests: input.tests.into_iter().map(|test| self.consume_test(test)).collect(), } } } diff --git a/leo/package/src/tst/default.rs b/leo/package/src/tst/default.rs index 01770a2175..407b4c76a1 100644 --- a/leo/package/src/tst/default.rs +++ b/leo/package/src/tst/default.rs @@ -46,8 +46,8 @@ impl DefaultTestFile { @native_test @interpreted_test transition test_helloworld() {{ - let result: u32 = helloworld.aleo/main(1u32, 2u32) - assert_eq!(result, 3u32) + let result: u32 = helloworld.aleo/main(1u32, 2u32); + assert_eq(result, 3u32); }} "# .to_string() From 8f0b83692fd37eb09660c36c023905c661526c76 Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Mon, 9 Dec 2024 09:59:11 -0800 Subject: [PATCH 14/23] Remove Test and use Program; tests are programs --- compiler/ast/src/lib.rs | 6 +- compiler/ast/src/passes/consumer.rs | 7 -- compiler/ast/src/passes/reconstructor.rs | 17 ----- compiler/ast/src/passes/visitor.rs | 8 --- compiler/ast/src/program/mod.rs | 7 +- compiler/ast/src/tst/mod.rs | 55 --------------- compiler/compiler/src/compiler.rs | 27 +------- compiler/parser/src/lib.rs | 15 +--- compiler/parser/src/parser/file.rs | 2 +- compiler/parser/src/parser/mod.rs | 13 ---- compiler/parser/src/parser/test.rs | 68 ------------------- .../rename_program.rs | 16 ----- .../passes/src/type_checking/check_program.rs | 2 +- compiler/span/src/symbol.rs | 2 +- leo/package/src/tst/default.rs | 19 +++--- temp/.gitignore | 5 ++ temp/build/README.md | 13 ++++ temp/build/main.aleo | 7 ++ temp/build/program.json | 6 ++ temp/leo.lock | 1 + temp/program.json | 8 +++ temp/src/main.leo | 7 ++ temp/tests/test.leo | 13 ++++ 23 files changed, 79 insertions(+), 245 deletions(-) delete mode 100644 compiler/ast/src/tst/mod.rs delete mode 100644 compiler/parser/src/parser/test.rs create mode 100644 temp/.gitignore create mode 100644 temp/build/README.md create mode 100644 temp/build/main.aleo create mode 100644 temp/build/program.json create mode 100644 temp/leo.lock create mode 100644 temp/program.json create mode 100644 temp/src/main.leo create mode 100644 temp/tests/test.leo diff --git a/compiler/ast/src/lib.rs b/compiler/ast/src/lib.rs index 26724d5ecb..1a42274a76 100644 --- a/compiler/ast/src/lib.rs +++ b/compiler/ast/src/lib.rs @@ -52,9 +52,6 @@ pub use self::program::*; pub mod statement; pub use self::statement::*; -pub mod tst; -pub use self::tst::*; - pub mod types; pub use self::types::*; @@ -88,11 +85,10 @@ impl Ast { /// 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, tests } = other.ast; + 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); - self.ast.tests.extend(tests); } /// Returns a reference to the inner program AST representation. diff --git a/compiler/ast/src/passes/consumer.rs b/compiler/ast/src/passes/consumer.rs index 5f7053ee50..3d8edea460 100644 --- a/compiler/ast/src/passes/consumer.rs +++ b/compiler/ast/src/passes/consumer.rs @@ -148,13 +148,6 @@ pub trait ProgramScopeConsumer { fn consume_program_scope(&mut self, input: ProgramScope) -> Self::Output; } -/// A `Consumer` for a `Test` -pub trait TestConsumer { - type Output; - - fn consume_test(&mut self, input: Test) -> Self::Output; -} - /// A Consumer trait for the program represented by the AST. pub trait ProgramConsumer { type Output; diff --git a/compiler/ast/src/passes/reconstructor.rs b/compiler/ast/src/passes/reconstructor.rs index 1f6a2692fd..4371a0e12c 100644 --- a/compiler/ast/src/passes/reconstructor.rs +++ b/compiler/ast/src/passes/reconstructor.rs @@ -425,7 +425,6 @@ pub trait ProgramReconstructor: StatementReconstructor { .into_iter() .map(|(id, scope)| (id, self.reconstruct_program_scope(scope))) .collect(), - tests: input.tests.into_iter().map(|test| self.reconstruct_test(test)).collect(), } } @@ -488,20 +487,4 @@ pub trait ProgramReconstructor: StatementReconstructor { fn reconstruct_mapping(&mut self, input: Mapping) -> Mapping { input } - - fn reconstruct_test(&mut self, input: Test) -> Test { - Test { - consts: input - .consts - .into_iter() - .map(|(i, c)| match self.reconstruct_const(c) { - (Statement::Const(declaration), _) => (i, declaration), - _ => 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(|(i, m)| (i, self.reconstruct_mapping(m))).collect(), - functions: input.functions.into_iter().map(|(i, f)| (i, self.reconstruct_function(f))).collect(), - } - } } diff --git a/compiler/ast/src/passes/visitor.rs b/compiler/ast/src/passes/visitor.rs index 412841331b..ca738feecb 100644 --- a/compiler/ast/src/passes/visitor.rs +++ b/compiler/ast/src/passes/visitor.rs @@ -224,7 +224,6 @@ pub trait ProgramVisitor<'a>: StatementVisitor<'a> { input.imports.values().for_each(|import| self.visit_import(&import.0)); input.stubs.values().for_each(|stub| self.visit_stub(stub)); input.program_scopes.values().for_each(|scope| self.visit_program_scope(scope)); - input.tests.iter().for_each(|test| self.visit_test(test)); } fn visit_program_scope(&mut self, input: &'a ProgramScope) { @@ -251,11 +250,4 @@ pub trait ProgramVisitor<'a>: StatementVisitor<'a> { fn visit_function_stub(&mut self, _input: &'a FunctionStub) {} fn visit_struct_stub(&mut self, _input: &'a Composite) {} - - fn visit_test(&mut self, input: &'a Test) { - 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))); - } } diff --git a/compiler/ast/src/program/mod.rs b/compiler/ast/src/program/mod.rs index 176d35b08e..083aa6072a 100644 --- a/compiler/ast/src/program/mod.rs +++ b/compiler/ast/src/program/mod.rs @@ -24,7 +24,7 @@ pub use program_scope::*; use leo_span::{Span, Symbol}; -use crate::{Stub, Test}; +use crate::Stub; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; use std::fmt; @@ -38,9 +38,6 @@ pub struct Program { pub stubs: IndexMap, /// A map from program names to program scopes. pub program_scopes: IndexMap, - /// A map from test file names to test defintions. - // TODO: This is a temporary way to store tests in the AST, without requiring an overhaul of the compiler. - pub tests: Vec, } impl fmt::Display for Program { @@ -63,6 +60,6 @@ impl fmt::Display for Program { impl Default for Program { /// Constructs an empty program node. fn default() -> Self { - Self { imports: IndexMap::new(), stubs: IndexMap::new(), program_scopes: IndexMap::new(), tests: Vec::new() } + Self { imports: IndexMap::new(), stubs: IndexMap::new(), program_scopes: IndexMap::new() } } } diff --git a/compiler/ast/src/tst/mod.rs b/compiler/ast/src/tst/mod.rs deleted file mode 100644 index 71c57b1d3c..0000000000 --- a/compiler/ast/src/tst/mod.rs +++ /dev/null @@ -1,55 +0,0 @@ -// 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 . - -//! A Leo test consists of struct, record, function, transition, and mapping definitions. -//! Anything that can be defined within a program scope can be defined in a test. - -use crate::{Composite, ConstDeclaration, Function, Mapping}; - -use leo_span::Symbol; -use serde::{Deserialize, Serialize}; -use std::fmt; - -/// An abstract syntax tree for a Leo test. -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub struct Test { - /// The constant definitions - pub consts: Vec<(Symbol, ConstDeclaration)>, - /// A vector of struct/record definitions. - pub structs: Vec<(Symbol, Composite)>, - /// A vector of mapping definitions. - pub mappings: Vec<(Symbol, Mapping)>, - /// A vector of function definitions. - pub functions: Vec<(Symbol, Function)>, -} - -impl fmt::Display for Test { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - for (_, const_) in self.consts.iter() { - writeln!(f, " {const_}")?; - } - for (_, struct_) in self.structs.iter() { - writeln!(f, " {struct_}")?; - } - for (_, mapping) in self.mappings.iter() { - writeln!(f, " {mapping}")?; - } - for (_, function) in self.functions.iter() { - writeln!(f, " {function}")?; - } - Ok(()) - } -} diff --git a/compiler/compiler/src/compiler.rs b/compiler/compiler/src/compiler.rs index 8ebea25e7d..12ebeb6868 100644 --- a/compiler/compiler/src/compiler.rs +++ b/compiler/compiler/src/compiler.rs @@ -133,31 +133,6 @@ impl<'a, N: Network> Compiler<'a, N> { Ok(()) } - /// Parses and stores the test source , constructs the AST, and optionally outputs it. - pub fn parse_test(&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_test_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(()) - } - /// Runs the symbol table pass. pub fn symbol_table_pass(&self) -> Result { let symbol_table = SymbolTableCreator::do_pass((&self.ast, self.handler))?; @@ -496,7 +471,7 @@ impl<'a, N: Network> Compiler<'a, N> { /// Returns the compiled Leo tests. pub fn compile_tests(&mut self) -> Result { // Parse the program. - self.parse_test()?; + self.parse()?; // Copy the dependencies specified in `program.json` into the AST. self.add_import_stubs()?; // Run the intermediate compiler stages. diff --git a/compiler/parser/src/lib.rs b/compiler/parser/src/lib.rs index d6e6f7687d..82fff54479 100644 --- a/compiler/parser/src/lib.rs +++ b/compiler/parser/src/lib.rs @@ -31,7 +31,7 @@ pub(crate) use tokenizer::*; pub mod parser; pub use parser::*; -use leo_ast::{Ast, NodeBuilder, Test}; +use leo_ast::{Ast, NodeBuilder}; use leo_errors::{Result, emitter::Handler}; use snarkvm::prelude::Network; @@ -48,16 +48,3 @@ pub fn parse_ast( ) -> Result { Ok(Ast::new(parse::(handler, node_builder, source, start_pos)?)) } - -/// Creates a new test AST from a given file path and source code text. -pub fn parse_test_ast( - handler: &Handler, - node_builder: &NodeBuilder, - source: &str, - start_pos: BytePos, -) -> Result { - let test = parse_test::(handler, node_builder, source, start_pos)?; - let mut program = leo_ast::Program::default(); - program.tests.push(test); - Ok(Ast::new(program)) -} diff --git a/compiler/parser/src/parser/file.rs b/compiler/parser/src/parser/file.rs index 0f52e8f4a7..d20dd8b494 100644 --- a/compiler/parser/src/parser/file.rs +++ b/compiler/parser/src/parser/file.rs @@ -55,7 +55,7 @@ impl ParserContext<'_, N> { return Err(ParserError::missing_program_scope(self.token.span).into()); } - Ok(Program { imports, stubs: IndexMap::new(), program_scopes, tests: Vec::new() }) + Ok(Program { imports, stubs: IndexMap::new(), program_scopes }) } pub(super) fn unexpected_item(token: &SpannedToken, expected: &[Token]) -> ParserError { diff --git a/compiler/parser/src/parser/mod.rs b/compiler/parser/src/parser/mod.rs index e4c261f4d3..f39da3a305 100644 --- a/compiler/parser/src/parser/mod.rs +++ b/compiler/parser/src/parser/mod.rs @@ -36,7 +36,6 @@ pub(super) use context::ParserContext; mod expression; mod file; mod statement; -mod test; pub(super) mod type_; /// Creates a new program from a given file path and source code text. @@ -50,15 +49,3 @@ pub fn parse( tokens.parse_program() } - -/// Creates a new test from a given file path and source code text. -pub fn parse_test( - handler: &Handler, - node_builder: &NodeBuilder, - source: &str, - start_pos: BytePos, -) -> Result { - let mut tokens = ParserContext::::new(handler, node_builder, crate::tokenize(source, start_pos)?); - - tokens.parse_test() -} diff --git a/compiler/parser/src/parser/test.rs b/compiler/parser/src/parser/test.rs deleted file mode 100644 index dd28efcd31..0000000000 --- a/compiler/parser/src/parser/test.rs +++ /dev/null @@ -1,68 +0,0 @@ -// 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 leo_errors::Result; -use leo_span::Symbol; - -impl ParserContext<'_, N> { - /// Parses a test file. - pub fn parse_test(&mut self) -> Result { - // Initialize storage for the components of the test file - let mut consts: Vec<(Symbol, ConstDeclaration)> = Vec::new(); - let mut functions = Vec::new(); - let mut structs: Vec<(Symbol, Composite)> = Vec::new(); - let mut mappings: Vec<(Symbol, Mapping)> = Vec::new(); - // Parse the components of the test file. - while self.has_next() { - match &self.token.token { - Token::Const => { - let declaration = self.parse_const_declaration_statement()?; - consts.push((Symbol::intern(&declaration.place.to_string()), declaration)); - } - Token::Struct | Token::Record => { - let (id, struct_) = self.parse_struct()?; - structs.push((id, struct_)); - } - Token::Mapping => { - let (id, mapping) = self.parse_mapping()?; - mappings.push((id, mapping)); - } - Token::At | Token::Async | Token::Function | Token::Transition | Token::Inline | Token::Interpret => { - let (id, function) = self.parse_function()?; - functions.push((id, function)); - } - _ => { - return Err(Self::unexpected_item(&self.token, &[ - Token::Const, - Token::Struct, - Token::Record, - Token::Mapping, - Token::At, - Token::Async, - Token::Function, - Token::Transition, - Token::Inline, - ]) - .into()); - } - } - } - - Ok(Test { consts, functions, structs, mappings }) - } -} diff --git a/compiler/passes/src/static_single_assignment/rename_program.rs b/compiler/passes/src/static_single_assignment/rename_program.rs index 321d1273f7..1a8dce86e1 100644 --- a/compiler/passes/src/static_single_assignment/rename_program.rs +++ b/compiler/passes/src/static_single_assignment/rename_program.rs @@ -28,8 +28,6 @@ use leo_ast::{ ProgramScopeConsumer, StatementConsumer, StructConsumer, - Test, - TestConsumer, }; use leo_span::{Symbol, sym}; @@ -111,19 +109,6 @@ impl ProgramScopeConsumer for StaticSingleAssigner<'_> { } } -impl TestConsumer for StaticSingleAssigner<'_> { - type Output = Test; - - fn consume_test(&mut self, input: Test) -> Self::Output { - Test { - structs: input.structs.into_iter().map(|(i, s)| (i, self.consume_struct(s))).collect(), - mappings: input.mappings, - functions: input.functions.into_iter().map(|(i, f)| (i, self.consume_function(f))).collect(), - consts: input.consts, - } - } -} - impl ProgramConsumer for StaticSingleAssigner<'_> { type Output = Program; @@ -140,7 +125,6 @@ impl ProgramConsumer for StaticSingleAssigner<'_> { .into_iter() .map(|(name, scope)| (name, self.consume_program_scope(scope))) .collect(), - tests: input.tests.into_iter().map(|test| self.consume_test(test)).collect(), } } } diff --git a/compiler/passes/src/type_checking/check_program.rs b/compiler/passes/src/type_checking/check_program.rs index 5ff804a0db..479279b5de 100644 --- a/compiler/passes/src/type_checking/check_program.rs +++ b/compiler/passes/src/type_checking/check_program.rs @@ -229,7 +229,7 @@ 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. - let valid_annotations = [sym::should_fail, sym::native_test, sym::interpreted_test]; + let valid_annotations = [sym::should_fail, sym::compiled_test, sym::interpreted_test]; for annotation in function.annotations.iter() { // All Leo annotations currently apply only to test code. if !self.build_tests || !valid_annotations.contains(&annotation.identifier.name) { diff --git a/compiler/span/src/symbol.rs b/compiler/span/src/symbol.rs index b1c845aa34..c787b558ad 100644 --- a/compiler/span/src/symbol.rs +++ b/compiler/span/src/symbol.rs @@ -233,7 +233,7 @@ symbols! { // annotations should_fail, - native_test, + compiled_test, interpreted_test, // general keywords diff --git a/leo/package/src/tst/default.rs b/leo/package/src/tst/default.rs index 407b4c76a1..04230b1b87 100644 --- a/leo/package/src/tst/default.rs +++ b/leo/package/src/tst/default.rs @@ -40,15 +40,18 @@ impl DefaultTestFile { } fn template() -> String { - r#"// A default Leo test file. + r#"// A Leo test file. // To learn more about testing your program, see the documentation at https://docs.leo-lang.org - -@native_test -@interpreted_test -transition test_helloworld() {{ - let result: u32 = helloworld.aleo/main(1u32, 2u32); - assert_eq(result, 3u32); -}} +import helloworld.aleo; + +program test.aleo { + @compiled_test + @interpreted_test + transition test_helloworld() {{ + let result: u32 = helloworld.aleo/main(1u32, 2u32); + assert_eq(result, 3u32); + }} +} "# .to_string() } diff --git a/temp/.gitignore b/temp/.gitignore new file mode 100644 index 0000000000..f721f7f6f4 --- /dev/null +++ b/temp/.gitignore @@ -0,0 +1,5 @@ +.env +*.avm +*.prover +*.verifier +outputs/ diff --git a/temp/build/README.md b/temp/build/README.md new file mode 100644 index 0000000000..4443cf700d --- /dev/null +++ b/temp/build/README.md @@ -0,0 +1,13 @@ +# temp.aleo + +## Build Guide + +To compile this Aleo program, run: +```bash +snarkvm build +``` + +To execute this Aleo program, run: +```bash +snarkvm run hello +``` diff --git a/temp/build/main.aleo b/temp/build/main.aleo new file mode 100644 index 0000000000..b6fba11e20 --- /dev/null +++ b/temp/build/main.aleo @@ -0,0 +1,7 @@ +program temp.aleo; + +function main: + input r0 as u32.public; + input r1 as u32.private; + add r0 r1 into r2; + output r2 as u32.private; diff --git a/temp/build/program.json b/temp/build/program.json new file mode 100644 index 0000000000..70f26d0033 --- /dev/null +++ b/temp/build/program.json @@ -0,0 +1,6 @@ +{ + "program": "temp.aleo", + "version": "0.0.0", + "description": "", + "license": "MIT" +} diff --git a/temp/leo.lock b/temp/leo.lock new file mode 100644 index 0000000000..c4293b3b9f --- /dev/null +++ b/temp/leo.lock @@ -0,0 +1 @@ +package = [] diff --git a/temp/program.json b/temp/program.json new file mode 100644 index 0000000000..85f0c159ba --- /dev/null +++ b/temp/program.json @@ -0,0 +1,8 @@ +{ + "program": "temp.aleo", + "version": "0.1.0", + "description": "", + "license": "MIT", + "dependencies": null, + "dev_dependencies": null +} \ No newline at end of file diff --git a/temp/src/main.leo b/temp/src/main.leo new file mode 100644 index 0000000000..909b1e0838 --- /dev/null +++ b/temp/src/main.leo @@ -0,0 +1,7 @@ +// The 'temp' program. +program temp.aleo { + transition main(public a: u32, b: u32) -> u32 { + let c: u32 = a + b; + return c; + } +} diff --git a/temp/tests/test.leo b/temp/tests/test.leo new file mode 100644 index 0000000000..9fbebb7f59 --- /dev/null +++ b/temp/tests/test.leo @@ -0,0 +1,13 @@ +// A Leo test file. +// To learn more about testing your program, see the documentation at https://docs.leo-lang.org +import helloworld.aleo; + +program test.aleo { + @compiled_test + @interpreted_test + transition test_helloworld() {{ + let result: u32 = helloworld.aleo/main(1u32, 2u32); + assert_eq(result, 3u32); + }} +} + From 1623a77c22a505bff154bdefd0420bd3782605b8 Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:00:49 -0800 Subject: [PATCH 15/23] Delete temp, add to gitignore --- .gitignore | 3 ++- temp/.gitignore | 5 ----- temp/build/README.md | 13 ------------- temp/build/main.aleo | 7 ------- temp/build/program.json | 6 ------ temp/leo.lock | 1 - temp/program.json | 8 -------- temp/src/main.leo | 7 ------- temp/tests/test.leo | 13 ------------- 9 files changed, 2 insertions(+), 61 deletions(-) delete mode 100644 temp/.gitignore delete mode 100644 temp/build/README.md delete mode 100644 temp/build/main.aleo delete mode 100644 temp/build/program.json delete mode 100644 temp/leo.lock delete mode 100644 temp/program.json delete mode 100644 temp/src/main.leo delete mode 100644 temp/tests/test.leo 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/temp/.gitignore b/temp/.gitignore deleted file mode 100644 index f721f7f6f4..0000000000 --- a/temp/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.env -*.avm -*.prover -*.verifier -outputs/ diff --git a/temp/build/README.md b/temp/build/README.md deleted file mode 100644 index 4443cf700d..0000000000 --- a/temp/build/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# temp.aleo - -## Build Guide - -To compile this Aleo program, run: -```bash -snarkvm build -``` - -To execute this Aleo program, run: -```bash -snarkvm run hello -``` diff --git a/temp/build/main.aleo b/temp/build/main.aleo deleted file mode 100644 index b6fba11e20..0000000000 --- a/temp/build/main.aleo +++ /dev/null @@ -1,7 +0,0 @@ -program temp.aleo; - -function main: - input r0 as u32.public; - input r1 as u32.private; - add r0 r1 into r2; - output r2 as u32.private; diff --git a/temp/build/program.json b/temp/build/program.json deleted file mode 100644 index 70f26d0033..0000000000 --- a/temp/build/program.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "program": "temp.aleo", - "version": "0.0.0", - "description": "", - "license": "MIT" -} diff --git a/temp/leo.lock b/temp/leo.lock deleted file mode 100644 index c4293b3b9f..0000000000 --- a/temp/leo.lock +++ /dev/null @@ -1 +0,0 @@ -package = [] diff --git a/temp/program.json b/temp/program.json deleted file mode 100644 index 85f0c159ba..0000000000 --- a/temp/program.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "program": "temp.aleo", - "version": "0.1.0", - "description": "", - "license": "MIT", - "dependencies": null, - "dev_dependencies": null -} \ No newline at end of file diff --git a/temp/src/main.leo b/temp/src/main.leo deleted file mode 100644 index 909b1e0838..0000000000 --- a/temp/src/main.leo +++ /dev/null @@ -1,7 +0,0 @@ -// The 'temp' program. -program temp.aleo { - transition main(public a: u32, b: u32) -> u32 { - let c: u32 = a + b; - return c; - } -} diff --git a/temp/tests/test.leo b/temp/tests/test.leo deleted file mode 100644 index 9fbebb7f59..0000000000 --- a/temp/tests/test.leo +++ /dev/null @@ -1,13 +0,0 @@ -// A Leo test file. -// To learn more about testing your program, see the documentation at https://docs.leo-lang.org -import helloworld.aleo; - -program test.aleo { - @compiled_test - @interpreted_test - transition test_helloworld() {{ - let result: u32 = helloworld.aleo/main(1u32, 2u32); - assert_eq(result, 3u32); - }} -} - From 3152050ddb5c7e1d25ea0b206e1bf7799df6e17a Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:38:24 -0800 Subject: [PATCH 16/23] Simplify default test definition, add dev dependencies to retriever --- leo/cli/commands/build.rs | 1 + leo/package/src/tst/default.rs | 5 +---- utils/retriever/src/retriever/mod.rs | 7 ++++++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/leo/cli/commands/build.rs b/leo/cli/commands/build.rs index 6372afde0c..97c5f2cf67 100644 --- a/leo/cli/commands/build.rs +++ b/leo/cli/commands/build.rs @@ -193,6 +193,7 @@ fn handle_build(command: &LeoBuild, context: Context) -> Result<(main_sym.to_string(), &package_path, &handler, command.options.clone(), main_stubs.clone())?; } diff --git a/leo/package/src/tst/default.rs b/leo/package/src/tst/default.rs index 04230b1b87..d0f598b8a4 100644 --- a/leo/package/src/tst/default.rs +++ b/leo/package/src/tst/default.rs @@ -42,14 +42,11 @@ impl DefaultTestFile { fn template() -> String { r#"// A Leo test file. // To learn more about testing your program, see the documentation at https://docs.leo-lang.org -import helloworld.aleo; - program test.aleo { @compiled_test @interpreted_test transition test_helloworld() {{ - let result: u32 = helloworld.aleo/main(1u32, 2u32); - assert_eq(result, 3u32); + assert_eq(1u32 + 2u32, 3u32); }} } "# diff --git a/utils/retriever/src/retriever/mod.rs b/utils/retriever/src/retriever/mod.rs index 30eb1239f4..753def99e4 100644 --- a/utils/retriever/src/retriever/mod.rs +++ b/utils/retriever/src/retriever/mod.rs @@ -409,11 +409,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) } From c80f7486e987922a0f24a8a97c1ac7ed426f18f2 Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Sun, 5 Jan 2025 16:07:21 -0800 Subject: [PATCH 17/23] Support annotation bodies --- compiler/ast/src/functions/annotation.rs | 18 +++++++++- compiler/parser/src/parser/file.rs | 33 +++++++++++++++++-- .../passes/src/type_checking/check_program.rs | 2 +- compiler/span/src/symbol.rs | 3 +- errors/src/errors/parser/parser_errors.rs | 7 ++++ leo/package/src/tst/default.rs | 3 +- 6 files changed, 57 insertions(+), 9 deletions(-) diff --git a/compiler/ast/src/functions/annotation.rs b/compiler/ast/src/functions/annotation.rs index 2cb07b54f4..722f7f6ac4 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}\",")), + } + } + string + } + }; + write!(f, "@{}{}", self.identifier, data) } } diff --git a/compiler/parser/src/parser/file.rs b/compiler/parser/src/parser/file.rs index 7d34701bac..75914c5acc 100644 --- a/compiler/parser/src/parser/file.rs +++ b/compiler/parser/src/parser/file.rs @@ -317,10 +317,37 @@ 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::Eq) { + 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 diff --git a/compiler/passes/src/type_checking/check_program.rs b/compiler/passes/src/type_checking/check_program.rs index a916d0c318..141a3ea85b 100644 --- a/compiler/passes/src/type_checking/check_program.rs +++ b/compiler/passes/src/type_checking/check_program.rs @@ -237,7 +237,7 @@ 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. - let valid_annotations = [sym::should_fail, sym::compiled_test, sym::interpreted_test]; + let valid_annotations = [sym::should_fail, sym::test]; for annotation in function.annotations.iter() { // All Leo annotations currently apply only to test code. if !self.build_tests || !valid_annotations.contains(&annotation.identifier.name) { diff --git a/compiler/span/src/symbol.rs b/compiler/span/src/symbol.rs index 4fa63735db..aa25f7c9c3 100644 --- a/compiler/span/src/symbol.rs +++ b/compiler/span/src/symbol.rs @@ -238,8 +238,7 @@ symbols! { // annotations should_fail, - compiled_test, - interpreted_test, + test, // general keywords As: "as", 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/leo/package/src/tst/default.rs b/leo/package/src/tst/default.rs index d0f598b8a4..c228cc0c64 100644 --- a/leo/package/src/tst/default.rs +++ b/leo/package/src/tst/default.rs @@ -43,8 +43,7 @@ impl DefaultTestFile { r#"// A Leo test file. // To learn more about testing your program, see the documentation at https://docs.leo-lang.org program test.aleo { - @compiled_test - @interpreted_test + @test transition test_helloworld() {{ assert_eq(1u32 + 2u32, 3u32); }} From a43458eb74b24612f0357dccb2dc07d9a639bf81 Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Sun, 5 Jan 2025 18:50:35 -0800 Subject: [PATCH 18/23] Improve validation for test annotations --- compiler/ast/src/functions/annotation.rs | 2 +- compiler/parser/src/parser/file.rs | 2 +- .../passes/src/type_checking/check_program.rs | 7 +- compiler/passes/src/type_checking/checker.rs | 75 ++++++++++++++++++- .../errors/type_checker/type_checker_error.rs | 3 +- .../type_checker/type_checker_warning.rs | 35 +++++++++ .../compiler/function/annotated_function.out | 13 ++++ .../function/annotated_function_fail.out | 49 +++++++----- .../compiler/function/annotated_function.leo | 20 +++++ .../function/annotated_function_fail.leo | 14 +++- 10 files changed, 188 insertions(+), 32 deletions(-) create mode 100644 tests/expectations/compiler/function/annotated_function.out create mode 100644 tests/tests/compiler/function/annotated_function.leo diff --git a/compiler/ast/src/functions/annotation.rs b/compiler/ast/src/functions/annotation.rs index 722f7f6ac4..13819aac35 100644 --- a/compiler/ast/src/functions/annotation.rs +++ b/compiler/ast/src/functions/annotation.rs @@ -50,7 +50,7 @@ impl fmt::Display for Annotation { Some(value) => string.push_str(&format!("{key} = \"{value}\",")), } } - string + format!("({string})") } }; write!(f, "@{}{}", self.identifier, data) diff --git a/compiler/parser/src/parser/file.rs b/compiler/parser/src/parser/file.rs index 75914c5acc..7f49a565f9 100644 --- a/compiler/parser/src/parser/file.rs +++ b/compiler/parser/src/parser/file.rs @@ -328,7 +328,7 @@ impl ParserContext<'_, N> { Token::LeftParen => { let (data, _, span) = self.parse_paren_comma_list(|p| { let key = p.expect_identifier()?; - let value = if p.eat(&Token::Eq) { + let value = if p.eat(&Token::Assign) { match &p.token.token { Token::StaticString(s) => { let value = s.clone(); diff --git a/compiler/passes/src/type_checking/check_program.rs b/compiler/passes/src/type_checking/check_program.rs index 141a3ea85b..f9d2283b65 100644 --- a/compiler/passes/src/type_checking/check_program.rs +++ b/compiler/passes/src/type_checking/check_program.rs @@ -237,13 +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. - let valid_annotations = [sym::should_fail, sym::test]; for annotation in function.annotations.iter() { - // All Leo annotations currently apply only to test code. - if !self.build_tests || !valid_annotations.contains(&annotation.identifier.name) { - // TODO: Change to compiler warning. - self.emit_err(TypeCheckerError::unknown_annotation(annotation, annotation.span)); - } + self.check_annotation(annotation); } // `interpret` can only be used for tests. diff --git a/compiler/passes/src/type_checking/checker.rs b/compiler/passes/src/type_checking/checker.rs index ebf51a4de7..fdf05ba30f 100644 --- a/compiler/passes/src/type_checking/checker.rs +++ b/compiler/passes/src/type_checking/checker.rs @@ -26,13 +26,14 @@ 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; use indexmap::{IndexMap, IndexSet}; use itertools::Itertools; -use std::{cell::RefCell, marker::PhantomData}; +use snarkvm::prelude::PrivateKey; +use std::{cell::RefCell, marker::PhantomData, str::FromStr}; pub struct TypeChecker<'a, N: Network> { /// The symbol table for the program. @@ -1364,6 +1365,76 @@ impl<'a, N: Network> TypeChecker<'a, N> { 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) { + match annotation.identifier.name { + sym::test => { + // Check the annotation body. + for (key, value) in 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.emit_warning(TypeCheckerWarning::missing_annotation_value( + annotation.identifier, + key, + key.span, + )), + Some(string) => { + if let Err(err) = PrivateKey::::from_str(&string) { + self.emit_warning(TypeCheckerWarning::invalid_annotation_value( + annotation.identifier, + key, + string, + err, + key.span, + )); + } + } + } + } else if key.name == Symbol::intern("seed") || key.name == Symbol::intern("batch") { + // Attempt to parse the value as a u64. + match value { + None => self.emit_warning(TypeCheckerWarning::missing_annotation_value( + annotation.identifier, + key, + key.span, + )), + Some(string) => { + if let Err(err) = string.parse::() { + self.emit_warning(TypeCheckerWarning::invalid_annotation_value( + 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.emit_warning(TypeCheckerWarning::unexpected_annotation_value( + annotation.identifier, + key, + string, + key.span, + )); + } + } else { + self.emit_warning(TypeCheckerWarning::unknown_annotation_key( + annotation.identifier, + key, + key.span, + )) + } + } + } + _ => self.emit_warning(TypeCheckerWarning::unknown_annotation(annotation.identifier, annotation.span)), + } + } } fn types_to_string(types: &[Type]) -> String { diff --git a/errors/src/errors/type_checker/type_checker_error.rs b/errors/src/errors/type_checker/type_checker_error.rs index 17dd923cd8..8093672711 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), @@ -419,7 +419,6 @@ create_messages!( } // TODO: Consider changing this to a warning. - @formatted assign_unit_expression_to_variable { args: (), diff --git a/errors/src/errors/type_checker/type_checker_warning.rs b/errors/src/errors/type_checker/type_checker_warning.rs index 1ac294cba3..e33587cd74 100644 --- a/errors/src/errors/type_checker/type_checker_warning.rs +++ b/errors/src/errors/type_checker/type_checker_warning.rs @@ -52,4 +52,39 @@ 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, + } + + @formatted + unknown_annotation_key { + args: (annotation: impl Display, key: impl Display), + msg: format!("Unknown key `{key}` in annotation `{annotation}`."), + help: None, + } + + @formatted + missing_annotation_value { + args: (annotation: impl Display, key: impl Display), + msg: format!("Missing value for key `{key}` in 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 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 annotation `{annotation}`."), + help: None, + } ); 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/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); + } +} From f1e37d3d03aa37850a8d65f73bcc9ce72626edf7 Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Fri, 10 Jan 2025 09:32:50 -0800 Subject: [PATCH 19/23] Introduce test manifest generation pass, improve test build --- compiler/compiler/src/compiler.rs | 18 ++- compiler/passes/src/lib.rs | 3 + .../src/test_manifest_generation/generator.rs | 148 ++++++++++++++++++ .../src/test_manifest_generation/manifest.rs | 50 ++++++ .../src/test_manifest_generation/mod.rs | 46 ++++++ compiler/passes/src/type_checking/checker.rs | 71 +-------- errors/src/errors/mod.rs | 9 ++ errors/src/errors/test/mod.rs | 19 +++ errors/src/errors/test/test_error.rs | 67 ++++++++ .../type_checker/type_checker_warning.rs | 28 ---- leo/cli/commands/build.rs | 42 +++-- 11 files changed, 387 insertions(+), 114 deletions(-) create mode 100644 compiler/passes/src/test_manifest_generation/generator.rs create mode 100644 compiler/passes/src/test_manifest_generation/manifest.rs create mode 100644 compiler/passes/src/test_manifest_generation/mod.rs create mode 100644 errors/src/errors/test/mod.rs create mode 100644 errors/src/errors/test/test_error.rs diff --git a/compiler/compiler/src/compiler.rs b/compiler/compiler/src/compiler.rs index beb027e995..a9acfb43bd 100644 --- a/compiler/compiler/src/compiler.rs +++ b/compiler/compiler/src/compiler.rs @@ -323,6 +323,12 @@ impl<'a, N: Network> Compiler<'a, N> { Ok((symbol_table, 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 loop unrolling pass. pub fn test_loop_unrolling_pass(&mut self, symbol_table: SymbolTable) -> Result { let (ast, symbol_table) = Unroller::do_pass(( @@ -437,10 +443,12 @@ impl<'a, N: Network> Compiler<'a, N> { } /// Runs the test compiler stages. - pub fn test_compiler_stages(&mut self) -> Result<(SymbolTable, StructGraph, CallGraph)> { + pub fn test_compiler_stages(&mut self) -> Result<(SymbolTable, StructGraph, CallGraph, TestManifest)> { let st = self.test_symbol_table_pass()?; let (st, struct_graph, call_graph) = self.test_type_checker_pass(st)?; + let test_manifest = self.test_manifest_pass()?; + let st = self.test_loop_unrolling_pass(st)?; self.test_static_single_assignment_pass(&st)?; @@ -453,7 +461,7 @@ impl<'a, N: Network> Compiler<'a, N> { self.test_dead_code_elimination_pass()?; - Ok((st, struct_graph, call_graph)) + Ok((st, struct_graph, call_graph, test_manifest)) } /// Returns a compiled Leo program. @@ -470,16 +478,16 @@ impl<'a, N: Network> Compiler<'a, N> { } /// Returns the compiled Leo tests. - pub fn compile_tests(&mut self) -> Result { + 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) = self.test_compiler_stages()?; + let (symbol_table, struct_graph, call_graph, test_manifest) = self.test_compiler_stages()?; // Run code generation. let bytecode = self.test_code_generation_pass(&symbol_table, &struct_graph, &call_graph)?; - Ok(bytecode) + Ok((bytecode, test_manifest)) } /// Writes the AST to a JSON file. 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..6603502095 --- /dev/null +++ b/compiler/passes/src/test_manifest_generation/generator.rs @@ -0,0 +1,148 @@ +// 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}; +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 { program_id, tests: Vec::new() }); + } +} + +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 { 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/manifest.rs b/compiler/passes/src/test_manifest_generation/manifest.rs new file mode 100644 index 0000000000..279ff67e68 --- /dev/null +++ b/compiler/passes/src/test_manifest_generation/manifest.rs @@ -0,0 +1,50 @@ +// 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 leo_ast::ProgramId; +use snarkvm::prelude::PrivateKey; + +/// A manifest describing the tests to be run and their associated metadata. +pub struct TestManifest { + /// The program ID. + pub program_id: ProgramId, + /// 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, 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. +pub struct TestMetadata { + /// 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/passes/src/test_manifest_generation/mod.rs b/compiler/passes/src/test_manifest_generation/mod.rs new file mode 100644 index 0000000000..ec1c195745 --- /dev/null +++ b/compiler/passes/src/test_manifest_generation/mod.rs @@ -0,0 +1,46 @@ +// 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; + +mod manifest; +pub use manifest::*; + +use crate::Pass; + +use leo_ast::{Ast, ProgramVisitor}; +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/checker.rs b/compiler/passes/src/type_checking/checker.rs index e8271f2cd0..93683c44c3 100644 --- a/compiler/passes/src/type_checking/checker.rs +++ b/compiler/passes/src/type_checking/checker.rs @@ -28,11 +28,11 @@ use leo_ast::*; use leo_errors::{TypeCheckerError, TypeCheckerWarning, emitter::Handler}; use leo_span::{Span, Symbol, sym}; -use snarkvm::{console::network::Network, prelude::PrivateKey}; +use snarkvm::console::network::Network; use indexmap::{IndexMap, IndexSet}; use itertools::Itertools; -use std::{cell::RefCell, marker::PhantomData, ops::Deref, str::FromStr}; +use std::{cell::RefCell, marker::PhantomData, ops::Deref}; pub struct TypeChecker<'a, N: Network> { /// The symbol table for the program. @@ -1217,71 +1217,8 @@ impl<'a, N: Network> TypeChecker<'a, N> { // Check if the annotation is valid. pub(crate) fn check_annotation(&mut self, annotation: &Annotation) { - match annotation.identifier.name { - sym::test => { - // Check the annotation body. - for (key, value) in 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.emit_warning(TypeCheckerWarning::missing_annotation_value( - annotation.identifier, - key, - key.span, - )), - Some(string) => { - if let Err(err) = PrivateKey::::from_str(string) { - self.emit_warning(TypeCheckerWarning::invalid_annotation_value( - annotation.identifier, - key, - string, - err, - key.span, - )); - } - } - } - } else if key.name == Symbol::intern("seed") || key.name == Symbol::intern("batch") { - // Attempt to parse the value as a u64. - match value { - None => self.emit_warning(TypeCheckerWarning::missing_annotation_value( - annotation.identifier, - key, - key.span, - )), - Some(string) => { - if let Err(err) = string.parse::() { - self.emit_warning(TypeCheckerWarning::invalid_annotation_value( - 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.emit_warning(TypeCheckerWarning::unexpected_annotation_value( - annotation.identifier, - key, - string, - key.span, - )); - } - } else { - self.emit_warning(TypeCheckerWarning::unknown_annotation_key( - annotation.identifier, - key, - key.span, - )) - } - } - } - _ => self.emit_warning(TypeCheckerWarning::unknown_annotation(annotation.identifier, annotation.span)), + if annotation.identifier.name != sym::test { + self.emit_warning(TypeCheckerWarning::unknown_annotation(annotation.identifier, annotation.span)); } } } 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/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..1c24cc3077 --- /dev/null +++ b/errors/src/errors/test/test_error.rs @@ -0,0 +1,67 @@ +// 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, + } +); diff --git a/errors/src/errors/type_checker/type_checker_warning.rs b/errors/src/errors/type_checker/type_checker_warning.rs index e33587cd74..288ea94ed1 100644 --- a/errors/src/errors/type_checker/type_checker_warning.rs +++ b/errors/src/errors/type_checker/type_checker_warning.rs @@ -59,32 +59,4 @@ create_messages!( msg: format!("Unknown annotation: `{annotation}`."), help: None, } - - @formatted - unknown_annotation_key { - args: (annotation: impl Display, key: impl Display), - msg: format!("Unknown key `{key}` in annotation `{annotation}`."), - help: None, - } - - @formatted - missing_annotation_value { - args: (annotation: impl Display, key: impl Display), - msg: format!("Missing value for key `{key}` in 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 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 annotation `{annotation}`."), - help: None, - } ); diff --git a/leo/cli/commands/build.rs b/leo/cli/commands/build.rs index cb856feb61..4b86133f13 100644 --- a/leo/cli/commands/build.rs +++ b/leo/cli/commands/build.rs @@ -31,7 +31,7 @@ use snarkvm::{ use indexmap::IndexMap; use leo_package::tst::TestDirectory; use leo_span::source_map::FileName; -use snarkvm::prelude::CanaryV0; +use snarkvm::prelude::{CanaryV0, Program}; use std::{ io::Write, path::{Path, PathBuf}, @@ -252,6 +252,12 @@ fn compile_tests( // 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(|e| CliError::general_cli_error(format!("Failed to create `build/tests` directory: {e}")))?; + // Construct the compiler. let mut compiler = Compiler::::new( "tests".to_string(), @@ -263,6 +269,9 @@ fn compile_tests( ); // 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(|e| { @@ -276,19 +285,24 @@ fn compile_tests( compiler.reset(vec![(FileName::Real(file_path.clone()), file_content)]); // Compile the test file. - let output = compiler.compile_tests()?; - - // Create a subdirectory for the test. - let build_dir = BuildDirectory::open(package_path)?; - let test_dir = build_dir.join("tests"); - std::fs::create_dir_all(&test_dir) - .map_err(|e| CliError::general_cli_error(format!("Failed to create `build/tests` directory: {e}")))?; - - // Write the outputs. - let test_file_name = file_path.file_name().unwrap().to_str().unwrap(); - let test_file_path = test_dir.join(test_file_name); - std::fs::write(&test_file_path, output).map_err(|e| { - CliError::general_cli_error(format!("Failed to write test file '{:?}': {e}", test_file_path)) + 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 program_id = ProgramID::::from_str(&test_package_name)?; + 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(|e| { + CliError::general_cli_error(format!("Failed to write test file '{:?}': {e}", main_file_path)) })?; } tracing::info!("✅ Compiled tests for '{name}'"); From 42dc09a53318e2c71902bf1c342530487ddeee54 Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Sat, 18 Jan 2025 19:26:36 -0800 Subject: [PATCH 20/23] WIP test driver --- Cargo.lock | 1 + Cargo.toml | 3 + compiler/ast/src/lib.rs | 3 + .../src/test}/manifest.rs | 18 +- compiler/ast/src/test/mod.rs | 18 ++ compiler/compiler/src/compiler.rs | 159 ++---------------- .../src/test_manifest_generation/generator.rs | 15 +- .../src/test_manifest_generation/mod.rs | 5 +- compiler/passes/src/type_checking/checker.rs | 5 +- compiler/passes/src/type_checking/mod.rs | 6 +- errors/src/errors/test/test_error.rs | 7 + leo/cli/commands/build.rs | 9 +- leo/cli/commands/test.rs | 102 ++++++++++- 13 files changed, 176 insertions(+), 175 deletions(-) rename compiler/{passes/src/test_manifest_generation => ast/src/test}/manifest.rs (76%) create mode 100644 compiler/ast/src/test/mod.rs diff --git a/Cargo.lock b/Cargo.lock index fb6f362ebc..366b7f3b31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1709,6 +1709,7 @@ dependencies = [ "num_cpus", "rand", "rand_chacha", + "rayon", "rpassword", "rusty-hook", "self_update 0.41.0", diff --git a/Cargo.toml b/Cargo.toml index ff84d6f311..f88982caaf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -235,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/lib.rs b/compiler/ast/src/lib.rs index 1a42274a76..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::*; diff --git a/compiler/passes/src/test_manifest_generation/manifest.rs b/compiler/ast/src/test/manifest.rs similarity index 76% rename from compiler/passes/src/test_manifest_generation/manifest.rs rename to compiler/ast/src/test/manifest.rs index 279ff67e68..ba3a303459 100644 --- a/compiler/passes/src/test_manifest_generation/manifest.rs +++ b/compiler/ast/src/test/manifest.rs @@ -14,23 +14,25 @@ // You should have received a copy of the GNU General Public License // along with the Leo library. If not, see . -use super::*; +use crate::ProgramId; +use snarkvm::prelude::{Network, PrivateKey}; -use leo_ast::ProgramId; -use snarkvm::prelude::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: ProgramId, + 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, tests: Vec::new() } + pub fn new(program_id: &ProgramId) -> Self { + Self { program_id: program_id.to_string(), tests: Vec::new() } } /// Add a test to the manifest. @@ -40,7 +42,11 @@ impl TestManifest { } /// 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. 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 a9acfb43bd..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::*; @@ -145,7 +145,7 @@ impl<'a, N: Network> Compiler<'a, N> { /// Runs the type checker pass. pub fn type_checker_pass(&'a self, symbol_table: SymbolTable) -> Result<(SymbolTable, StructGraph, CallGraph)> { let (symbol_table, struct_graph, call_graph) = - TypeChecker::::do_pass((&self.ast, self.handler, symbol_table, &self.type_table, false))?; + TypeChecker::::do_pass((&self.ast, self.handler, symbol_table, &self.type_table))?; if self.compiler_options.output.type_checked_symbol_table { self.write_symbol_table_to_json("type_checked_symbol_table.json", &symbol_table)?; } @@ -301,165 +301,30 @@ impl<'a, N: Network> Compiler<'a, N> { Ok((st, struct_graph, call_graph)) } - /// Runs the test symbol table pass. - pub fn test_symbol_table_pass(&self) -> Result { - let symbol_table = SymbolTableCreator::do_pass((&self.ast, self.handler))?; - if self.compiler_options.output.initial_symbol_table { - self.write_symbol_table_to_json("initial_symbol_table.json", &symbol_table)?; - } - Ok(symbol_table) - } - - /// Runs the test type checker pass. - pub fn test_type_checker_pass( - &'a self, - symbol_table: SymbolTable, - ) -> Result<(SymbolTable, StructGraph, CallGraph)> { - let (symbol_table, struct_graph, call_graph) = - TypeChecker::::do_pass((&self.ast, self.handler, symbol_table, &self.type_table, true))?; - if self.compiler_options.output.type_checked_symbol_table { - self.write_symbol_table_to_json("type_checked_symbol_table.json", &symbol_table)?; - } - Ok((symbol_table, struct_graph, call_graph)) - } - /// Generates the test manifest. pub fn test_manifest_pass(&self) -> Result> { - let manifest = TestManifestGenerator::do_pass((&self.ast, &self.handler))?; + let manifest = TestManifestGenerator::do_pass((&self.ast, self.handler))?; Ok(manifest) } - /// Runs the test loop unrolling pass. - pub fn test_loop_unrolling_pass(&mut self, symbol_table: SymbolTable) -> Result { - let (ast, symbol_table) = Unroller::do_pass(( - std::mem::take(&mut self.ast), - self.handler, - &self.node_builder, - symbol_table, - &self.type_table, - ))?; - self.ast = ast; - - if self.compiler_options.output.unrolled_ast { - self.write_ast_to_json("unrolled_ast.json")?; - } - - if self.compiler_options.output.unrolled_symbol_table { - self.write_symbol_table_to_json("unrolled_symbol_table.json", &symbol_table)?; - } - - Ok(symbol_table) - } - - /// Runs the test static single assignment pass. - pub fn test_static_single_assignment_pass(&mut self, symbol_table: &SymbolTable) -> Result<()> { - self.ast = StaticSingleAssigner::do_pass(( - std::mem::take(&mut self.ast), - &self.node_builder, - &self.assigner, - symbol_table, - &self.type_table, - ))?; - - if self.compiler_options.output.ssa_ast { - self.write_ast_to_json("ssa_ast.json")?; - } - - Ok(()) - } - - /// Runs the test flattening pass. - pub fn test_flattening_pass(&mut self, symbol_table: &SymbolTable) -> Result<()> { - self.ast = Flattener::do_pass(( - std::mem::take(&mut self.ast), - symbol_table, - &self.type_table, - &self.node_builder, - &self.assigner, - ))?; - - if self.compiler_options.output.flattened_ast { - self.write_ast_to_json("flattened_ast.json")?; - } - - Ok(()) - } - - /// Runs the test destructuring pass. - pub fn test_destructuring_pass(&mut self) -> Result<()> { - self.ast = Destructurer::do_pass(( - std::mem::take(&mut self.ast), - &self.type_table, - &self.node_builder, - &self.assigner, - ))?; - - if self.compiler_options.output.destructured_ast { - self.write_ast_to_json("destructured_ast.json")?; - } - - Ok(()) - } - - /// Runs the test function inlining pass. - pub fn test_function_inlining_pass(&mut self, call_graph: &CallGraph) -> Result<()> { - let ast = FunctionInliner::do_pass(( - std::mem::take(&mut self.ast), - &self.node_builder, - call_graph, - &self.assigner, - &self.type_table, - ))?; - self.ast = ast; - - if self.compiler_options.output.inlined_ast { - self.write_ast_to_json("inlined_ast.json")?; - } - - Ok(()) - } - - /// Runs the test dead code elimination pass. - pub fn test_dead_code_elimination_pass(&mut self) -> Result<()> { - if self.compiler_options.build.dce_enabled { - self.ast = DeadCodeEliminator::do_pass((std::mem::take(&mut self.ast), &self.node_builder))?; - } - - if self.compiler_options.output.dce_ast { - self.write_ast_to_json("dce_ast.json")?; - } - - Ok(()) - } - - /// Runs the test code generation pass. - pub fn test_code_generation_pass( - &mut self, - symbol_table: &SymbolTable, - struct_graph: &StructGraph, - call_graph: &CallGraph, - ) -> Result { - CodeGenerator::do_pass((&self.ast, symbol_table, &self.type_table, struct_graph, call_graph, &self.ast.ast)) - } - /// Runs the test compiler stages. pub fn test_compiler_stages(&mut self) -> Result<(SymbolTable, StructGraph, CallGraph, TestManifest)> { - let st = self.test_symbol_table_pass()?; - let (st, struct_graph, call_graph) = self.test_type_checker_pass(st)?; + 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.test_loop_unrolling_pass(st)?; + let st = self.loop_unrolling_pass(st)?; - self.test_static_single_assignment_pass(&st)?; + self.static_single_assignment_pass(&st)?; - self.test_flattening_pass(&st)?; + self.flattening_pass(&st)?; - self.test_destructuring_pass()?; + self.destructuring_pass()?; - self.test_function_inlining_pass(&call_graph)?; + self.function_inlining_pass(&call_graph)?; - self.test_dead_code_elimination_pass()?; + self.dead_code_elimination_pass()?; Ok((st, struct_graph, call_graph, test_manifest)) } @@ -486,7 +351,7 @@ impl<'a, N: Network> Compiler<'a, N> { // Run the intermediate compiler stages. let (symbol_table, struct_graph, call_graph, test_manifest) = self.test_compiler_stages()?; // Run code generation. - let bytecode = self.test_code_generation_pass(&symbol_table, &struct_graph, &call_graph)?; + let bytecode = self.code_generation_pass(&symbol_table, &struct_graph, &call_graph)?; Ok((bytecode, test_manifest)) } diff --git a/compiler/passes/src/test_manifest_generation/generator.rs b/compiler/passes/src/test_manifest_generation/generator.rs index 6603502095..13b76378a3 100644 --- a/compiler/passes/src/test_manifest_generation/generator.rs +++ b/compiler/passes/src/test_manifest_generation/generator.rs @@ -18,7 +18,7 @@ use super::*; use snarkvm::prelude::{Itertools, PrivateKey}; use std::str::FromStr; -use leo_ast::{ExpressionVisitor, Function, ProgramId, ProgramScope, StatementVisitor}; +use leo_ast::{ExpressionVisitor, Function, ProgramId, ProgramScope, StatementVisitor, TestManifest, TestMetadata}; use leo_errors::TestError; use leo_span::{Symbol, sym}; @@ -36,15 +36,15 @@ impl<'a, N: Network> TestManifestGenerator<'a, N> { } /// Initialize the manifest. - pub fn initialize_manifest(&mut self, program_id: ProgramId) { - self.manifest = Some(TestManifest { program_id, tests: Vec::new() }); + 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); + self.initialize_manifest(&input.program_id); // Visit the functions in the program scope. input.functions.iter().for_each(|(_, c)| (self.visit_function(c))); } @@ -136,7 +136,12 @@ impl<'a, N: Network> ProgramVisitor<'a> for TestManifestGenerator<'a, N> { } // Add the test to the manifest. - self.manifest.as_mut().unwrap().add_test(TestMetadata { private_key, seed, should_fail }); + self.manifest.as_mut().unwrap().add_test(TestMetadata { + function_name: input.identifier.to_string(), + private_key, + seed, + should_fail, + }); } } diff --git a/compiler/passes/src/test_manifest_generation/mod.rs b/compiler/passes/src/test_manifest_generation/mod.rs index ec1c195745..d6133ee78f 100644 --- a/compiler/passes/src/test_manifest_generation/mod.rs +++ b/compiler/passes/src/test_manifest_generation/mod.rs @@ -17,12 +17,9 @@ mod generator; pub use generator::TestManifestGenerator; -mod manifest; -pub use manifest::*; - use crate::Pass; -use leo_ast::{Ast, ProgramVisitor}; +use leo_ast::{Ast, ProgramVisitor, TestManifest}; use leo_errors::{Result, emitter::Handler}; use snarkvm::prelude::Network; diff --git a/compiler/passes/src/type_checking/checker.rs b/compiler/passes/src/type_checking/checker.rs index 93683c44c3..147992db6c 100644 --- a/compiler/passes/src/type_checking/checker.rs +++ b/compiler/passes/src/type_checking/checker.rs @@ -53,15 +53,13 @@ pub struct TypeChecker<'a, N: Network> { pub(crate) async_function_callers: IndexMap>, /// The set of used composites. pub(crate) used_structs: IndexSet, - /// Are we compiling tests? - pub(crate) build_tests: bool, // Allows the type checker to be generic over the network. phantom: PhantomData, } impl<'a, N: Network> TypeChecker<'a, N> { /// Returns a new type checker given a symbol table and error handler. - pub fn new(symbol_table: SymbolTable, type_table: &'a TypeTable, handler: &'a Handler, is_test: bool) -> Self { + pub fn new(symbol_table: SymbolTable, type_table: &'a TypeTable, handler: &'a Handler) -> Self { let struct_names = symbol_table.structs.keys().map(|loc| loc.name).collect(); let function_names = symbol_table.functions.keys().map(|loc| loc.name).collect(); @@ -76,7 +74,6 @@ impl<'a, N: Network> TypeChecker<'a, N> { async_function_input_types: IndexMap::new(), async_function_callers: IndexMap::new(), used_structs: IndexSet::new(), - build_tests: is_test, phantom: Default::default(), } } diff --git a/compiler/passes/src/type_checking/mod.rs b/compiler/passes/src/type_checking/mod.rs index abeff42142..88991c5091 100644 --- a/compiler/passes/src/type_checking/mod.rs +++ b/compiler/passes/src/type_checking/mod.rs @@ -34,11 +34,11 @@ use leo_errors::{Result, emitter::Handler}; use snarkvm::prelude::Network; impl<'a, N: Network> Pass for TypeChecker<'a, N> { - type Input = (&'a Ast, &'a Handler, SymbolTable, &'a TypeTable, bool); + type Input = (&'a Ast, &'a Handler, SymbolTable, &'a TypeTable); type Output = Result<(SymbolTable, StructGraph, CallGraph)>; - fn do_pass((ast, handler, st, tt, is_test): Self::Input) -> Self::Output { - let mut visitor = TypeChecker::::new(st, tt, handler, is_test); + fn do_pass((ast, handler, st, tt): Self::Input) -> Self::Output { + let mut visitor = TypeChecker::::new(st, tt, handler); visitor.visit_program(ast.as_repr()); handler.last_err().map_err(|e| *e)?; diff --git a/errors/src/errors/test/test_error.rs b/errors/src/errors/test/test_error.rs index 1c24cc3077..56d2513716 100644 --- a/errors/src/errors/test/test_error.rs +++ b/errors/src/errors/test/test_error.rs @@ -64,4 +64,11 @@ create_messages!( msg: format!("A test annotation is only allowed on transition functions."), help: None, } + + @backtraced + test_error { + args: (error: impl Display), + msg: format!("Test Error: {error}"), + help: None, + } ); diff --git a/leo/cli/commands/build.rs b/leo/cli/commands/build.rs index 4b86133f13..d4bec75509 100644 --- a/leo/cli/commands/build.rs +++ b/leo/cli/commands/build.rs @@ -293,7 +293,6 @@ fn compile_tests( // Initialize the test package path. let test_package_name = program_id.name().to_string(); - let program_id = ProgramID::::from_str(&test_package_name)?; let test_package_path = test_dir.join(test_package_name); // Initialize a new package. @@ -304,6 +303,14 @@ fn compile_tests( std::fs::write(&main_file_path, output).map_err(|e| { CliError::general_cli_error(format!("Failed to write test file '{:?}': {e}", main_file_path)) })?; + + // 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(|e| CliError::general_cli_error(format!("Failed to serialize test manifest: {e}")))?; + std::fs::write(&manifest_file_path, manifest_json).map_err(|e| { + CliError::general_cli_error(format!("Failed to write test manifest '{:?}': {e}", manifest_file_path)) + })?; } tracing::info!("✅ Compiled tests for '{name}'"); Ok(()) diff --git a/leo/cli/commands/test.rs b/leo/cli/commands/test.rs index 5c78701c17..c7bd032437 100644 --- a/leo/cli/commands/test.rs +++ b/leo/cli/commands/test.rs @@ -16,16 +16,25 @@ use super::*; +use leo_ast::TestManifest; +use leo_errors::TestError; + +use snarkvm::prelude::{Itertools, Value}; + +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 = "TESTNAME", help = "If specified, only run tests containing this string in their names.")] - name: Option, + #[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, the maximum is the number of CPUs.")] + #[clap(short, long, help = "Number of parallel jobs, defaults to the number of CPUs.")] jobs: Option, #[clap(long, help = "Skip running the native tests.", default_value = "false")] skip_native: bool, @@ -78,9 +87,92 @@ impl Command for LeoTest { // 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. - // If the number exceeds the number of CPUs, it is clamped to the number of CPUs. let num_cpus = num_cpus::get(); - let jobs = command.jobs.unwrap_or(num_cpus).min(num_cpus); + let jobs = command.jobs.unwrap_or(num_cpus); + + // Initialize the Rayon thread pool. + ThreadPoolBuilder::new().num_threads(jobs).build_global().map_err(TestError::test_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::test_error)? + .into_iter() + .flat_map(|dir_entry| dir_entry.map(|dir_entry| dir_entry.path())) + .collect_vec(); + + println!("Running tests..."); + println!("Test directories: {:?}", test_directories); + + // For each test package within the tests directory: + // - Open the package as a snarkVM `Package`. + // - Open the manifest. + // - Initialize the VM. + // - Run each test within the manifest sequentially. + let errors: Vec<_> = test_directories + .into_par_iter() + .map(|path| -> Result<::Output> { + // Open the package as a snarkVM `Package`. + let package = snarkvm::package::Package::::open(&path).map_err(TestError::test_error)?; + + // Load the `manifest.json` within the test directory. + let manifest_path = path.join("manifest.json"); + let manifest_json = std::fs::read_to_string(&manifest_path).map_err(TestError::test_error)?; + let manifest: TestManifest = + serde_json::from_str(&manifest_json).map_err(TestError::test_error)?; + + // TODO (@d0cd). Change this to VM initialization. + // Initialize the process. + let process = package.get_process()?; + + // TODO (@d0cd). Handle this correctly. + // Initialize a vector to aggregate the results. + // let results = Vec::with_capacity(manifest.tests.len()); + + // Run each test within the manifest. + for test in manifest.tests { + // 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 => PrivateKey::new(rng)?, + }; + + // Determine whether or not the function should fail. + let should_fail = test.should_fail; + + // Execute the function. + let inputs: Vec> = Vec::new(); + let authorization = process.authorize::( + &private_key, + &manifest.program_id, + &test.function_name, + inputs.iter(), + rng, + )?; + let result = process.execute::(authorization, rng); + + // Construct the result. + // TODO (@d0cd) Pipe result output. + match result { + Ok(_) => println!("Test passed!"), + Err(e) => println!("Test failed: {e}"), + } + } + + Ok(()) + }) + .collect(); Ok(()) } From 93c06e76976ad777363f594bcc067f7e9d55ad2d Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Sun, 19 Jan 2025 17:39:19 -0800 Subject: [PATCH 21/23] Capture test results --- errors/src/errors/test/test_error.rs | 2 +- leo/cli/commands/{test.rs => test/mod.rs} | 101 +++++++++++++++------- leo/cli/commands/test/result.rs | 55 ++++++++++++ 3 files changed, 125 insertions(+), 33 deletions(-) rename leo/cli/commands/{test.rs => test/mod.rs} (62%) create mode 100644 leo/cli/commands/test/result.rs diff --git a/errors/src/errors/test/test_error.rs b/errors/src/errors/test/test_error.rs index 56d2513716..7ccd0c907a 100644 --- a/errors/src/errors/test/test_error.rs +++ b/errors/src/errors/test/test_error.rs @@ -66,7 +66,7 @@ create_messages!( } @backtraced - test_error { + default_error { args: (error: impl Display), msg: format!("Test Error: {error}"), help: None, diff --git a/leo/cli/commands/test.rs b/leo/cli/commands/test/mod.rs similarity index 62% rename from leo/cli/commands/test.rs rename to leo/cli/commands/test/mod.rs index c7bd032437..63a9862de8 100644 --- a/leo/cli/commands/test.rs +++ b/leo/cli/commands/test/mod.rs @@ -14,6 +14,9 @@ // You should have received a copy of the GNU General Public License // along with the Leo library. If not, see . +mod result; +use result::*; + use super::*; use leo_ast::TestManifest; @@ -36,14 +39,6 @@ pub struct LeoTest { no_fail_fast: bool, #[clap(short, long, help = "Number of parallel jobs, defaults to the number of CPUs.")] jobs: Option, - #[clap(long, help = "Skip running the native tests.", default_value = "false")] - skip_native: bool, - // TODO: The default should eventually be `false`. - #[clap(long, help = "Skip running the interpreted tests.", default_value = "true")] - skip_interpreted: bool, - // TODO: The default should eventually be `false`. - #[clap(long, help = "Skip running the end-to-end tests.", default_value = "true")] - skip_end_to_end: bool, #[clap(flatten)] compiler_options: BuildOptions, } @@ -91,48 +86,70 @@ fn handle_test(command: LeoTest, context: Context) -> Result</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::test_error)? - .into_iter() + .map_err(TestError::default_error)? .flat_map(|dir_entry| dir_entry.map(|dir_entry| dir_entry.path())) .collect_vec(); - println!("Running tests..."); - println!("Test directories: {:?}", test_directories); - // For each test package within the tests directory: // - Open the package as a snarkVM `Package`. // - Open the manifest. // - Initialize the VM. // - Run each test within the manifest sequentially. - let errors: Vec<_> = test_directories + let results: Vec<_> = test_directories .into_par_iter() - .map(|path| -> Result<::Output> { + .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"; // Open the package as a snarkVM `Package`. - let package = snarkvm::package::Package::::open(&path).map_err(TestError::test_error)?; + let package = match snarkvm::package::Package::::open(&path) { + Ok(package) => package, + Err(e) => return format!("Failed to open test package: {e}. {BUG_MESSAGE}"), + }; // Load the `manifest.json` within the test directory. let manifest_path = path.join("manifest.json"); - let manifest_json = std::fs::read_to_string(&manifest_path).map_err(TestError::test_error)?; - let manifest: TestManifest = - serde_json::from_str(&manifest_json).map_err(TestError::test_error)?; + 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}"), + }; // TODO (@d0cd). Change this to VM initialization. // Initialize the process. - let process = package.get_process()?; + let process = match package.get_process() { + Ok(process) => process, + Err(e) => return format!("Failed to get process: {e}. {BUG_MESSAGE}"), + }; - // TODO (@d0cd). Handle this correctly. - // Initialize a vector to aggregate the results. - // let results = Vec::with_capacity(manifest.tests.len()); + println!("3"); + + // 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, @@ -145,7 +162,14 @@ fn handle_test(command: LeoTest, context: Context) -> Result< private_key, - None => PrivateKey::new(rng)?, + 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. @@ -153,26 +177,39 @@ fn handle_test(command: LeoTest, context: Context) -> Result<> = Vec::new(); - let authorization = process.authorize::( + let authorization = match process.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 result = process.execute::(authorization, rng); // Construct the result. - // TODO (@d0cd) Pipe result output. - match result { - Ok(_) => println!("Test passed!"), - Err(e) => println!("Test failed: {e}"), + match (result, should_fail) { + (Ok(_), true) => results.add_result(test_name, "❌ Test should have failed".to_string()), + (Err(e), false) => results.add_result(test_name, format!("❌ Test failed: {e}")), + (Ok(_), false) => results.add_result(test_name, "✅ Test succeeded".to_string()), + (Err(e), true) => results.add_result(test_name, format!("✅ Test failed as expected: {e}")), } } - Ok(()) + // 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(()) + } +} From 848f27fa476823050d5a78478840afad4b4d5bfc Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Sun, 19 Jan 2025 19:34:38 -0800 Subject: [PATCH 22/23] Remove process and use VM to drive test execution --- leo/cli/commands/build.rs | 2 +- leo/cli/commands/test/mod.rs | 133 +++++++++++++++++++---- leo/cli/commands/test/utilities.rs | 167 +++++++++++++++++++++++++++++ 3 files changed, 280 insertions(+), 22 deletions(-) create mode 100644 leo/cli/commands/test/utilities.rs diff --git a/leo/cli/commands/build.rs b/leo/cli/commands/build.rs index d4bec75509..da890c346c 100644 --- a/leo/cli/commands/build.rs +++ b/leo/cli/commands/build.rs @@ -296,7 +296,7 @@ fn compile_tests( let test_package_path = test_dir.join(test_package_name); // Initialize a new package. - Package::create(&test_package_path, &program_id)?; + 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"); diff --git a/leo/cli/commands/test/mod.rs b/leo/cli/commands/test/mod.rs index 63a9862de8..5e8f5ee0df 100644 --- a/leo/cli/commands/test/mod.rs +++ b/leo/cli/commands/test/mod.rs @@ -17,12 +17,15 @@ mod result; use result::*; +mod utilities; +use utilities::*; + use super::*; use leo_ast::TestManifest; use leo_errors::TestError; -use snarkvm::prelude::{Itertools, Value}; +use snarkvm::prelude::{Itertools, Program, Value, execution_cost_v1, execution_cost_v2}; use rand::{Rng, SeedableRng, rngs::OsRng}; use rand_chacha::ChaChaRng; @@ -79,6 +82,7 @@ impl Command for LeoTest { } } +// TODO (@d0cd) The logic needs a lot of refactoring. // 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. @@ -98,7 +102,6 @@ fn handle_test(command: LeoTest, context: Context) -> Result<(command: LeoTest, context: Context) -> Result<::open(&path) { - Ok(package) => package, - Err(e) => return format!("Failed to open test package: {e}. {BUG_MESSAGE}"), - }; - // 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) { @@ -125,14 +122,39 @@ fn handle_test(command: LeoTest, context: Context) -> Result< return format!("Failed to parse manifest.json: {e}. {BUG_MESSAGE}"), }; - // TODO (@d0cd). Change this to VM initialization. - // Initialize the process. - let process = match package.get_process() { - Ok(process) => process, - Err(e) => return format!("Failed to get process: {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); + } - println!("3"); + // 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()); @@ -177,7 +199,7 @@ fn handle_test(command: LeoTest, context: Context) -> Result<> = Vec::new(); - let authorization = match process.authorize::( + let authorization = match vm.process().read().authorize::( &private_key, &manifest.program_id, &test.function_name, @@ -190,14 +212,83 @@ fn handle_test(command: LeoTest, context: Context) -> Result<(authorization, rng); + 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 (result, should_fail) { - (Ok(_), true) => results.add_result(test_name, "❌ Test should have failed".to_string()), - (Err(e), false) => results.add_result(test_name, format!("❌ Test failed: {e}")), - (Ok(_), false) => results.add_result(test_name, "✅ Test succeeded".to_string()), - (Err(e), true) => results.add_result(test_name, format!("✅ Test failed as expected: {e}")), + 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()), } } 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()) +} From b2234baf78ffcde6c16ee35dc0247e631af5fa4a Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Sat, 25 Jan 2025 21:45:06 -0800 Subject: [PATCH 23/23] Baseline framework is working --- leo/cli/commands/build.rs | 62 +++++++++++++++++++--------- leo/cli/commands/test/mod.rs | 11 ++--- utils/retriever/src/retriever/mod.rs | 8 ++++ 3 files changed, 57 insertions(+), 24 deletions(-) diff --git a/leo/cli/commands/build.rs b/leo/cli/commands/build.rs index da890c346c..cc8acff8a5 100644 --- a/leo/cli/commands/build.rs +++ b/leo/cli/commands/build.rs @@ -31,7 +31,7 @@ use snarkvm::{ use indexmap::IndexMap; use leo_package::tst::TestDirectory; use leo_span::source_map::FileName; -use snarkvm::prelude::{CanaryV0, Program}; +use snarkvm::prelude::{CanaryV0, Itertools, Program}; use std::{ io::Write, path::{Path, PathBuf}, @@ -193,7 +193,6 @@ fn handle_build(command: &LeoBuild, context: Context) -> Result<(main_sym.to_string(), &package_path, &handler, command.options.clone(), main_stubs.clone())?; } @@ -214,9 +213,8 @@ fn compile_leo_files( // 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(|e| { - CliError::general_cli_error(format!("Failed to read source file '{:?}': {e}", file_path.as_path())) - })?; // Read the file content. + 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)); } @@ -256,7 +254,29 @@ fn compile_tests( let build_dir = BuildDirectory::open(package_path)?; let test_dir = build_dir.join("tests"); std::fs::create_dir_all(&test_dir) - .map_err(|e| CliError::general_cli_error(format!("Failed to create `build/tests` directory: {e}")))?; + .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( @@ -274,12 +294,8 @@ fn compile_tests( // 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(|e| { - CliError::general_cli_error(format!( - "Failed to read test file '{:?}': {e}", - file_path.clone().into_os_string() - )) - })?; + 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)]); @@ -300,17 +316,25 @@ fn compile_tests( // 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(|e| { - CliError::general_cli_error(format!("Failed to write test file '{:?}': {e}", main_file_path)) - })?; + 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(|e| CliError::general_cli_error(format!("Failed to serialize test manifest: {e}")))?; - std::fs::write(&manifest_file_path, manifest_json).map_err(|e| { - CliError::general_cli_error(format!("Failed to write test manifest '{:?}': {e}", manifest_file_path)) - })?; + .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/test/mod.rs b/leo/cli/commands/test/mod.rs index 5e8f5ee0df..c15484a3ac 100644 --- a/leo/cli/commands/test/mod.rs +++ b/leo/cli/commands/test/mod.rs @@ -82,7 +82,6 @@ impl Command for LeoTest { } } -// TODO (@d0cd) The logic needs a lot of refactoring. // 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. @@ -101,6 +100,8 @@ fn handle_test(command: LeoTest, context: Context) -> Result<(command: LeoTest, context: Context) -> Result< results.add_result(test_name, "❌ Test passed but should have failed".to_string()), + (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()) + 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()), + (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()), } } diff --git a/utils/retriever/src/retriever/mod.rs b/utils/retriever/src/retriever/mod.rs index 753def99e4..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,