diff --git a/crates/analyzer/src/namespace/items.rs b/crates/analyzer/src/namespace/items.rs index f45e52cce1..7dc077a621 100644 --- a/crates/analyzer/src/namespace/items.rs +++ b/crates/analyzer/src/namespace/items.rs @@ -290,6 +290,7 @@ pub struct Ingot { // pub version: SmolStr, pub mode: IngotMode, pub src_dir: SmolStr, + pub test_file: SmolStr, } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)] @@ -337,6 +338,7 @@ impl IngotId { name: name.into(), mode, src_dir: file_path_prefix.as_str().into(), + test_file: file_path_prefix.parent().unwrap().join("tests.fe").into(), })); // Intern the source files @@ -676,6 +678,20 @@ impl ModuleId { .collect::>() } + /// All functions, including from submodules and including duplicates + pub fn all_functions(&self, db: &dyn AnalyzerDb) -> Vec { + self.items(db) + .iter() + .filter_map(|(_, item)| { + if let Item::Function(function) = item { + Some(*function) + } else { + None + } + }) + .collect() + } + /// Returns the map of ingot deps, built-ins, and the ingot itself as /// "ingot". pub fn global_items(&self, db: &dyn AnalyzerDb) -> IndexMap { diff --git a/crates/codegen/src/yul/isel/function.rs b/crates/codegen/src/yul/isel/function.rs index 51078bdc9c..a87d6db913 100644 --- a/crates/codegen/src/yul/isel/function.rs +++ b/crates/codegen/src/yul/isel/function.rs @@ -34,6 +34,7 @@ use crate::{ }, }; +// here pub fn lower_function( db: &dyn CodegenDb, ctx: &mut Context, diff --git a/crates/codegen/src/yul/isel/mod.rs b/crates/codegen/src/yul/isel/mod.rs index f210a28064..330ed22d0f 100644 --- a/crates/codegen/src/yul/isel/mod.rs +++ b/crates/codegen/src/yul/isel/mod.rs @@ -2,6 +2,8 @@ pub mod context; mod contract; mod function; mod inst_order; +mod test; pub use contract::{lower_contract, lower_contract_deployable}; +pub use test::lower_test; pub use function::lower_function; diff --git a/crates/codegen/src/yul/isel/test.rs b/crates/codegen/src/yul/isel/test.rs new file mode 100644 index 0000000000..424b6f9856 --- /dev/null +++ b/crates/codegen/src/yul/isel/test.rs @@ -0,0 +1,70 @@ +use super::context::Context; +use fe_analyzer::namespace::items::FunctionId; +use yultsur::{yul, *}; +use crate::{ + db::CodegenDb, +}; + +pub fn lower_test(db: &dyn CodegenDb, test: FunctionId) -> yul::Object { + let mut context = Context::default(); + let test = db.mir_lowered_func_signature(test); + context.function_dependency.insert(test); + + let dep_constants = context.resolve_constant_dependency(db); + let dep_functions: Vec<_> = context + .resolve_function_dependency(db) + .into_iter() + .map(yul::Statement::FunctionDefinition) + .collect(); + let runtime_funcs: Vec<_> = context + .runtime + .collect_definitions() + .into_iter() + .map(yul::Statement::FunctionDefinition) + .collect(); + let test_func_name = identifier! { (db.codegen_function_symbol_name(test)) }; + let call = expression! {[test_func_name]()}; + + let code = code! { + [dep_functions...] + [runtime_funcs...] + }; + + let name = identifier! { test }; + let object = yul::Object { + name, + code, + objects: vec![], + data: dep_constants, + }; + + normalize_object(object) + +} + +fn normalize_object(obj: yul::Object) -> yul::Object { + let data = obj + .data + .into_iter() + .map(|data| yul::Data { + name: data.name, + value: data + .value + .replace('\\', "\\\\\\\\") + .replace('\n', "\\\\n") + .replace('"', "\\\\\"") + .replace('\r', "\\\\r") + .replace('\t', "\\\\t"), + }) + .collect::>(); + yul::Object { + name: obj.name, + code: obj.code, + objects: obj + .objects + .into_iter() + .map(normalize_object) + .collect::>(), + data, + } +} diff --git a/crates/driver/src/lib.rs b/crates/driver/src/lib.rs index 77d6d9c186..26bead38b8 100644 --- a/crates/driver/src/lib.rs +++ b/crates/driver/src/lib.rs @@ -3,7 +3,7 @@ pub use fe_codegen::db::{CodegenDb, Db}; //use fe_codegen::yul::runtime::RuntimeProvider; -use fe_analyzer::namespace::items::{IngotId, IngotMode, ModuleId}; +use fe_analyzer::namespace::items::{IngotId, IngotMode, ModuleId, FunctionId}; use fe_analyzer::AnalyzerDb; use fe_analyzer::{context::Analysis, namespace::items::ContractId}; use fe_common::db::Upcast; @@ -22,6 +22,7 @@ pub struct CompiledModule { pub src_ast: String, pub lowered_ast: String, pub contracts: IndexMap, + pub tests: IndexMap, } /// The artifacts of a compiled contract. @@ -57,6 +58,24 @@ pub fn compile_single_file( } } +pub fn compile_test_file( + db: &mut Db, + path: &str, + src: &str, + optimize: bool, +) -> Result, CompileError> { + let module = ModuleId::new_standalone(db, path, src); + let diags = module.diagnostics(db); + + if diags.is_empty() { + let test = compile_test_to_yul(db, module.all_functions(db)[0]); + compile_to_evm("test", &test, optimize); + Err(CompileError(diags)) + } else { + Err(CompileError(diags)) + } +} + // Run analysis with ingot // Return vector error,waring... pub fn check_ingot( @@ -108,7 +127,8 @@ pub fn compile_ingot( let main_module = ingot .root_module(db) .expect("missing root module, with no diagnostic"); - compile_module_id(db, main_module, with_bytecode, optimize) + let mut module = compile_module_id(db, main_module, with_bytecode, optimize)?; + Ok(module) } /// Returns graphviz string. @@ -160,6 +180,7 @@ fn compile_module_id( src_ast: format!("{:#?}", module_id.ast(db)), lowered_ast: format!("{:#?}", module_id.ast(db)), contracts, + tests: IndexMap::new(), }) } @@ -197,6 +218,11 @@ fn compile_to_yul(db: &mut Db, contract: ContractId) -> String { yul_contract.to_string().replace('"', "\\\"") } +fn compile_test_to_yul(db: &mut Db, test: FunctionId) -> String { + let yul_test = fe_codegen::yul::isel::lower_test(db, test); + yul_test.to_string().replace('"', "\\\"") +} + #[cfg(feature = "solc-backend")] fn compile_to_evm(name: &str, yul_object: &str, optimize: bool) -> String { match fe_yulc::compile_single_contract(name, yul_object, optimize) { diff --git a/crates/fe/src/task/test.rs b/crates/fe/src/task/test.rs new file mode 100644 index 0000000000..f259cc9f64 --- /dev/null +++ b/crates/fe/src/task/test.rs @@ -0,0 +1,236 @@ +use std::fs; +use std::io::{Error, Write}; +use std::path::Path; + +use clap::{ArgEnum, Args}; +use fe_common::diagnostics::print_diagnostics; +use fe_common::files::SourceFileId; +use fe_driver::CompiledModule; + +const DEFAULT_OUTPUT_DIR_NAME: &str = "output"; +const DEFAULT_INGOT: &str = "main"; + +use super::utils::load_files_from_dir; + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ArgEnum, Debug)] +enum Emit { + Abi, + Ast, + LoweredAst, + Bytecode, + Tokens, + Yul, +} + +#[derive(Args)] +#[clap(about = "Build the current project")] +pub struct BuildArgs { + input_path: String, + #[clap(short, long, default_value = DEFAULT_OUTPUT_DIR_NAME)] + output_dir: String, + #[clap( + arg_enum, + use_value_delimiter = true, + long, + short, + default_value = "abi,bytecode" + )] + emit: Vec, + #[clap(long)] + mir: bool, + #[clap(long)] + overwrite: bool, + #[clap(long, takes_value(true))] + optimize: Option, +} + +fn build_ingot(compile_arg: &BuildArgs) -> (String, CompiledModule) { + let emit = &compile_arg.emit; + let with_bytecode = emit.contains(&Emit::Bytecode); + let input_path = &compile_arg.input_path; + let optimize = compile_arg.optimize.unwrap_or(true); + + if !Path::new(input_path).exists() { + eprintln!("Input directory does not exist: `{}`.", input_path); + std::process::exit(1) + } + + let files = match load_files_from_dir(input_path) { + Ok(files) if files.is_empty() => { + eprintln!("Input directory is not an ingot: `{}`", input_path); + std::process::exit(1) + } + Ok(files) => files, + Err(err) => { + eprintln!("Failed to load project files. Error: {}", err); + std::process::exit(1) + } + }; + + let mut db = fe_driver::Db::default(); + let compiled_module = match fe_driver::compile_ingot( + &mut db, + DEFAULT_INGOT, // TODO: real ingot name + &files, + with_bytecode, + optimize, + ) { + Ok(module) => module, + Err(error) => { + eprintln!("Unable to compile {}.", input_path); + print_diagnostics(&db, &error.0); + std::process::exit(1) + } + }; + + // no file content for ingots + ("".to_string(), compiled_module) +} + +pub fn build(compile_arg: BuildArgs) { + let emit = &compile_arg.emit; + + let input_path = &compile_arg.input_path; + + if compile_arg.mir { + return mir_dump(input_path); + } + + let _with_bytecode = emit.contains(&Emit::Bytecode); + #[cfg(not(feature = "solc-backend"))] + if _with_bytecode { + eprintln!("Warning: bytecode output requires 'solc-backend' feature. Try `cargo build --release --features solc-backend`. Skipping."); + } + + let (content, compiled_module) = if Path::new(input_path).is_file() { + build_single_file(&compile_arg) + } else { + build_ingot(&compile_arg) + }; + + let output_dir = &compile_arg.output_dir; + let overwrite = compile_arg.overwrite; + match write_compiled_module(compiled_module, &content, emit, output_dir, overwrite) { + Ok(_) => eprintln!("Compiled {}. Outputs in `{}`", input_path, output_dir), + Err(err) => { + eprintln!( + "Failed to write output to directory: `{}`. Error: {}", + output_dir, err + ); + std::process::exit(1) + } + } +} + +fn write_compiled_module( + mut module: CompiledModule, + file_content: &str, + targets: &[Emit], + output_dir: &str, + overwrite: bool, +) -> Result<(), String> { + let output_dir = Path::new(output_dir); + if output_dir.is_file() { + return Err(format!( + "A file exists at path `{}`, the location of the output directory. Refusing to overwrite.", + output_dir.display() + )); + } + + if !overwrite { + verify_nonexistent_or_empty(output_dir)?; + } + + fs::create_dir_all(output_dir).map_err(ioerr_to_string)?; + + if targets.contains(&Emit::Ast) { + write_output(&output_dir.join("module.ast"), &module.src_ast)?; + } + + if targets.contains(&Emit::LoweredAst) { + write_output(&output_dir.join("lowered_module.ast"), &module.lowered_ast)?; + } + + if targets.contains(&Emit::Tokens) { + let tokens = { + let lexer = fe_parser::lexer::Lexer::new(SourceFileId::dummy_file(), file_content); + lexer.collect::>() + }; + write_output(&output_dir.join("module.tokens"), &format!("{:#?}", tokens))?; + } + + for (name, contract) in module.contracts.drain(0..) { + let contract_output_dir = output_dir.join(&name); + fs::create_dir_all(&contract_output_dir).map_err(ioerr_to_string)?; + + if targets.contains(&Emit::Abi) { + let file_name = format!("{}_abi.json", &name); + write_output(&contract_output_dir.join(file_name), &contract.json_abi)?; + } + + if targets.contains(&Emit::Yul) { + let file_name = format!("{}_ir.yul", &name); + write_output(&contract_output_dir.join(file_name), &contract.yul)?; + } + + #[cfg(feature = "solc-backend")] + if targets.contains(&Emit::Bytecode) { + let file_name = format!("{}.bin", &name); + write_output(&contract_output_dir.join(file_name), &contract.bytecode)?; + } + } + + Ok(()) +} + +fn write_output(path: &Path, content: &str) -> Result<(), String> { + let mut file = fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(path) + .map_err(ioerr_to_string)?; + file.write_all(content.as_bytes()) + .map_err(ioerr_to_string)?; + Ok(()) +} + +fn ioerr_to_string(error: Error) -> String { + format!("{}", error) +} + +fn verify_nonexistent_or_empty(dir: &Path) -> Result<(), String> { + if !dir.exists() || dir.read_dir().map_err(ioerr_to_string)?.next().is_none() { + Ok(()) + } else { + Err(format!( + "Directory '{}' is not empty. Use --overwrite to overwrite.", + dir.display() + )) + } +} + +fn mir_dump(input_path: &str) { + let mut db = fe_driver::Db::default(); + if Path::new(input_path).is_file() { + let content = match std::fs::read_to_string(input_path) { + Err(err) => { + eprintln!("Failed to load file: `{}`. Error: {}", input_path, err); + std::process::exit(1) + } + Ok(content) => content, + }; + + match fe_driver::dump_mir_single_file(&mut db, input_path, &content) { + Ok(text) => println!("{}", text), + Err(err) => { + eprintln!("Unable to dump mir `{}", input_path); + print_diagnostics(&db, &err.0); + std::process::exit(1) + } + } + } else { + eprintln!("dumping mir for ingot is not supported yet"); + std::process::exit(1) + } +} diff --git a/crates/test-files/fixtures/ingots/basic_ingot/tests.fe b/crates/test-files/fixtures/ingots/basic_ingot/tests.fe new file mode 100644 index 0000000000..a3de38cb1f --- /dev/null +++ b/crates/test-files/fixtures/ingots/basic_ingot/tests.fe @@ -0,0 +1,3 @@ +fn test_1() { + revert +} \ No newline at end of file