use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::fmt::Display;
use std::sync::Mutex;
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 std::sync::LazyLock;
use crate::project::Project;
use crate::solc::solc_compiler::SolcCompiler;
use crate::solc::Compiler;
static PVM_BLOB_CACHE: LazyLock<Mutex<HashMap<CachedBlob, Vec<u8>>>> =
LazyLock::new(Default::default);
static EVM_BLOB_CACHE: LazyLock<Mutex<HashMap<CachedBlob, Vec<u8>>>> =
LazyLock::new(Default::default);
static EVM_RUNTIME_BLOB_CACHE: LazyLock<Mutex<HashMap<CachedBlob, Vec<u8>>>> =
LazyLock::new(Default::default);
static YUL_IR_CACHE: LazyLock<Mutex<HashMap<CachedBlob, String>>> = LazyLock::new(Default::default);
const DEBUG_CONFIG: revive_llvm_context::DebugConfig = DebugConfig::new(None, true);
#[derive(Hash, PartialEq, Eq)]
struct CachedBlob {
contract_name: String,
solc_optimizer_enabled: bool,
solidity: String,
opt: String,
}
pub fn build_solidity(
sources: BTreeMap<String, SolcStandardJsonInputSource>,
) -> anyhow::Result<SolcStandardJsonOutput> {
build_solidity_with_options(
sources,
Default::default(),
Default::default(),
OptimizerSettings::cycles(),
true,
Default::default(),
)
}
pub fn build_solidity_with_options(
sources: BTreeMap<String, SolcStandardJsonInputSource>,
libraries: SolcStandardJsonInputSettingsLibraries,
remappings: BTreeSet<String>,
optimizer_settings: OptimizerSettings,
solc_optimizer_enabled: bool,
suppressed_warnings: Vec<ResolcWarning>,
) -> anyhow::Result<SolcStandardJsonOutput> {
check_dependencies();
inkwell::support::enable_llvm_pretty_stack_trace();
initialize_llvm(PolkaVMTarget::PVM, crate::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)
}
pub fn build_solidity_with_options_evm(
sources: BTreeMap<String, SolcStandardJsonInputSource>,
libraries: SolcStandardJsonInputSettingsLibraries,
remappings: BTreeSet<String>,
solc_optimizer_enabled: bool,
) -> anyhow::Result<BTreeMap<String, (Bytecode, DeployedBytecode)>> {
check_dependencies();
inkwell::support::enable_llvm_pretty_stack_trace();
initialize_llvm(PolkaVMTarget::PVM, crate::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)
}
pub fn build_solidity_and_detect_missing_libraries<T: ToString>(
sources: &[(T, T)],
libraries: SolcStandardJsonInputSettingsLibraries,
) -> anyhow::Result<SolcStandardJsonOutput> {
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 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)
}
pub fn build_yul<T: ToString + Display>(
sources: &[(T, T)],
) -> anyhow::Result<BTreeMap<String, Vec<u8>>> {
check_dependencies();
inkwell::support::enable_llvm_pretty_stack_trace();
initialize_llvm(PolkaVMTarget::PVM, crate::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
}))
}
pub fn build_yul_standard_json(
mut solc_input: SolcStandardJsonInput,
) -> anyhow::Result<SolcStandardJsonOutput> {
check_dependencies();
inkwell::support::enable_llvm_pretty_stack_trace();
initialize_llvm(PolkaVMTarget::PVM, crate::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)
}
pub fn compile_blob(contract_name: &str, source_code: &str) -> Vec<u8> {
compile_blob_with_options(
contract_name,
source_code,
true,
OptimizerSettings::cycles(),
Default::default(),
)
}
pub fn compile_blob_with_options(
contract_name: &str,
source_code: &str,
solc_optimizer_enabled: bool,
optimizer_settings: OptimizerSettings,
libraries: SolcStandardJsonInputSettingsLibraries,
) -> Vec<u8> {
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()),
)]),
libraries,
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
}
pub fn compile_evm_bin_runtime(
contract_name: &str,
source_code: &str,
libraries: SolcStandardJsonInputSettingsLibraries,
) -> Vec<u8> {
compile_evm(contract_name, source_code, true, true, libraries)
}
pub fn compile_evm_deploy_code(
contract_name: &str,
source_code: &str,
solc_optimizer_enabled: bool,
libraries: SolcStandardJsonInputSettingsLibraries,
) -> Vec<u8> {
compile_evm(
contract_name,
source_code,
solc_optimizer_enabled,
false,
libraries,
)
}
pub fn sources<T: ToString>(sources: &[(T, T)]) -> BTreeMap<String, SolcStandardJsonInputSource> {
BTreeMap::from_iter(
sources
.iter()
.map(|(path, code)| (path.to_string(), code.to_string().into())),
)
}
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}}"
);
}
let _ = crate::process::native_process::EXECUTABLE.set(
which::which(crate::r#const::DEFAULT_EXECUTABLE_NAME).expect("resolc should be in ${PATH}"),
);
}
fn compile_evm(
contract_name: &str,
source_code: &str,
solc_optimizer_enabled: bool,
runtime: bool,
libraries: SolcStandardJsonInputSettingsLibraries,
) -> Vec<u8> {
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()),
)]),
libraries,
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
}
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);
}
}