//! Common helper utilities used in tests and benchmarks. use std::collections::BTreeMap; use std::collections::BTreeSet; use std::collections::HashMap; use std::fmt::Display; use std::path::PathBuf; use std::sync::Mutex; use once_cell::sync::Lazy; use revive_common::MetadataHash; use revive_llvm_context::initialize_llvm; use revive_llvm_context::DebugConfig; use revive_llvm_context::OptimizerSettings; use revive_llvm_context::PolkaVMTarget; use revive_solc_json_interface::standard_json::output::contract::evm::bytecode::Bytecode; use revive_solc_json_interface::standard_json::output::contract::evm::bytecode::DeployedBytecode; use revive_solc_json_interface::ResolcWarning; use revive_solc_json_interface::SolcStandardJsonInput; use revive_solc_json_interface::SolcStandardJsonInputSettingsLibraries; use revive_solc_json_interface::SolcStandardJsonInputSettingsMetadata; use revive_solc_json_interface::SolcStandardJsonInputSettingsOptimizer; use revive_solc_json_interface::SolcStandardJsonInputSettingsSelection; use revive_solc_json_interface::SolcStandardJsonInputSource; use revive_solc_json_interface::SolcStandardJsonOutput; use revive_solc_json_interface::SolcStandardJsonOutputErrorHandler; use crate::project::Project; use crate::solc::solc_compiler::SolcCompiler; use crate::solc::Compiler; static PVM_BLOB_CACHE: Lazy>>> = Lazy::new(Default::default); static EVM_BLOB_CACHE: Lazy>>> = Lazy::new(Default::default); static EVM_RUNTIME_BLOB_CACHE: Lazy>>> = Lazy::new(Default::default); static YUL_IR_CACHE: Lazy>> = Lazy::new(Default::default); const DEBUG_CONFIG: revive_llvm_context::DebugConfig = DebugConfig::new(None, true); /// Tests may share and re-use contract code. /// The compiled blob cache helps avoiding duplicate compilation. #[derive(Hash, PartialEq, Eq)] struct CachedBlob { /// The contract name. contract_name: String, /// Whether the solc optimizer is enabled. solc_optimizer_enabled: bool, /// The contract code. solidity: String, /// The optimization level. opt: String, } /// Builds the Solidity project and returns the standard JSON output. pub fn build_solidity( sources: BTreeMap, ) -> anyhow::Result { build_solidity_with_options( sources, Default::default(), Default::default(), OptimizerSettings::cycles(), true, Default::default(), ) } /// Builds the Solidity project and returns the standard JSON output. pub fn build_solidity_with_options( sources: BTreeMap, libraries: SolcStandardJsonInputSettingsLibraries, remappings: BTreeSet, optimizer_settings: OptimizerSettings, solc_optimizer_enabled: bool, suppressed_warnings: Vec, ) -> anyhow::Result { check_dependencies(); inkwell::support::enable_llvm_pretty_stack_trace(); initialize_llvm(PolkaVMTarget::PVM, crate::DEFAULT_EXECUTABLE_NAME, &[]); let _ = crate::process::native_process::EXECUTABLE .set(PathBuf::from(crate::r#const::DEFAULT_EXECUTABLE_NAME)); let solc = SolcCompiler::new(SolcCompiler::DEFAULT_EXECUTABLE_NAME.to_owned())?; let solc_version = solc.version()?; let mut input = SolcStandardJsonInput::try_from_solidity_sources( None, sources.clone(), libraries.clone(), remappings, SolcStandardJsonInputSettingsSelection::new_required_for_tests(), SolcStandardJsonInputSettingsOptimizer::new( solc_optimizer_enabled, optimizer_settings .middle_end_as_string() .chars() .last() .unwrap(), Default::default(), ), SolcStandardJsonInputSettingsMetadata::default(), suppressed_warnings, Default::default(), Default::default(), false, )?; let mut output = solc.standard_json(&mut input, &mut vec![], None, vec![], None)?; if output.has_errors() { return Ok(output); } let debug_config = DebugConfig::new(None, optimizer_settings.middle_end_as_string() != "z"); let linker_symbols = libraries.as_linker_symbols()?; let build = Project::try_from_standard_json_output( &mut output, libraries, &solc_version, &debug_config, )? .compile( &mut vec![], optimizer_settings, MetadataHash::Keccak256, &debug_config, Default::default(), Default::default(), )?; build.check_errors()?; let build = build.link(linker_symbols, &debug_config); build.check_errors()?; build.write_to_standard_json(&mut output, &solc_version)?; output.check_errors()?; Ok(output) } /// Build a Solidity contract and get the EVM code pub fn build_solidity_with_options_evm( sources: BTreeMap, libraries: SolcStandardJsonInputSettingsLibraries, remappings: BTreeSet, solc_optimizer_enabled: bool, ) -> anyhow::Result> { check_dependencies(); inkwell::support::enable_llvm_pretty_stack_trace(); initialize_llvm(PolkaVMTarget::PVM, crate::DEFAULT_EXECUTABLE_NAME, &[]); let _ = crate::process::native_process::EXECUTABLE .set(PathBuf::from(crate::r#const::DEFAULT_EXECUTABLE_NAME)); let solc = SolcCompiler::new(SolcCompiler::DEFAULT_EXECUTABLE_NAME.to_owned())?; let mut input = SolcStandardJsonInput::try_from_solidity_sources( None, sources.clone(), libraries.clone(), remappings, SolcStandardJsonInputSettingsSelection::new_required_for_tests(), SolcStandardJsonInputSettingsOptimizer::new( solc_optimizer_enabled, Default::default(), Default::default(), ), SolcStandardJsonInputSettingsMetadata::default(), Default::default(), Default::default(), Default::default(), false, )?; let mut contracts = BTreeMap::new(); for files in solc .standard_json(&mut input, &mut vec![], None, vec![], None)? .contracts { for (name, contract) in files.1 { if let Some(evm) = contract.evm { let (Some(bytecode), Some(deployed_bytecode)) = (evm.bytecode.as_ref(), evm.deployed_bytecode.as_ref()) else { continue; }; contracts.insert(name.clone(), (bytecode.clone(), deployed_bytecode.clone())); } } } Ok(contracts) } /// Builds the Solidity project and returns the standard JSON output. pub fn build_solidity_and_detect_missing_libraries( sources: &[(T, T)], libraries: SolcStandardJsonInputSettingsLibraries, ) -> anyhow::Result { check_dependencies(); let deployed_libraries = libraries.as_paths(); let sources = BTreeMap::from_iter( sources .iter() .map(|(path, code)| (path.to_string(), code.to_string().into())), ); inkwell::support::enable_llvm_pretty_stack_trace(); initialize_llvm(PolkaVMTarget::PVM, crate::DEFAULT_EXECUTABLE_NAME, &[]); let _ = crate::process::native_process::EXECUTABLE .set(PathBuf::from(crate::r#const::DEFAULT_EXECUTABLE_NAME)); let solc = SolcCompiler::new(SolcCompiler::DEFAULT_EXECUTABLE_NAME.to_owned())?; let solc_version = solc.version()?; let mut input = SolcStandardJsonInput::try_from_solidity_sources( None, sources.clone(), libraries.clone(), Default::default(), SolcStandardJsonInputSettingsSelection::new_required_for_tests(), SolcStandardJsonInputSettingsOptimizer::default(), SolcStandardJsonInputSettingsMetadata::default(), Default::default(), Default::default(), Default::default(), true, )?; let mut output = solc.standard_json(&mut input, &mut vec![], None, vec![], None)?; if output.has_errors() { return Ok(output); } let project = Project::try_from_standard_json_output( &mut output, libraries, &solc_version, &DEBUG_CONFIG, )?; let missing_libraries = project.get_missing_libraries(&deployed_libraries); missing_libraries.write_to_standard_json(&mut output, &solc.version()?); Ok(output) } /// Checks if the Yul project can be built without errors. pub fn build_yul( sources: &[(T, T)], ) -> anyhow::Result>> { check_dependencies(); inkwell::support::enable_llvm_pretty_stack_trace(); initialize_llvm(PolkaVMTarget::PVM, crate::DEFAULT_EXECUTABLE_NAME, &[]); let _ = crate::process::native_process::EXECUTABLE .set(PathBuf::from(crate::r#const::DEFAULT_EXECUTABLE_NAME)); let mut build = Project::try_from_yul_sources( sources .iter() .map(|(path, source)| { ( path.to_string(), SolcStandardJsonInputSource::from(source.to_string()), ) }) .collect(), Default::default(), None, &DEBUG_CONFIG, )? .compile( &mut vec![], OptimizerSettings::size(), MetadataHash::Keccak256, &DEBUG_CONFIG, Default::default(), Default::default(), )?; build.take_and_write_warnings(); build.check_errors()?; let mut build = build.link(Default::default(), &DEBUG_CONFIG); build.take_and_write_warnings(); build.check_errors()?; Ok(build .results .into_iter() .fold(BTreeMap::new(), |mut init, (path, result)| { init.insert(path.to_string(), result.unwrap().build.bytecode); init })) } /// Builds the Yul standard JSON and returns the standard JSON output. pub fn build_yul_standard_json( mut solc_input: SolcStandardJsonInput, ) -> anyhow::Result { check_dependencies(); inkwell::support::enable_llvm_pretty_stack_trace(); initialize_llvm(PolkaVMTarget::PVM, crate::DEFAULT_EXECUTABLE_NAME, &[]); let _ = crate::process::native_process::EXECUTABLE .set(PathBuf::from(crate::r#const::DEFAULT_EXECUTABLE_NAME)); let solc = SolcCompiler::new(SolcCompiler::DEFAULT_EXECUTABLE_NAME.to_owned())?; let mut output = solc.validate_yul_standard_json(&mut solc_input, &mut vec![])?; if output.has_errors() { return Ok(output); } let build = Project::try_from_yul_sources( solc_input.sources, Default::default(), Some(&mut output), &DEBUG_CONFIG, )? .compile( &mut vec![], OptimizerSettings::try_from_cli(solc_input.settings.optimizer.mode)?, MetadataHash::Keccak256, &DEBUG_CONFIG, Default::default(), Default::default(), )?; build.check_errors()?; let build = build.link(Default::default(), &Default::default()); build.check_errors()?; build.write_to_standard_json(&mut output, &solc.version()?)?; Ok(output) } /// Compile the blob of `contract_name` found in given `source_code`. /// The `solc` optimizer will be enabled pub fn compile_blob(contract_name: &str, source_code: &str) -> Vec { compile_blob_with_options( contract_name, source_code, true, OptimizerSettings::cycles(), ) } /// Compile the blob of `contract_name` found in given `source_code`. pub fn compile_blob_with_options( contract_name: &str, source_code: &str, solc_optimizer_enabled: bool, optimizer_settings: OptimizerSettings, ) -> Vec { let id = CachedBlob { contract_name: contract_name.to_owned(), opt: optimizer_settings.middle_end_as_string(), solc_optimizer_enabled, solidity: source_code.to_owned(), }; if let Some(blob) = PVM_BLOB_CACHE.lock().unwrap().get(&id) { return blob.clone(); } let file_name = "contract.sol"; let contracts = build_solidity_with_options( BTreeMap::from([( file_name.to_owned(), SolcStandardJsonInputSource::from(source_code.to_owned()), )]), Default::default(), Default::default(), optimizer_settings, solc_optimizer_enabled, Default::default(), ) .expect("source should compile") .contracts; let bytecode = contracts[file_name][contract_name] .evm .as_ref() .expect("source should produce EVM output") .bytecode .as_ref() .expect("source should produce assembly text") .object .as_str(); let blob = hex::decode(bytecode).expect("hex encoding should always be valid"); assert_eq!(&blob[..3], b"PVM"); PVM_BLOB_CACHE.lock().unwrap().insert(id, blob.clone()); blob } /// Compile the EVM bin-runtime of `contract_name` found in given `source_code`. /// The `solc` optimizer will be enabled pub fn compile_evm_bin_runtime(contract_name: &str, source_code: &str) -> Vec { compile_evm(contract_name, source_code, true, true) } /// Compile the EVM bin of `contract_name` found in given `source_code`. pub fn compile_evm_deploy_code( contract_name: &str, source_code: &str, solc_optimizer_enabled: bool, ) -> Vec { compile_evm(contract_name, source_code, solc_optimizer_enabled, false) } /// Convert `(path, solidity)` tuples to a standard JSON input source. pub fn sources(sources: &[(T, T)]) -> BTreeMap { BTreeMap::from_iter( sources .iter() .map(|(path, code)| (path.to_string(), code.to_string().into())), ) } /// Checks if the required executables are present in `${PATH}`. fn check_dependencies() { for executable in [ crate::r#const::DEFAULT_EXECUTABLE_NAME, SolcCompiler::DEFAULT_EXECUTABLE_NAME, ] .iter() { assert!( which::which(executable).is_ok(), "The `{executable}` executable not found in ${{PATH}}" ); } } /// The internal EVM bytecode compile helper. fn compile_evm( contract_name: &str, source_code: &str, solc_optimizer_enabled: bool, runtime: bool, ) -> Vec { let id = CachedBlob { contract_name: contract_name.to_owned(), solidity: source_code.to_owned(), solc_optimizer_enabled, opt: String::new(), }; let cache = if runtime { &EVM_RUNTIME_BLOB_CACHE } else { &EVM_BLOB_CACHE }; if let Some(blob) = cache.lock().unwrap().get(&id) { return blob.clone(); } let file_name = "contract.sol"; let contracts = build_solidity_with_options_evm( BTreeMap::from([( file_name.into(), SolcStandardJsonInputSource::from(source_code.to_owned()), )]), Default::default(), Default::default(), solc_optimizer_enabled, ) .expect("source should compile"); let object = &contracts .get(contract_name) .unwrap_or_else(|| panic!("contract '{contract_name}' didn't produce bin-runtime")); let code = if runtime { object.1.object.as_str() } else { object.0.object.as_str() }; let blob = hex::decode(code).expect("code shold be hex encoded"); cache.lock().unwrap().insert(id, blob.clone()); blob } /// Compiles the Solidity source code into Yul IR and returns /// the Yul IR code of the contract named `contract_name`. pub fn compile_to_yul( contract_name: &str, source_code: &str, solc_optimizer_enabled: bool, ) -> String { check_dependencies(); let optimizer = SolcStandardJsonInputSettingsOptimizer::new( solc_optimizer_enabled, Default::default(), Default::default(), ); let id = CachedBlob { contract_name: contract_name.to_owned(), solc_optimizer_enabled, solidity: source_code.to_owned(), opt: optimizer.mode.into(), }; if let Some(yul) = YUL_IR_CACHE.lock().unwrap().get(&id) { return yul.clone(); } let file_name = "contract.sol"; let sources = BTreeMap::from([( file_name.to_owned(), SolcStandardJsonInputSource::from(source_code.to_owned()), )]); let mut input = SolcStandardJsonInput::try_from_solidity_sources( None, sources.clone(), Default::default(), Default::default(), SolcStandardJsonInputSettingsSelection::new_required_for_tests(), optimizer, Default::default(), Default::default(), Default::default(), Default::default(), false, ) .unwrap(); let solc = SolcCompiler::new(SolcCompiler::DEFAULT_EXECUTABLE_NAME.to_owned()).unwrap(); let output = solc .standard_json(&mut input, &mut vec![], None, vec![], None) .unwrap(); output.check_errors().unwrap(); let yul = output .contracts .get(file_name) .unwrap_or_else(|| panic!("file `{file_name}` not found in solc output")) .get(contract_name) .unwrap_or_else(|| panic!("contract `{contract_name}` not found in solc output")) .ir_optimized .to_owned(); YUL_IR_CACHE.lock().unwrap().insert(id, yul.clone()); yul } #[cfg(test)] mod tests { use std::fs::read_to_string; use super::compile_to_yul; use crate::cli_utils::SOLIDITY_DEPENDENCY_CONTRACT_PATH; #[test] fn compiles_to_yul() { let contract_name = "Dependency"; let source_code = read_to_string(SOLIDITY_DEPENDENCY_CONTRACT_PATH).unwrap(); let yul = compile_to_yul(contract_name, &source_code, true); assert!( yul.contains(&format!("object \"{contract_name}")), "the `{contract_name}` contract IR code should contain a Yul object" ); } #[test] #[should_panic(expected = "contract `Nonexistent` not found in solc output")] fn error_nonexistent_contract_in_yul() { let contract_name = "Nonexistent"; let source_code = read_to_string(SOLIDITY_DEPENDENCY_CONTRACT_PATH).unwrap(); compile_to_yul(contract_name, &source_code, true); } }