Emerge Yul recompiler (#1)

Provide a modified (and incomplete) version of ZKSync zksolc that can compile the most basic contracts
This commit is contained in:
Cyrill Leutwiler
2024-03-12 12:06:02 +01:00
committed by GitHub
parent d238d8f39e
commit cffa14a4d2
247 changed files with 35357 additions and 4905 deletions
+170
View File
@@ -0,0 +1,170 @@
//!
//! The Solidity contract build.
//!
use std::collections::HashSet;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use serde::Deserialize;
use serde::Serialize;
use crate::solc::combined_json::contract::Contract as CombinedJsonContract;
use crate::solc::standard_json::output::contract::Contract as StandardJsonOutputContract;
///
/// The Solidity contract build.
///
#[derive(Debug, Serialize, Deserialize)]
pub struct Contract {
/// The contract path.
pub path: String,
/// The auxiliary identifier. Used to identify Yul objects.
pub identifier: String,
/// The LLVM module build.
pub build: era_compiler_llvm_context::EraVMBuild,
/// The metadata JSON.
pub metadata_json: serde_json::Value,
/// The factory dependencies.
pub factory_dependencies: HashSet<String>,
}
impl Contract {
///
/// A shortcut constructor.
///
pub fn new(
path: String,
identifier: String,
build: era_compiler_llvm_context::EraVMBuild,
metadata_json: serde_json::Value,
factory_dependencies: HashSet<String>,
) -> Self {
Self {
path,
identifier,
build,
metadata_json,
factory_dependencies,
}
}
///
/// Writes the contract text assembly and bytecode to files.
///
pub fn write_to_directory(
self,
path: &Path,
output_assembly: bool,
output_binary: bool,
overwrite: bool,
) -> anyhow::Result<()> {
let file_name = Self::short_path(self.path.as_str());
if output_assembly {
let file_name = format!(
"{}.{}",
file_name,
era_compiler_common::EXTENSION_ERAVM_ASSEMBLY
);
let mut file_path = path.to_owned();
file_path.push(file_name);
if file_path.exists() && !overwrite {
eprintln!(
"Refusing to overwrite an existing file {file_path:?} (use --overwrite to force)."
);
} else {
File::create(&file_path)
.map_err(|error| {
anyhow::anyhow!("File {:?} creating error: {}", file_path, error)
})?
.write_all(self.build.assembly_text.as_bytes())
.map_err(|error| {
anyhow::anyhow!("File {:?} writing error: {}", file_path, error)
})?;
}
}
if output_binary {
let file_name = format!(
"{}.{}",
file_name,
era_compiler_common::EXTENSION_ERAVM_BINARY
);
let mut file_path = path.to_owned();
file_path.push(file_name);
if file_path.exists() && !overwrite {
eprintln!(
"Refusing to overwrite an existing file {file_path:?} (use --overwrite to force)."
);
} else {
File::create(&file_path)
.map_err(|error| {
anyhow::anyhow!("File {:?} creating error: {}", file_path, error)
})?
.write_all(self.build.bytecode.as_slice())
.map_err(|error| {
anyhow::anyhow!("File {:?} writing error: {}", file_path, error)
})?;
}
}
Ok(())
}
///
/// Writes the contract text assembly and bytecode to the combined JSON.
///
pub fn write_to_combined_json(
self,
combined_json_contract: &mut CombinedJsonContract,
) -> anyhow::Result<()> {
if let Some(metadata) = combined_json_contract.metadata.as_mut() {
*metadata = self.metadata_json.to_string();
}
if let Some(asm) = combined_json_contract.asm.as_mut() {
*asm = serde_json::Value::String(self.build.assembly_text);
}
let hexadecimal_bytecode = hex::encode(self.build.bytecode);
combined_json_contract.bin = Some(hexadecimal_bytecode);
combined_json_contract.bin_runtime = combined_json_contract.bin.clone();
combined_json_contract.factory_deps = Some(self.build.factory_dependencies);
Ok(())
}
///
/// Writes the contract text assembly and bytecode to the standard JSON.
///
pub fn write_to_standard_json(
self,
standard_json_contract: &mut StandardJsonOutputContract,
) -> anyhow::Result<()> {
standard_json_contract.metadata = Some(self.metadata_json);
let assembly_text = self.build.assembly_text;
let bytecode = hex::encode(self.build.bytecode.as_slice());
if let Some(evm) = standard_json_contract.evm.as_mut() {
evm.modify(assembly_text, bytecode);
}
standard_json_contract.factory_dependencies = Some(self.build.factory_dependencies);
standard_json_contract.hash = Some(self.build.bytecode_hash);
Ok(())
}
///
/// Converts the full path to a short one.
///
pub fn short_path(path: &str) -> &str {
path.rfind('/')
.map(|last_slash| &path[last_slash + 1..])
.unwrap_or_else(|| path)
}
}
+107
View File
@@ -0,0 +1,107 @@
//!
//! The Solidity project build.
//!
pub mod contract;
use std::collections::BTreeMap;
use std::path::Path;
use crate::solc::combined_json::CombinedJson;
use crate::solc::standard_json::output::Output as StandardJsonOutput;
use crate::solc::version::Version as SolcVersion;
use self::contract::Contract;
///
/// The Solidity project build.
///
#[derive(Debug, Default)]
pub struct Build {
/// The contract data,
pub contracts: BTreeMap<String, Contract>,
}
impl Build {
///
/// Writes all contracts to the specified directory.
///
pub fn write_to_directory(
self,
output_directory: &Path,
output_assembly: bool,
output_binary: bool,
overwrite: bool,
) -> anyhow::Result<()> {
for (_path, contract) in self.contracts.into_iter() {
contract.write_to_directory(
output_directory,
output_assembly,
output_binary,
overwrite,
)?;
}
Ok(())
}
///
/// Writes all contracts assembly and bytecode to the combined JSON.
///
pub fn write_to_combined_json(
self,
combined_json: &mut CombinedJson,
zksolc_version: &semver::Version,
) -> anyhow::Result<()> {
for (path, contract) in self.contracts.into_iter() {
let combined_json_contract = combined_json
.contracts
.iter_mut()
.find_map(|(json_path, contract)| {
if path.ends_with(json_path) {
Some(contract)
} else {
None
}
})
.ok_or_else(|| anyhow::anyhow!("Contract `{}` not found in the project", path))?;
contract.write_to_combined_json(combined_json_contract)?;
}
combined_json.zk_version = Some(zksolc_version.to_string());
Ok(())
}
///
/// Writes all contracts assembly and bytecode to the standard JSON.
///
pub fn write_to_standard_json(
mut self,
standard_json: &mut StandardJsonOutput,
solc_version: &SolcVersion,
zksolc_version: &semver::Version,
) -> anyhow::Result<()> {
let contracts = match standard_json.contracts.as_mut() {
Some(contracts) => contracts,
None => return Ok(()),
};
for (path, contracts) in contracts.iter_mut() {
for (name, contract) in contracts.iter_mut() {
let full_name = format!("{path}:{name}");
if let Some(contract_data) = self.contracts.remove(full_name.as_str()) {
contract_data.write_to_standard_json(contract)?;
}
}
}
standard_json.version = Some(solc_version.default.to_string());
standard_json.long_version = Some(solc_version.long.to_owned());
standard_json.zk_version = Some(zksolc_version.to_string());
Ok(())
}
}
+20
View File
@@ -0,0 +1,20 @@
//!
//! Solidity to EraVM compiler constants.
//!
#![allow(dead_code)]
/// The default executable name.
pub static DEFAULT_EXECUTABLE_NAME: &str = "zksolc";
/// The `keccak256` scratch space offset.
pub const OFFSET_SCRATCH_SPACE: usize = 0;
/// The memory pointer offset.
pub const OFFSET_MEMORY_POINTER: usize = 2 * era_compiler_common::BYTE_LENGTH_FIELD;
/// The empty slot offset.
pub const OFFSET_EMPTY_SLOT: usize = 3 * era_compiler_common::BYTE_LENGTH_FIELD;
/// The non-reserved memory offset.
pub const OFFSET_NON_RESERVED: usize = 4 * era_compiler_common::BYTE_LENGTH_FIELD;
@@ -0,0 +1,79 @@
//!
//! The inner JSON legacy assembly code element.
//!
use std::collections::HashSet;
use serde::Deserialize;
use serde::Serialize;
use crate::evmla::assembly::Assembly;
///
/// The inner JSON legacy assembly code element.
///
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(untagged)]
pub enum Data {
/// The assembly code wrapper.
Assembly(Assembly),
/// The hash.
Hash(String),
/// The full contract path after the factory dependencies replacing pass.
Path(String),
}
impl Data {
///
/// Returns the inner assembly reference if it is present.
///
pub fn get_assembly(&self) -> Option<&Assembly> {
match self {
Self::Assembly(ref assembly) => Some(assembly),
Self::Hash(_) => None,
Self::Path(_) => None,
}
}
///
/// Returns the inner assembly mutable reference if it is present.
///
pub fn get_assembly_mut(&mut self) -> Option<&mut Assembly> {
match self {
Self::Assembly(ref mut assembly) => Some(assembly),
Self::Hash(_) => None,
Self::Path(_) => None,
}
}
///
/// Get the list of missing deployable libraries.
///
pub fn get_missing_libraries(&self) -> HashSet<String> {
match self {
Self::Assembly(assembly) => assembly.get_missing_libraries(),
Self::Hash(_) => HashSet::new(),
Self::Path(_) => HashSet::new(),
}
}
///
/// Gets the contract `keccak256` hash.
///
pub fn keccak256(&self) -> String {
match self {
Self::Assembly(assembly) => assembly.keccak256(),
Self::Hash(hash) => panic!("Expected assembly, found hash `{hash}`"),
Self::Path(path) => panic!("Expected assembly, found path `{path}`"),
}
}
}
impl std::fmt::Display for Data {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Assembly(inner) => writeln!(f, "{inner}"),
Self::Hash(inner) => writeln!(f, "Hash `{inner}`"),
Self::Path(inner) => writeln!(f, "Path `{inner}`"),
}
}
}
@@ -0,0 +1,88 @@
//!
//! Translates the CODECOPY use cases.
//!
///
/// Translates the contract hash copying.
///
pub fn contract_hash<'ctx, D>(
context: &mut era_compiler_llvm_context::EraVMContext<'ctx, D>,
offset: inkwell::values::IntValue<'ctx>,
value: inkwell::values::IntValue<'ctx>,
) -> anyhow::Result<()>
where
D: era_compiler_llvm_context::EraVMDependency + Clone,
{
let offset = context.builder().build_int_add(
offset,
context.field_const(
(era_compiler_common::BYTE_LENGTH_X32 + era_compiler_common::BYTE_LENGTH_FIELD) as u64,
),
"datacopy_contract_hash_offset",
)?;
era_compiler_llvm_context::eravm_evm_memory::store(context, offset, value)?;
Ok(())
}
///
/// Translates the library marker copying.
///
pub fn library_marker<D>(
context: &mut era_compiler_llvm_context::EraVMContext<D>,
offset: u64,
value: u64,
) -> anyhow::Result<()>
where
D: era_compiler_llvm_context::EraVMDependency + Clone,
{
era_compiler_llvm_context::eravm_evm_memory::store_byte(
context,
context.field_const(offset),
context.field_const(value),
)?;
Ok(())
}
///
/// Translates the static data copying.
///
pub fn static_data<'ctx, D>(
context: &mut era_compiler_llvm_context::EraVMContext<'ctx, D>,
destination: inkwell::values::IntValue<'ctx>,
source: &str,
) -> anyhow::Result<()>
where
D: era_compiler_llvm_context::EraVMDependency + Clone,
{
let mut offset = 0;
for (index, chunk) in source
.chars()
.collect::<Vec<char>>()
.chunks(era_compiler_common::BYTE_LENGTH_FIELD * 2)
.enumerate()
{
let mut value_string = chunk.iter().collect::<String>();
value_string.push_str(
"0".repeat((era_compiler_common::BYTE_LENGTH_FIELD * 2) - chunk.len())
.as_str(),
);
let datacopy_destination = context.builder().build_int_add(
destination,
context.field_const(offset as u64),
format!("datacopy_destination_index_{index}").as_str(),
)?;
let datacopy_value = context.field_const_str_hex(value_string.as_str());
era_compiler_llvm_context::eravm_evm_memory::store(
context,
datacopy_destination,
datacopy_value,
)?;
offset += chunk.len() / 2;
}
Ok(())
}
@@ -0,0 +1,75 @@
//!
//! Translates the jump operations.
//!
///
/// Translates the unconditional jump.
///
pub fn unconditional<D>(
context: &mut era_compiler_llvm_context::EraVMContext<D>,
destination: num::BigUint,
stack_hash: md5::Digest,
) -> anyhow::Result<()>
where
D: era_compiler_llvm_context::EraVMDependency + Clone,
{
let code_type = context
.code_type()
.ok_or_else(|| anyhow::anyhow!("The contract code part type is undefined"))?;
let block_key = era_compiler_llvm_context::EraVMFunctionBlockKey::new(code_type, destination);
let block = context
.current_function()
.borrow()
.evmla()
.find_block(&block_key, &stack_hash)?;
context.build_unconditional_branch(block.inner());
Ok(())
}
///
/// Translates the conditional jump.
///
pub fn conditional<D>(
context: &mut era_compiler_llvm_context::EraVMContext<D>,
destination: num::BigUint,
stack_hash: md5::Digest,
stack_height: usize,
) -> anyhow::Result<()>
where
D: era_compiler_llvm_context::EraVMDependency + Clone,
{
let code_type = context
.code_type()
.ok_or_else(|| anyhow::anyhow!("The contract code part type is undefined"))?;
let block_key = era_compiler_llvm_context::EraVMFunctionBlockKey::new(code_type, destination);
let condition_pointer = context.evmla().stack[stack_height]
.to_llvm()
.into_pointer_value();
let condition = context.build_load(
era_compiler_llvm_context::EraVMPointer::new_stack_field(context, condition_pointer),
format!("conditional_{block_key}_condition").as_str(),
)?;
let condition = context.builder().build_int_compare(
inkwell::IntPredicate::NE,
condition.into_int_value(),
context.field_const(0),
format!("conditional_{block_key}_condition_compared").as_str(),
)?;
let then_block = context
.current_function()
.borrow()
.evmla()
.find_block(&block_key, &stack_hash)?;
let join_block =
context.append_basic_block(format!("conditional_{block_key}_join_block").as_str());
context.build_conditional_branch(condition, then_block.inner(), join_block)?;
context.set_basic_block(join_block);
Ok(())
}
@@ -0,0 +1,398 @@
//!
//! The EVM instruction.
//!
pub mod codecopy;
pub mod jump;
pub mod name;
pub mod stack;
use std::collections::BTreeMap;
use serde::Deserialize;
use serde::Serialize;
use self::name::Name;
///
/// The EVM instruction.
///
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Instruction {
/// The opcode or tag identifier.
pub name: Name,
/// The optional value argument.
pub value: Option<String>,
/// The source code identifier.
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<isize>,
/// The source code location begin.
pub begin: isize,
/// The source code location end.
pub end: isize,
}
impl Instruction {
///
/// Returns the number of input stack arguments.
///
pub const fn input_size(&self, version: &semver::Version) -> usize {
match self.name {
Name::POP => 1,
Name::JUMP => 1,
Name::JUMPI => 2,
Name::ADD => 2,
Name::SUB => 2,
Name::MUL => 2,
Name::DIV => 2,
Name::MOD => 2,
Name::SDIV => 2,
Name::SMOD => 2,
Name::LT => 2,
Name::GT => 2,
Name::EQ => 2,
Name::ISZERO => 1,
Name::SLT => 2,
Name::SGT => 2,
Name::OR => 2,
Name::XOR => 2,
Name::NOT => 1,
Name::AND => 2,
Name::SHL => 2,
Name::SHR => 2,
Name::SAR => 2,
Name::BYTE => 2,
Name::ADDMOD => 3,
Name::MULMOD => 3,
Name::EXP => 2,
Name::SIGNEXTEND => 2,
Name::SHA3 => 2,
Name::KECCAK256 => 2,
Name::MLOAD => 1,
Name::MSTORE => 2,
Name::MSTORE8 => 2,
Name::MCOPY => 3,
Name::SLOAD => 1,
Name::SSTORE => 2,
Name::TLOAD => 1,
Name::TSTORE => 2,
Name::PUSHIMMUTABLE => 0,
Name::ASSIGNIMMUTABLE => {
if version.minor >= 8 {
2
} else {
1
}
}
Name::CALLDATALOAD => 1,
Name::CALLDATACOPY => 3,
Name::CODECOPY => 3,
Name::RETURNDATACOPY => 3,
Name::EXTCODESIZE => 1,
Name::EXTCODEHASH => 1,
Name::CALL => 7,
Name::CALLCODE => 7,
Name::STATICCALL => 6,
Name::DELEGATECALL => 6,
Name::RETURN => 2,
Name::REVERT => 2,
Name::SELFDESTRUCT => 1,
Name::LOG0 => 2,
Name::LOG1 => 3,
Name::LOG2 => 4,
Name::LOG3 => 5,
Name::LOG4 => 6,
Name::CREATE => 3,
Name::CREATE2 => 4,
Name::ZK_CREATE => 3,
Name::ZK_CREATE2 => 4,
Name::BALANCE => 1,
Name::BLOCKHASH => 1,
Name::BLOBHASH => 1,
Name::EXTCODECOPY => 4,
Name::RecursiveCall { input_size, .. } => input_size,
Name::RecursiveReturn { input_size } => input_size,
_ => 0,
}
}
///
/// Returns the number of output stack arguments.
///
pub const fn output_size(&self) -> usize {
match self.name {
Name::PUSH => 1,
Name::PUSH_Data => 1,
Name::PUSH_Tag => 1,
Name::PUSH_ContractHash => 1,
Name::PUSH_ContractHashSize => 1,
Name::PUSHLIB => 1,
Name::PUSHDEPLOYADDRESS => 1,
Name::PUSH1 => 1,
Name::PUSH2 => 1,
Name::PUSH3 => 1,
Name::PUSH4 => 1,
Name::PUSH5 => 1,
Name::PUSH6 => 1,
Name::PUSH7 => 1,
Name::PUSH8 => 1,
Name::PUSH9 => 1,
Name::PUSH10 => 1,
Name::PUSH11 => 1,
Name::PUSH12 => 1,
Name::PUSH13 => 1,
Name::PUSH14 => 1,
Name::PUSH15 => 1,
Name::PUSH16 => 1,
Name::PUSH17 => 1,
Name::PUSH18 => 1,
Name::PUSH19 => 1,
Name::PUSH20 => 1,
Name::PUSH21 => 1,
Name::PUSH22 => 1,
Name::PUSH23 => 1,
Name::PUSH24 => 1,
Name::PUSH25 => 1,
Name::PUSH26 => 1,
Name::PUSH27 => 1,
Name::PUSH28 => 1,
Name::PUSH29 => 1,
Name::PUSH30 => 1,
Name::PUSH31 => 1,
Name::PUSH32 => 1,
Name::DUP1 => 1,
Name::DUP2 => 1,
Name::DUP3 => 1,
Name::DUP4 => 1,
Name::DUP5 => 1,
Name::DUP6 => 1,
Name::DUP7 => 1,
Name::DUP8 => 1,
Name::DUP9 => 1,
Name::DUP10 => 1,
Name::DUP11 => 1,
Name::DUP12 => 1,
Name::DUP13 => 1,
Name::DUP14 => 1,
Name::DUP15 => 1,
Name::DUP16 => 1,
Name::ADD => 1,
Name::SUB => 1,
Name::MUL => 1,
Name::DIV => 1,
Name::MOD => 1,
Name::SDIV => 1,
Name::SMOD => 1,
Name::LT => 1,
Name::GT => 1,
Name::EQ => 1,
Name::ISZERO => 1,
Name::SLT => 1,
Name::SGT => 1,
Name::OR => 1,
Name::XOR => 1,
Name::NOT => 1,
Name::AND => 1,
Name::SHL => 1,
Name::SHR => 1,
Name::SAR => 1,
Name::BYTE => 1,
Name::ADDMOD => 1,
Name::MULMOD => 1,
Name::EXP => 1,
Name::SIGNEXTEND => 1,
Name::SHA3 => 1,
Name::KECCAK256 => 1,
Name::MLOAD => 1,
Name::SLOAD => 1,
Name::TLOAD => 1,
Name::PUSHIMMUTABLE => 1,
Name::CALLDATALOAD => 1,
Name::CALLDATASIZE => 1,
Name::CODESIZE => 1,
Name::PUSHSIZE => 1,
Name::RETURNDATASIZE => 1,
Name::EXTCODESIZE => 1,
Name::EXTCODEHASH => 1,
Name::CALL => 1,
Name::CALLCODE => 1,
Name::STATICCALL => 1,
Name::DELEGATECALL => 1,
Name::CREATE => 1,
Name::CREATE2 => 1,
Name::ZK_CREATE => 1,
Name::ZK_CREATE2 => 1,
Name::ADDRESS => 1,
Name::CALLER => 1,
Name::TIMESTAMP => 1,
Name::NUMBER => 1,
Name::CALLVALUE => 1,
Name::GAS => 1,
Name::BALANCE => 1,
Name::SELFBALANCE => 1,
Name::GASLIMIT => 1,
Name::GASPRICE => 1,
Name::ORIGIN => 1,
Name::CHAINID => 1,
Name::BLOCKHASH => 1,
Name::BLOBHASH => 1,
Name::DIFFICULTY => 1,
Name::PREVRANDAO => 1,
Name::COINBASE => 1,
Name::MSIZE => 1,
Name::BASEFEE => 1,
Name::BLOBBASEFEE => 1,
Name::PC => 1,
Name::RecursiveCall { output_size, .. } => output_size,
_ => 0,
}
}
///
/// Replaces the instruction data aliases with the actual data.
///
pub fn replace_data_aliases(
instructions: &mut [Self],
mapping: &BTreeMap<String, String>,
) -> anyhow::Result<()> {
for instruction in instructions.iter_mut() {
match instruction {
Instruction {
name: Name::PUSH_ContractHash | Name::PUSH_ContractHashSize,
value: Some(value),
..
} => {
*value = mapping.get(value.as_str()).cloned().ok_or_else(|| {
anyhow::anyhow!("Contract alias `{}` data not found", value)
})?;
}
Instruction {
name: Name::PUSH_Data,
value: Some(value),
..
} => {
let mut key_extended =
"0".repeat(era_compiler_common::BYTE_LENGTH_FIELD * 2 - value.len());
key_extended.push_str(value.as_str());
*value = mapping.get(key_extended.as_str()).cloned().ok_or_else(|| {
anyhow::anyhow!("Data chunk alias `{}` data not found", key_extended)
})?;
}
_ => {}
}
}
Ok(())
}
///
/// Initializes an `INVALID` instruction to terminate an invalid unreachable block part.
///
pub fn invalid(previous: &Self) -> Self {
Self {
name: Name::INVALID,
value: None,
source: previous.source,
begin: previous.begin,
end: previous.end,
}
}
///
/// Initializes a recursive function `Call` instruction.
///
pub fn recursive_call(
name: String,
entry_key: era_compiler_llvm_context::EraVMFunctionBlockKey,
stack_hash: md5::Digest,
input_size: usize,
output_size: usize,
return_address: era_compiler_llvm_context::EraVMFunctionBlockKey,
previous: &Self,
) -> Self {
Self {
name: Name::RecursiveCall {
name,
entry_key,
stack_hash,
input_size,
output_size,
return_address,
},
value: None,
source: previous.source,
begin: previous.begin,
end: previous.end,
}
}
///
/// Initializes a recursive function `Return` instruction.
///
pub fn recursive_return(input_size: usize, previous: &Self) -> Self {
Self {
name: Name::RecursiveReturn { input_size },
value: None,
source: previous.source,
begin: previous.begin,
end: previous.end,
}
}
}
impl std::fmt::Display for Instruction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let name = self.name.to_string();
match self.name {
Name::Tag => write!(f, "{:4}", name),
_ => write!(f, "{:15}", name),
}?;
match self.value {
Some(ref value) if value.len() <= 64 => write!(f, "{}", value)?,
Some(ref value) => write!(f, "... {}", &value[value.len() - 60..])?,
None => {}
}
Ok(())
}
}
@@ -0,0 +1,421 @@
//!
//! The EVM instruction name.
//!
use serde::Deserialize;
use serde::Serialize;
///
/// The EVM instruction name.
///
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
#[allow(non_camel_case_types)]
#[allow(clippy::upper_case_acronyms)]
pub enum Name {
/// The eponymous EVM instruction.
PUSH,
/// Pushes a constant tag index.
#[serde(rename = "PUSH [tag]")]
PUSH_Tag,
/// Pushes an unknown `data` value.
#[serde(rename = "PUSH data")]
PUSH_Data,
/// Pushes a contract hash size.
#[serde(rename = "PUSH #[$]")]
PUSH_ContractHashSize,
/// Pushes a contract hash.
#[serde(rename = "PUSH [$]")]
PUSH_ContractHash,
/// The eponymous EVM instruction.
PUSH1,
/// The eponymous EVM instruction.
PUSH2,
/// The eponymous EVM instruction.
PUSH3,
/// The eponymous EVM instruction.
PUSH4,
/// The eponymous EVM instruction.
PUSH5,
/// The eponymous EVM instruction.
PUSH6,
/// The eponymous EVM instruction.
PUSH7,
/// The eponymous EVM instruction.
PUSH8,
/// The eponymous EVM instruction.
PUSH9,
/// The eponymous EVM instruction.
PUSH10,
/// The eponymous EVM instruction.
PUSH11,
/// The eponymous EVM instruction.
PUSH12,
/// The eponymous EVM instruction.
PUSH13,
/// The eponymous EVM instruction.
PUSH14,
/// The eponymous EVM instruction.
PUSH15,
/// The eponymous EVM instruction.
PUSH16,
/// The eponymous EVM instruction.
PUSH17,
/// The eponymous EVM instruction.
PUSH18,
/// The eponymous EVM instruction.
PUSH19,
/// The eponymous EVM instruction.
PUSH20,
/// The eponymous EVM instruction.
PUSH21,
/// The eponymous EVM instruction.
PUSH22,
/// The eponymous EVM instruction.
PUSH23,
/// The eponymous EVM instruction.
PUSH24,
/// The eponymous EVM instruction.
PUSH25,
/// The eponymous EVM instruction.
PUSH26,
/// The eponymous EVM instruction.
PUSH27,
/// The eponymous EVM instruction.
PUSH28,
/// The eponymous EVM instruction.
PUSH29,
/// The eponymous EVM instruction.
PUSH30,
/// The eponymous EVM instruction.
PUSH31,
/// The eponymous EVM instruction.
PUSH32,
/// The eponymous EVM instruction.
DUP1,
/// The eponymous EVM instruction.
DUP2,
/// The eponymous EVM instruction.
DUP3,
/// The eponymous EVM instruction.
DUP4,
/// The eponymous EVM instruction.
DUP5,
/// The eponymous EVM instruction.
DUP6,
/// The eponymous EVM instruction.
DUP7,
/// The eponymous EVM instruction.
DUP8,
/// The eponymous EVM instruction.
DUP9,
/// The eponymous EVM instruction.
DUP10,
/// The eponymous EVM instruction.
DUP11,
/// The eponymous EVM instruction.
DUP12,
/// The eponymous EVM instruction.
DUP13,
/// The eponymous EVM instruction.
DUP14,
/// The eponymous EVM instruction.
DUP15,
/// The eponymous EVM instruction.
DUP16,
/// The eponymous EVM instruction.
SWAP1,
/// The eponymous EVM instruction.
SWAP2,
/// The eponymous EVM instruction.
SWAP3,
/// The eponymous EVM instruction.
SWAP4,
/// The eponymous EVM instruction.
SWAP5,
/// The eponymous EVM instruction.
SWAP6,
/// The eponymous EVM instruction.
SWAP7,
/// The eponymous EVM instruction.
SWAP8,
/// The eponymous EVM instruction.
SWAP9,
/// The eponymous EVM instruction.
SWAP10,
/// The eponymous EVM instruction.
SWAP11,
/// The eponymous EVM instruction.
SWAP12,
/// The eponymous EVM instruction.
SWAP13,
/// The eponymous EVM instruction.
SWAP14,
/// The eponymous EVM instruction.
SWAP15,
/// The eponymous EVM instruction.
SWAP16,
/// The eponymous EVM instruction.
POP,
/// Sets the current basic code block.
#[serde(rename = "tag")]
Tag,
/// The eponymous EVM instruction.
JUMP,
/// The eponymous EVM instruction.
JUMPI,
/// The eponymous EVM instruction.
JUMPDEST,
/// The eponymous EVM instruction.
ADD,
/// The eponymous EVM instruction.
SUB,
/// The eponymous EVM instruction.
MUL,
/// The eponymous EVM instruction.
DIV,
/// The eponymous EVM instruction.
MOD,
/// The eponymous EVM instruction.
SDIV,
/// The eponymous EVM instruction.
SMOD,
/// The eponymous EVM instruction.
LT,
/// The eponymous EVM instruction.
GT,
/// The eponymous EVM instruction.
EQ,
/// The eponymous EVM instruction.
ISZERO,
/// The eponymous EVM instruction.
SLT,
/// The eponymous EVM instruction.
SGT,
/// The eponymous EVM instruction.
OR,
/// The eponymous EVM instruction.
XOR,
/// The eponymous EVM instruction.
NOT,
/// The eponymous EVM instruction.
AND,
/// The eponymous EVM instruction.
SHL,
/// The eponymous EVM instruction.
SHR,
/// The eponymous EVM instruction.
SAR,
/// The eponymous EVM instruction.
BYTE,
/// The eponymous EVM instruction.
ADDMOD,
/// The eponymous EVM instruction.
MULMOD,
/// The eponymous EVM instruction.
EXP,
/// The eponymous EVM instruction.
SIGNEXTEND,
/// The eponymous EVM instruction.
SHA3,
/// The eponymous EVM instruction.
KECCAK256,
/// The eponymous EVM instruction.
MLOAD,
/// The eponymous EVM instruction.
MSTORE,
/// The eponymous EVM instruction.
MSTORE8,
/// The eponymous EVM instruction.
MCOPY,
/// The eponymous EVM instruction.
SLOAD,
/// The eponymous EVM instruction.
SSTORE,
/// The eponymous EVM instruction.
TLOAD,
/// The eponymous EVM instruction.
TSTORE,
/// The eponymous EVM instruction.
PUSHIMMUTABLE,
/// The eponymous EVM instruction.
ASSIGNIMMUTABLE,
/// The eponymous EVM instruction.
CALLDATALOAD,
/// The eponymous EVM instruction.
CALLDATASIZE,
/// The eponymous EVM instruction.
CALLDATACOPY,
/// The eponymous EVM instruction.
CODESIZE,
/// The eponymous EVM instruction.
CODECOPY,
/// The eponymous EVM instruction.
PUSHSIZE,
/// The eponymous EVM instruction.
EXTCODESIZE,
/// The eponymous EVM instruction.
EXTCODEHASH,
/// The eponymous EVM instruction.
RETURNDATASIZE,
/// The eponymous EVM instruction.
RETURNDATACOPY,
/// The eponymous EVM instruction.
RETURN,
/// The eponymous EVM instruction.
REVERT,
/// The eponymous EVM instruction.
STOP,
/// The eponymous EVM instruction.
INVALID,
/// The eponymous EVM instruction.
LOG0,
/// The eponymous EVM instruction.
LOG1,
/// The eponymous EVM instruction.
LOG2,
/// The eponymous EVM instruction.
LOG3,
/// The eponymous EVM instruction.
LOG4,
/// The eponymous EVM instruction.
CALL,
/// The eponymous EVM instruction.
STATICCALL,
/// The eponymous EVM instruction.
DELEGATECALL,
/// The eponymous EVM instruction.
CREATE,
/// The eponymous EVM instruction.
CREATE2,
/// The eponymous EraVM instruction.
#[serde(rename = "$ZK_CREATE")]
ZK_CREATE,
/// The eponymous EraVM instruction.
#[serde(rename = "$ZK_CREATE2")]
ZK_CREATE2,
/// The eponymous EVM instruction.
ADDRESS,
/// The eponymous EVM instruction.
CALLER,
/// The eponymous EVM instruction.
CALLVALUE,
/// The eponymous EVM instruction.
GAS,
/// The eponymous EVM instruction.
BALANCE,
/// The eponymous EVM instruction.
SELFBALANCE,
/// The eponymous EVM instruction.
PUSHLIB,
/// The eponymous EVM instruction.
PUSHDEPLOYADDRESS,
/// The eponymous EVM instruction.
GASLIMIT,
/// The eponymous EVM instruction.
GASPRICE,
/// The eponymous EVM instruction.
ORIGIN,
/// The eponymous EVM instruction.
CHAINID,
/// The eponymous EVM instruction.
TIMESTAMP,
/// The eponymous EVM instruction.
NUMBER,
/// The eponymous EVM instruction.
BLOCKHASH,
/// The eponymous EVM instruction.
BLOBHASH,
/// The eponymous EVM instruction.
DIFFICULTY,
/// The eponymous EVM instruction.
PREVRANDAO,
/// The eponymous EVM instruction.
COINBASE,
/// The eponymous EVM instruction.
BASEFEE,
/// The eponymous EVM instruction.
BLOBBASEFEE,
/// The eponymous EVM instruction.
MSIZE,
/// The eponymous EVM instruction.
CALLCODE,
/// The eponymous EVM instruction.
PC,
/// The eponymous EVM instruction.
EXTCODECOPY,
/// The eponymous EVM instruction.
SELFDESTRUCT,
/// The recursive function call instruction.
#[serde(skip)]
RecursiveCall {
/// The called function name.
name: String,
/// The called function key.
entry_key: era_compiler_llvm_context::EraVMFunctionBlockKey,
/// The stack state hash after return.
stack_hash: md5::Digest,
/// The input size.
input_size: usize,
/// The output size.
output_size: usize,
/// The return address.
return_address: era_compiler_llvm_context::EraVMFunctionBlockKey,
},
/// The recursive function return instruction.
#[serde(skip)]
RecursiveReturn {
/// The output size.
input_size: usize,
},
}
impl std::fmt::Display for Name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Tag => write!(f, "Tag"),
Self::RecursiveCall {
name,
entry_key,
input_size,
output_size,
return_address,
..
} => write!(
f,
"RECURSIVE_CALL({}_{}, {}, {}, {})",
name, entry_key, input_size, output_size, return_address
),
Self::RecursiveReturn { input_size } => write!(f, "RECURSIVE_RETURN({})", input_size),
_ => write!(
f,
"{}",
serde_json::to_string(self)
.expect("Always valid")
.trim_matches('\"')
),
}
}
}
@@ -0,0 +1,114 @@
//!
//! Translates the stack memory operations.
//!
use inkwell::values::BasicValue;
///
/// Translates the ordinar value push.
///
pub fn push<'ctx, D>(
context: &mut era_compiler_llvm_context::EraVMContext<'ctx, D>,
value: String,
) -> anyhow::Result<inkwell::values::BasicValueEnum<'ctx>>
where
D: era_compiler_llvm_context::EraVMDependency + Clone,
{
let result = context
.field_type()
.const_int_from_string(
value.to_ascii_uppercase().as_str(),
inkwell::types::StringRadix::Hexadecimal,
)
.expect("Always valid")
.as_basic_value_enum();
Ok(result)
}
///
/// Translates the block tag label push.
///
pub fn push_tag<'ctx, D>(
context: &mut era_compiler_llvm_context::EraVMContext<'ctx, D>,
value: String,
) -> anyhow::Result<inkwell::values::BasicValueEnum<'ctx>>
where
D: era_compiler_llvm_context::EraVMDependency + Clone,
{
let result = context
.field_type()
.const_int_from_string(value.as_str(), inkwell::types::StringRadix::Decimal)
.expect("Always valid");
Ok(result.as_basic_value_enum())
}
///
/// Translates the stack memory duplicate.
///
pub fn dup<'ctx, D>(
context: &mut era_compiler_llvm_context::EraVMContext<'ctx, D>,
offset: usize,
height: usize,
original: &mut Option<String>,
) -> anyhow::Result<inkwell::values::BasicValueEnum<'ctx>>
where
D: era_compiler_llvm_context::EraVMDependency + Clone,
{
let element = &context.evmla().stack[height - offset - 1];
let value = context.build_load(
era_compiler_llvm_context::EraVMPointer::new_stack_field(
context,
element.to_llvm().into_pointer_value(),
),
format!("dup{offset}").as_str(),
)?;
*original = element.original.to_owned();
Ok(value)
}
///
/// Translates the stack memory swap.
///
pub fn swap<D>(
context: &mut era_compiler_llvm_context::EraVMContext<D>,
offset: usize,
height: usize,
) -> anyhow::Result<()>
where
D: era_compiler_llvm_context::EraVMDependency + Clone,
{
let top_element = context.evmla().stack[height - 1].to_owned();
let top_pointer = era_compiler_llvm_context::EraVMPointer::new_stack_field(
context,
top_element.to_llvm().into_pointer_value(),
);
let top_value = context.build_load(top_pointer, format!("swap{offset}_top_value").as_str())?;
let swap_element = context.evmla().stack[height - offset - 1].to_owned();
let swap_pointer = era_compiler_llvm_context::EraVMPointer::new_stack_field(
context,
swap_element.to_llvm().into_pointer_value(),
);
let swap_value =
context.build_load(swap_pointer, format!("swap{offset}_swap_value").as_str())?;
context.evmla_mut().stack[height - 1].original = swap_element.original.to_owned();
context.evmla_mut().stack[height - offset - 1].original = top_element.original.to_owned();
context.build_store(top_pointer, swap_value)?;
context.build_store(swap_pointer, top_value)?;
Ok(())
}
///
/// Translates the stack memory pop.
///
pub fn pop<D>(_context: &mut era_compiler_llvm_context::EraVMContext<D>) -> anyhow::Result<()>
where
D: era_compiler_llvm_context::EraVMDependency + Clone,
{
Ok(())
}
+313
View File
@@ -0,0 +1,313 @@
//!
//! The `solc --asm-json` output.
//!
pub mod data;
pub mod instruction;
use std::collections::BTreeMap;
use std::collections::HashSet;
use serde::Deserialize;
use serde::Serialize;
use crate::evmla::ethereal_ir::entry_link::EntryLink;
use crate::evmla::ethereal_ir::EtherealIR;
use crate::solc::standard_json::output::contract::evm::extra_metadata::ExtraMetadata;
use self::data::Data;
use self::instruction::name::Name as InstructionName;
use self::instruction::Instruction;
///
/// The JSON assembly.
///
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Assembly {
/// The metadata string.
#[serde(rename = ".auxdata")]
pub auxdata: Option<String>,
/// The deploy code instructions.
#[serde(rename = ".code")]
pub code: Option<Vec<Instruction>>,
/// The runtime code.
#[serde(rename = ".data")]
pub data: Option<BTreeMap<String, Data>>,
/// The full contract path.
#[serde(skip_serializing_if = "Option::is_none")]
pub full_path: Option<String>,
/// The factory dependency paths.
#[serde(default = "HashSet::new")]
pub factory_dependencies: HashSet<String>,
/// The EVMLA extra metadata.
#[serde(skip_serializing_if = "Option::is_none")]
pub extra_metadata: Option<ExtraMetadata>,
}
impl Assembly {
///
/// Gets the contract `keccak256` hash.
///
pub fn keccak256(&self) -> String {
let json = serde_json::to_vec(self).expect("Always valid");
era_compiler_llvm_context::eravm_utils::keccak256(json.as_slice())
}
///
/// Sets the full contract path.
///
pub fn set_full_path(&mut self, full_path: String) {
self.full_path = Some(full_path);
}
///
/// Returns the full contract path if it is set, or `<undefined>` otherwise.
///
/// # Panics
/// If the `full_path` has not been set.
///
pub fn full_path(&self) -> &str {
self.full_path
.as_deref()
.unwrap_or_else(|| panic!("The full path of some contracts is unset"))
}
///
/// Get the list of missing deployable libraries.
///
pub fn get_missing_libraries(&self) -> HashSet<String> {
let mut missing_libraries = HashSet::new();
if let Some(code) = self.code.as_ref() {
for instruction in code.iter() {
if let InstructionName::PUSHLIB = instruction.name {
let library_path = instruction.value.to_owned().expect("Always exists");
missing_libraries.insert(library_path);
}
}
}
if let Some(data) = self.data.as_ref() {
for (_, data) in data.iter() {
missing_libraries.extend(data.get_missing_libraries());
}
}
missing_libraries
}
///
/// Replaces the deploy code dependencies with full contract path and returns the list.
///
pub fn deploy_dependencies_pass(
&mut self,
full_path: &str,
hash_data_mapping: &BTreeMap<String, String>,
) -> anyhow::Result<BTreeMap<String, String>> {
let mut index_path_mapping = BTreeMap::new();
let index = "0".repeat(era_compiler_common::BYTE_LENGTH_FIELD * 2);
index_path_mapping.insert(index, full_path.to_owned());
let dependencies = match self.data.as_mut() {
Some(dependencies) => dependencies,
None => return Ok(index_path_mapping),
};
for (index, data) in dependencies.iter_mut() {
if index == "0" {
continue;
}
let mut index_extended =
"0".repeat(era_compiler_common::BYTE_LENGTH_FIELD * 2 - index.len());
index_extended.push_str(index.as_str());
*data = match data {
Data::Assembly(assembly) => {
let hash = assembly.keccak256();
let full_path =
hash_data_mapping
.get(hash.as_str())
.cloned()
.ok_or_else(|| {
anyhow::anyhow!("Contract path not found for hash `{}`", hash)
})?;
self.factory_dependencies.insert(full_path.to_owned());
index_path_mapping.insert(index_extended, full_path.clone());
Data::Path(full_path)
}
Data::Hash(hash) => {
index_path_mapping.insert(index_extended, hash.to_owned());
continue;
}
_ => continue,
};
}
Ok(index_path_mapping)
}
///
/// Replaces the runtime code dependencies with full contract path and returns the list.
///
pub fn runtime_dependencies_pass(
&mut self,
full_path: &str,
hash_data_mapping: &BTreeMap<String, String>,
) -> anyhow::Result<BTreeMap<String, String>> {
let mut index_path_mapping = BTreeMap::new();
let index = "0".repeat(era_compiler_common::BYTE_LENGTH_FIELD * 2);
index_path_mapping.insert(index, full_path.to_owned());
let dependencies = match self
.data
.as_mut()
.and_then(|data| data.get_mut("0"))
.and_then(|data| data.get_assembly_mut())
.and_then(|assembly| assembly.data.as_mut())
{
Some(dependencies) => dependencies,
None => return Ok(index_path_mapping),
};
for (index, data) in dependencies.iter_mut() {
let mut index_extended =
"0".repeat(era_compiler_common::BYTE_LENGTH_FIELD * 2 - index.len());
index_extended.push_str(index.as_str());
*data = match data {
Data::Assembly(assembly) => {
let hash = assembly.keccak256();
let full_path =
hash_data_mapping
.get(hash.as_str())
.cloned()
.ok_or_else(|| {
anyhow::anyhow!("Contract path not found for hash `{}`", hash)
})?;
self.factory_dependencies.insert(full_path.to_owned());
index_path_mapping.insert(index_extended, full_path.clone());
Data::Path(full_path)
}
Data::Hash(hash) => {
index_path_mapping.insert(index_extended, hash.to_owned());
continue;
}
_ => continue,
};
}
Ok(index_path_mapping)
}
}
impl<D> era_compiler_llvm_context::EraVMWriteLLVM<D> for Assembly
where
D: era_compiler_llvm_context::EraVMDependency + Clone,
{
fn declare(
&mut self,
context: &mut era_compiler_llvm_context::EraVMContext<D>,
) -> anyhow::Result<()> {
let mut entry = era_compiler_llvm_context::EraVMEntryFunction::default();
entry.declare(context)?;
let mut runtime = era_compiler_llvm_context::EraVMRuntime::new(
era_compiler_llvm_context::EraVMAddressSpace::Heap,
);
runtime.declare(context)?;
era_compiler_llvm_context::EraVMDeployCodeFunction::new(
era_compiler_llvm_context::EraVMDummyLLVMWritable::default(),
)
.declare(context)?;
era_compiler_llvm_context::EraVMRuntimeCodeFunction::new(
era_compiler_llvm_context::EraVMDummyLLVMWritable::default(),
)
.declare(context)?;
entry.into_llvm(context)?;
runtime.into_llvm(context)?;
Ok(())
}
fn into_llvm(
mut self,
context: &mut era_compiler_llvm_context::EraVMContext<D>,
) -> anyhow::Result<()> {
let full_path = self.full_path().to_owned();
if let Some(debug_config) = context.debug_config() {
debug_config.dump_evmla(full_path.as_str(), self.to_string().as_str())?;
}
let deploy_code_blocks = EtherealIR::get_blocks(
context.evmla().version.to_owned(),
era_compiler_llvm_context::EraVMCodeType::Deploy,
self.code
.as_deref()
.ok_or_else(|| anyhow::anyhow!("Deploy code instructions not found"))?,
)?;
let data = self
.data
.ok_or_else(|| anyhow::anyhow!("Runtime code data not found"))?
.remove("0")
.expect("Always exists");
if let Some(debug_config) = context.debug_config() {
debug_config.dump_evmla(full_path.as_str(), data.to_string().as_str())?;
}
let runtime_code_instructions = match data {
Data::Assembly(assembly) => assembly
.code
.ok_or_else(|| anyhow::anyhow!("Runtime code instructions not found"))?,
Data::Hash(hash) => {
anyhow::bail!("Expected runtime code instructions, found hash `{}`", hash)
}
Data::Path(path) => {
anyhow::bail!("Expected runtime code instructions, found path `{}`", path)
}
};
let runtime_code_blocks = EtherealIR::get_blocks(
context.evmla().version.to_owned(),
era_compiler_llvm_context::EraVMCodeType::Runtime,
runtime_code_instructions.as_slice(),
)?;
let extra_metadata = self.extra_metadata.take().unwrap_or_default();
let mut blocks = deploy_code_blocks;
blocks.extend(runtime_code_blocks);
let mut ethereal_ir =
EtherealIR::new(context.evmla().version.to_owned(), extra_metadata, blocks)?;
if let Some(debug_config) = context.debug_config() {
debug_config.dump_ethir(full_path.as_str(), ethereal_ir.to_string().as_str())?;
}
ethereal_ir.declare(context)?;
ethereal_ir.into_llvm(context)?;
era_compiler_llvm_context::EraVMDeployCodeFunction::new(EntryLink::new(
era_compiler_llvm_context::EraVMCodeType::Deploy,
))
.into_llvm(context)?;
era_compiler_llvm_context::EraVMRuntimeCodeFunction::new(EntryLink::new(
era_compiler_llvm_context::EraVMCodeType::Runtime,
))
.into_llvm(context)?;
Ok(())
}
}
impl std::fmt::Display for Assembly {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(instructions) = self.code.as_ref() {
for (index, instruction) in instructions.iter().enumerate() {
match instruction.name {
InstructionName::Tag => writeln!(f, "{index:03} {instruction}")?,
_ => writeln!(f, "{index:03} {instruction}")?,
}
}
}
Ok(())
}
}
@@ -0,0 +1,58 @@
//!
//! The Ethereal IR entry function link.
//!
use inkwell::values::BasicValue;
use crate::evmla::ethereal_ir::EtherealIR;
///
/// The Ethereal IR entry function link.
///
/// The link represents branching between the deploy and runtime code.
///
#[derive(Debug, Clone)]
pub struct EntryLink {
/// The code part type.
pub code_type: era_compiler_llvm_context::EraVMCodeType,
}
impl EntryLink {
///
/// A shortcut constructor.
///
pub fn new(code_type: era_compiler_llvm_context::EraVMCodeType) -> Self {
Self { code_type }
}
}
impl<D> era_compiler_llvm_context::EraVMWriteLLVM<D> for EntryLink
where
D: era_compiler_llvm_context::EraVMDependency + Clone,
{
fn into_llvm(
self,
context: &mut era_compiler_llvm_context::EraVMContext<D>,
) -> anyhow::Result<()> {
let target = context
.get_function(EtherealIR::DEFAULT_ENTRY_FUNCTION_NAME)
.expect("Always exists")
.borrow()
.declaration();
let is_deploy_code = match self.code_type {
era_compiler_llvm_context::EraVMCodeType::Deploy => context
.integer_type(era_compiler_common::BIT_LENGTH_BOOLEAN)
.const_int(1, false),
era_compiler_llvm_context::EraVMCodeType::Runtime => context
.integer_type(era_compiler_common::BIT_LENGTH_BOOLEAN)
.const_int(0, false),
};
context.build_invoke(
target,
&[is_deploy_code.as_basic_value_enum()],
format!("call_link_{}", EtherealIR::DEFAULT_ENTRY_FUNCTION_NAME).as_str(),
);
Ok(())
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,41 @@
//!
//! The Ethereal IR block element stack element.
//!
///
/// The Ethereal IR block element stack element.
///
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Element {
/// The runtime value.
Value(String),
/// The compile-time value.
Constant(num::BigUint),
/// The compile-time destination tag.
Tag(num::BigUint),
/// The compile-time path.
Path(String),
/// The compile-time hexadecimal data chunk.
Data(String),
/// The recursive function return address.
ReturnAddress(usize),
}
impl Element {
pub fn value(identifier: String) -> Self {
Self::Value(identifier)
}
}
impl std::fmt::Display for Element {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Value(identifier) => write!(f, "V_{identifier}"),
Self::Constant(value) => write!(f, "{value:X}"),
Self::Tag(tag) => write!(f, "T_{tag}"),
Self::Path(path) => write!(f, "{path}"),
Self::Data(data) => write!(f, "{data}"),
Self::ReturnAddress(_) => write!(f, "RETURN_ADDRESS"),
}
}
}
@@ -0,0 +1,149 @@
//!
//! The Ethereal IR block element stack.
//!
pub mod element;
use self::element::Element;
///
/// The Ethereal IR block element stack.
///
#[derive(Debug, Default, Clone)]
pub struct Stack {
/// The stack elements.
pub elements: Vec<Element>,
}
impl Stack {
/// The default stack size.
pub const DEFAULT_STACK_SIZE: usize = 16;
///
/// A shortcut constructor.
///
pub fn new() -> Self {
Self {
elements: Vec::with_capacity(Self::DEFAULT_STACK_SIZE),
}
}
///
/// A shortcut constructor.
///
pub fn with_capacity(capacity: usize) -> Self {
Self {
elements: Vec::with_capacity(capacity),
}
}
///
/// A shortcut constructor.
///
pub fn new_with_elements(elements: Vec<Element>) -> Self {
Self { elements }
}
///
/// The stack state hash, which acts as a block identifier.
///
/// Each block clone has its own initial stack state, which uniquely identifies the block.
///
pub fn hash(&self) -> md5::Digest {
let mut hash_context = md5::Context::new();
for element in self.elements.iter() {
match element {
Element::Tag(tag) => hash_context.consume(tag.to_bytes_be()),
_ => hash_context.consume([0]),
}
}
hash_context.compute()
}
///
/// Pushes an element onto the stack.
///
pub fn push(&mut self, element: Element) {
self.elements.push(element);
}
///
/// Appends another stack on top of this one.
///
pub fn append(&mut self, other: &mut Self) {
self.elements.append(&mut other.elements);
}
///
/// Pops a stack element.
///
pub fn pop(&mut self) -> anyhow::Result<Element> {
self.elements
.pop()
.ok_or_else(|| anyhow::anyhow!("Stack underflow"))
}
///
/// Pops the tag from the top.
///
pub fn pop_tag(&mut self) -> anyhow::Result<num::BigUint> {
match self.elements.pop() {
Some(Element::Tag(tag)) => Ok(tag),
Some(element) => anyhow::bail!("Expected tag, found {}", element),
None => anyhow::bail!("Stack underflow"),
}
}
///
/// Swaps two stack elements.
///
pub fn swap(&mut self, index: usize) -> anyhow::Result<()> {
if self.elements.len() < index + 1 {
anyhow::bail!("Stack underflow");
}
let length = self.elements.len();
self.elements.swap(length - 1, length - 1 - index);
Ok(())
}
///
/// Duplicates a stack element.
///
pub fn dup(&mut self, index: usize) -> anyhow::Result<Element> {
if self.elements.len() < index {
anyhow::bail!("Stack underflow");
}
Ok(self.elements[self.elements.len() - index].to_owned())
}
///
/// Returns the stack length.
///
pub fn len(&self) -> usize {
self.elements.len()
}
///
/// Returns an emptiness flag.
///
pub fn is_empty(&self) -> bool {
self.elements.len() == 0
}
}
impl std::fmt::Display for Stack {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"[ {} ]",
self.elements
.iter()
.map(Element::to_string)
.collect::<Vec<String>>()
.join(" | ")
)
}
}
@@ -0,0 +1,167 @@
//!
//! The Ethereal IR block.
//!
pub mod element;
use std::collections::HashSet;
use num::Zero;
use crate::evmla::assembly::instruction::name::Name as InstructionName;
use crate::evmla::assembly::instruction::Instruction;
use self::element::stack::Stack as ElementStack;
use self::element::Element;
///
/// The Ethereal IR block.
///
#[derive(Debug, Clone)]
pub struct Block {
/// The Solidity compiler version.
pub solc_version: semver::Version,
/// The block key.
pub key: era_compiler_llvm_context::EraVMFunctionBlockKey,
/// The block instance.
pub instance: Option<usize>,
/// The block elements relevant to the stack consistency.
pub elements: Vec<Element>,
/// The block predecessors.
pub predecessors: HashSet<(era_compiler_llvm_context::EraVMFunctionBlockKey, usize)>,
/// The initial stack state.
pub initial_stack: ElementStack,
/// The stack.
pub stack: ElementStack,
/// The extra block hashes for alternative routes.
pub extra_hashes: Vec<md5::Digest>,
}
impl Block {
/// The elements vector initial capacity.
pub const ELEMENTS_VECTOR_DEFAULT_CAPACITY: usize = 64;
/// The predecessors hashset initial capacity.
pub const PREDECESSORS_HASHSET_DEFAULT_CAPACITY: usize = 4;
///
/// Assembles a block from the sequence of instructions.
///
pub fn try_from_instructions(
solc_version: semver::Version,
code_type: era_compiler_llvm_context::EraVMCodeType,
slice: &[Instruction],
) -> anyhow::Result<(Self, usize)> {
let mut cursor = 0;
let tag: num::BigUint = match slice[cursor].name {
InstructionName::Tag => {
let tag = slice[cursor]
.value
.as_deref()
.expect("Always exists")
.parse()
.expect("Always valid");
cursor += 1;
tag
}
_ => num::BigUint::zero(),
};
let mut block = Self {
solc_version: solc_version.clone(),
key: era_compiler_llvm_context::EraVMFunctionBlockKey::new(code_type, tag),
instance: None,
elements: Vec::with_capacity(Self::ELEMENTS_VECTOR_DEFAULT_CAPACITY),
predecessors: HashSet::with_capacity(Self::PREDECESSORS_HASHSET_DEFAULT_CAPACITY),
initial_stack: ElementStack::new(),
stack: ElementStack::new(),
extra_hashes: vec![],
};
let mut dead_code = false;
while cursor < slice.len() {
if !dead_code {
let element: Element = Element::new(solc_version.clone(), slice[cursor].to_owned());
block.elements.push(element);
}
match slice[cursor].name {
InstructionName::RETURN
| InstructionName::REVERT
| InstructionName::STOP
| InstructionName::INVALID => {
cursor += 1;
dead_code = true;
}
InstructionName::JUMP => {
cursor += 1;
dead_code = true;
}
InstructionName::Tag => {
break;
}
_ => {
cursor += 1;
}
}
}
Ok((block, cursor))
}
///
/// Inserts a predecessor tag.
///
pub fn insert_predecessor(
&mut self,
key: era_compiler_llvm_context::EraVMFunctionBlockKey,
instance: usize,
) {
self.predecessors.insert((key, instance));
}
}
impl<D> era_compiler_llvm_context::EraVMWriteLLVM<D> for Block
where
D: era_compiler_llvm_context::EraVMDependency + Clone,
{
fn into_llvm(
self,
context: &mut era_compiler_llvm_context::EraVMContext<D>,
) -> anyhow::Result<()> {
context.set_code_type(self.key.code_type);
for element in self.elements.into_iter() {
element.into_llvm(context)?;
}
Ok(())
}
}
impl std::fmt::Display for Block {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(
f,
"block_{}/{}: {}",
self.key,
self.instance.unwrap_or_default(),
if self.predecessors.is_empty() {
"".to_owned()
} else {
format!(
"(predecessors: {})",
self.predecessors
.iter()
.map(|(key, instance)| format!("{}/{}", key, instance))
.collect::<Vec<String>>()
.join(", ")
)
},
)?;
for element in self.elements.iter() {
writeln!(f, " {element}")?;
}
Ok(())
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,35 @@
//!
//! The Ethereal IR block queue element.
//!
use crate::evmla::ethereal_ir::function::block::element::stack::Stack;
///
/// The Ethereal IR block queue element.
///
#[derive(Debug, Clone)]
pub struct QueueElement {
/// The block key.
pub block_key: era_compiler_llvm_context::EraVMFunctionBlockKey,
/// The block predecessor.
pub predecessor: Option<(era_compiler_llvm_context::EraVMFunctionBlockKey, usize)>,
/// The predecessor's last stack state.
pub stack: Stack,
}
impl QueueElement {
///
/// A shortcut constructor.
///
pub fn new(
block_key: era_compiler_llvm_context::EraVMFunctionBlockKey,
predecessor: Option<(era_compiler_llvm_context::EraVMFunctionBlockKey, usize)>,
stack: Stack,
) -> Self {
Self {
block_key,
predecessor,
stack,
}
}
}
@@ -0,0 +1,49 @@
//!
//! The Ethereal IR function type.
//!
///
/// The Ethereal IR function type.
///
#[derive(Debug, Clone)]
pub enum Type {
/// The initial function, combining deploy and runtime code.
Initial,
/// The recursive function with a specific block starting its recursive context.
Recursive {
/// The function name.
name: String,
/// The function initial block key.
block_key: era_compiler_llvm_context::EraVMFunctionBlockKey,
/// The size of stack input (in cells or 256-bit words).
input_size: usize,
/// The size of stack output (in cells or 256-bit words).
output_size: usize,
},
}
impl Type {
///
/// A shortcut constructor.
///
pub fn new_initial() -> Self {
Self::Initial
}
///
/// A shortcut constructor.
///
pub fn new_recursive(
name: String,
block_key: era_compiler_llvm_context::EraVMFunctionBlockKey,
input_size: usize,
output_size: usize,
) -> Self {
Self::Recursive {
name,
block_key,
input_size,
output_size,
}
}
}
@@ -0,0 +1,71 @@
//!
//! The Ethereal IR block visited element.
//!
use std::cmp::Ordering;
///
/// The Ethereal IR block visited element.
///
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct VisitedElement {
/// The block key.
pub block_key: era_compiler_llvm_context::EraVMFunctionBlockKey,
/// The initial stack state hash.
pub stack_hash: md5::Digest,
}
impl VisitedElement {
///
/// A shortcut constructor.
///
pub fn new(
block_key: era_compiler_llvm_context::EraVMFunctionBlockKey,
stack_hash: md5::Digest,
) -> Self {
Self {
block_key,
stack_hash,
}
}
}
impl PartialOrd for VisitedElement {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for VisitedElement {
fn cmp(&self, other: &Self) -> Ordering {
match (self.block_key.code_type, other.block_key.code_type) {
(
era_compiler_llvm_context::EraVMCodeType::Deploy,
era_compiler_llvm_context::EraVMCodeType::Runtime,
) => Ordering::Less,
(
era_compiler_llvm_context::EraVMCodeType::Runtime,
era_compiler_llvm_context::EraVMCodeType::Deploy,
) => Ordering::Greater,
(
era_compiler_llvm_context::EraVMCodeType::Deploy,
era_compiler_llvm_context::EraVMCodeType::Deploy,
)
| (
era_compiler_llvm_context::EraVMCodeType::Runtime,
era_compiler_llvm_context::EraVMCodeType::Runtime,
) => {
let tag_comparison = self.block_key.tag.cmp(&other.block_key.tag);
if tag_comparison == Ordering::Equal {
if self.stack_hash == other.stack_hash {
Ordering::Equal
} else {
Ordering::Less
}
} else {
tag_comparison
}
}
}
}
}
@@ -0,0 +1,150 @@
//!
//! The Ethereal IR of the EVM bytecode.
//!
pub mod entry_link;
pub mod function;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::collections::HashMap;
use crate::evmla::assembly::instruction::Instruction;
use crate::solc::standard_json::output::contract::evm::extra_metadata::ExtraMetadata;
use self::function::block::Block;
use self::function::r#type::Type as FunctionType;
use self::function::Function;
///
/// The Ethereal IR of the EVM bytecode.
///
/// The Ethereal IR (EthIR) is a special IR between the EVM legacy assembly and LLVM IR. It is
/// created to facilitate the translation and provide an additional environment for applying some
/// transformations, duplicating parts of the call and control flow graphs, tracking the
/// data flow, and a few more algorithms of static analysis.
///
/// The most important feature of EthIR is flattening the block tags and duplicating blocks for
/// each of initial states of the stack. The LLVM IR supports only static control flow, so the
/// stack state must be known all the way throughout the program.
///
#[derive(Debug)]
pub struct EtherealIR {
/// The Solidity compiler version.
pub solc_version: semver::Version,
/// The EVMLA extra metadata.
pub extra_metadata: ExtraMetadata,
/// The all-inlined function.
pub entry_function: Function,
/// The recursive functions.
pub recursive_functions: BTreeMap<era_compiler_llvm_context::EraVMFunctionBlockKey, Function>,
}
impl EtherealIR {
/// The default entry function name.
pub const DEFAULT_ENTRY_FUNCTION_NAME: &'static str = "main";
/// The blocks hashmap initial capacity.
pub const BLOCKS_HASHMAP_DEFAULT_CAPACITY: usize = 64;
///
/// Assembles a sequence of functions from the sequence of instructions.
///
pub fn new(
solc_version: semver::Version,
extra_metadata: ExtraMetadata,
blocks: HashMap<era_compiler_llvm_context::EraVMFunctionBlockKey, Block>,
) -> anyhow::Result<Self> {
let mut entry_function = Function::new(solc_version.clone(), FunctionType::new_initial());
let mut recursive_functions = BTreeMap::new();
let mut visited_functions = BTreeSet::new();
entry_function.traverse(
&blocks,
&mut recursive_functions,
&extra_metadata,
&mut visited_functions,
)?;
Ok(Self {
solc_version,
extra_metadata,
entry_function,
recursive_functions,
})
}
///
/// Gets blocks for the specified type of the contract code.
///
pub fn get_blocks(
solc_version: semver::Version,
code_type: era_compiler_llvm_context::EraVMCodeType,
instructions: &[Instruction],
) -> anyhow::Result<HashMap<era_compiler_llvm_context::EraVMFunctionBlockKey, Block>> {
let mut blocks = HashMap::with_capacity(Self::BLOCKS_HASHMAP_DEFAULT_CAPACITY);
let mut offset = 0;
while offset < instructions.len() {
let (block, size) = Block::try_from_instructions(
solc_version.clone(),
code_type,
&instructions[offset..],
)?;
blocks.insert(
era_compiler_llvm_context::EraVMFunctionBlockKey::new(
code_type,
block.key.tag.clone(),
),
block,
);
offset += size;
}
Ok(blocks)
}
}
impl<D> era_compiler_llvm_context::EraVMWriteLLVM<D> for EtherealIR
where
D: era_compiler_llvm_context::EraVMDependency + Clone,
{
fn declare(
&mut self,
context: &mut era_compiler_llvm_context::EraVMContext<D>,
) -> anyhow::Result<()> {
self.entry_function.declare(context)?;
for (_key, function) in self.recursive_functions.iter_mut() {
function.declare(context)?;
}
Ok(())
}
fn into_llvm(
self,
context: &mut era_compiler_llvm_context::EraVMContext<D>,
) -> anyhow::Result<()> {
context.evmla_mut().stack = vec![];
self.entry_function.into_llvm(context)?;
for (_key, function) in self.recursive_functions.into_iter() {
function.into_llvm(context)?;
}
Ok(())
}
}
impl std::fmt::Display for EtherealIR {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "{}", self.entry_function)?;
for (_key, function) in self.recursive_functions.iter() {
writeln!(f, "{}", function)?;
}
Ok(())
}
}
+6
View File
@@ -0,0 +1,6 @@
//!
//! The EVM legacy assembly compiling tools.
//!
pub mod assembly;
pub mod ethereal_ir;
+396
View File
@@ -0,0 +1,396 @@
//!
//! Solidity to EraVM compiler library.
//!
pub(crate) mod build;
pub(crate) mod r#const;
pub(crate) mod evmla;
pub(crate) mod missing_libraries;
pub(crate) mod process;
pub(crate) mod project;
pub(crate) mod solc;
pub(crate) mod warning;
pub(crate) mod yul;
pub use self::build::contract::Contract as ContractBuild;
pub use self::build::Build;
pub use self::missing_libraries::MissingLibraries;
pub use self::process::input::Input as ProcessInput;
pub use self::process::output::Output as ProcessOutput;
pub use self::process::run as run_process;
pub use self::process::EXECUTABLE;
pub use self::project::contract::Contract as ProjectContract;
pub use self::project::Project;
pub use self::r#const::*;
pub use self::solc::combined_json::contract::Contract as SolcCombinedJsonContract;
pub use self::solc::combined_json::CombinedJson as SolcCombinedJson;
pub use self::solc::pipeline::Pipeline as SolcPipeline;
pub use self::solc::standard_json::input::language::Language as SolcStandardJsonInputLanguage;
pub use self::solc::standard_json::input::settings::metadata::Metadata as SolcStandardJsonInputSettingsMetadata;
pub use self::solc::standard_json::input::settings::optimizer::Optimizer as SolcStandardJsonInputSettingsOptimizer;
pub use self::solc::standard_json::input::settings::selection::file::flag::Flag as SolcStandardJsonInputSettingsSelectionFileFlag;
pub use self::solc::standard_json::input::settings::selection::file::File as SolcStandardJsonInputSettingsSelectionFile;
pub use self::solc::standard_json::input::settings::selection::Selection as SolcStandardJsonInputSettingsSelection;
pub use self::solc::standard_json::input::settings::Settings as SolcStandardJsonInputSettings;
pub use self::solc::standard_json::input::source::Source as SolcStandardJsonInputSource;
pub use self::solc::standard_json::input::Input as SolcStandardJsonInput;
pub use self::solc::standard_json::output::contract::evm::bytecode::Bytecode as SolcStandardJsonOutputContractEVMBytecode;
pub use self::solc::standard_json::output::contract::evm::EVM as SolcStandardJsonOutputContractEVM;
pub use self::solc::standard_json::output::contract::Contract as SolcStandardJsonOutputContract;
pub use self::solc::standard_json::output::Output as SolcStandardJsonOutput;
pub use self::solc::version::Version as SolcVersion;
pub use self::solc::Compiler as SolcCompiler;
pub use self::warning::Warning;
pub mod test_utils;
pub mod tests;
use std::collections::BTreeSet;
use std::path::PathBuf;
///
/// Runs the Yul mode.
///
pub fn yul(
input_files: &[PathBuf],
solc: &mut SolcCompiler,
optimizer_settings: era_compiler_llvm_context::OptimizerSettings,
is_system_mode: bool,
include_metadata_hash: bool,
debug_config: Option<era_compiler_llvm_context::DebugConfig>,
) -> anyhow::Result<Build> {
let path = match input_files.len() {
1 => input_files.first().expect("Always exists"),
0 => anyhow::bail!("The input file is missing"),
length => anyhow::bail!(
"Only one input file is allowed in the Yul mode, but found {}",
length,
),
};
let solc_validator = if is_system_mode {
None
} else {
if solc.version()?.default != SolcCompiler::LAST_SUPPORTED_VERSION {
anyhow::bail!(
"The Yul mode is only supported with the most recent version of the Solidity compiler: {}",
SolcCompiler::LAST_SUPPORTED_VERSION,
);
}
Some(&*solc)
};
let project = Project::try_from_yul_path(path, solc_validator)?;
let build = project.compile(
optimizer_settings,
is_system_mode,
include_metadata_hash,
false,
debug_config,
)?;
Ok(build)
}
///
/// Runs the LLVM IR mode.
///
pub fn llvm_ir(
input_files: &[PathBuf],
optimizer_settings: era_compiler_llvm_context::OptimizerSettings,
is_system_mode: bool,
include_metadata_hash: bool,
debug_config: Option<era_compiler_llvm_context::DebugConfig>,
) -> anyhow::Result<Build> {
let path = match input_files.len() {
1 => input_files.first().expect("Always exists"),
0 => anyhow::bail!("The input file is missing"),
length => anyhow::bail!(
"Only one input file is allowed in the LLVM IR mode, but found {}",
length,
),
};
let project = Project::try_from_llvm_ir_path(path)?;
let build = project.compile(
optimizer_settings,
is_system_mode,
include_metadata_hash,
false,
debug_config,
)?;
Ok(build)
}
///
/// Runs the EraVM assembly mode.
///
pub fn zkasm(
input_files: &[PathBuf],
include_metadata_hash: bool,
debug_config: Option<era_compiler_llvm_context::DebugConfig>,
) -> anyhow::Result<Build> {
let path = match input_files.len() {
1 => input_files.first().expect("Always exists"),
0 => anyhow::bail!("The input file is missing"),
length => anyhow::bail!(
"Only one input file is allowed in the EraVM assembly mode, but found {}",
length,
),
};
let project = Project::try_from_zkasm_path(path)?;
let optimizer_settings = era_compiler_llvm_context::OptimizerSettings::none();
let build = project.compile(
optimizer_settings,
false,
include_metadata_hash,
false,
debug_config,
)?;
Ok(build)
}
///
/// Runs the standard output mode.
///
#[allow(clippy::too_many_arguments)]
pub fn standard_output(
input_files: &[PathBuf],
libraries: Vec<String>,
solc: &mut SolcCompiler,
evm_version: Option<era_compiler_common::EVMVersion>,
solc_optimizer_enabled: bool,
optimizer_settings: era_compiler_llvm_context::OptimizerSettings,
force_evmla: bool,
is_system_mode: bool,
include_metadata_hash: bool,
base_path: Option<String>,
include_paths: Vec<String>,
allow_paths: Option<String>,
remappings: Option<BTreeSet<String>>,
suppressed_warnings: Option<Vec<Warning>>,
debug_config: Option<era_compiler_llvm_context::DebugConfig>,
) -> anyhow::Result<Build> {
let solc_version = solc.version()?;
let solc_pipeline = SolcPipeline::new(&solc_version, force_evmla);
let solc_input = SolcStandardJsonInput::try_from_paths(
SolcStandardJsonInputLanguage::Solidity,
evm_version,
input_files,
libraries,
remappings,
SolcStandardJsonInputSettingsSelection::new_required(solc_pipeline),
SolcStandardJsonInputSettingsOptimizer::new(
solc_optimizer_enabled,
None,
&solc_version.default,
optimizer_settings.is_fallback_to_size_enabled(),
optimizer_settings.is_system_request_memoization_disabled(),
),
None,
solc_pipeline == SolcPipeline::Yul,
suppressed_warnings,
)?;
let source_code_files = solc_input
.sources
.iter()
.map(|(path, source)| (path.to_owned(), source.content.to_owned()))
.collect();
let libraries = solc_input.settings.libraries.clone().unwrap_or_default();
let mut solc_output = solc.standard_json(
solc_input,
solc_pipeline,
base_path,
include_paths,
allow_paths,
)?;
if let Some(errors) = solc_output.errors.as_deref() {
let mut has_errors = false;
for error in errors.iter() {
if error.severity.as_str() == "error" {
has_errors = true;
}
eprintln!("{error}");
}
if has_errors {
anyhow::bail!("Error(s) found. Compilation aborted");
}
}
let project = solc_output.try_to_project(
source_code_files,
libraries,
solc_pipeline,
&solc_version,
debug_config.as_ref(),
)?;
let build = project.compile(
optimizer_settings,
is_system_mode,
include_metadata_hash,
false,
debug_config,
)?;
Ok(build)
}
///
/// Runs the standard JSON mode.
///
#[allow(clippy::too_many_arguments)]
pub fn standard_json(
solc: &mut SolcCompiler,
detect_missing_libraries: bool,
force_evmla: bool,
is_system_mode: bool,
base_path: Option<String>,
include_paths: Vec<String>,
allow_paths: Option<String>,
debug_config: Option<era_compiler_llvm_context::DebugConfig>,
) -> anyhow::Result<()> {
let solc_version = solc.version()?;
let solc_pipeline = SolcPipeline::new(&solc_version, force_evmla);
let zksolc_version = semver::Version::parse(env!("CARGO_PKG_VERSION")).expect("Always valid");
let solc_input = SolcStandardJsonInput::try_from_stdin(solc_pipeline)?;
let source_code_files = solc_input
.sources
.iter()
.map(|(path, source)| (path.to_owned(), source.content.to_owned()))
.collect();
let optimizer_settings =
era_compiler_llvm_context::OptimizerSettings::try_from(&solc_input.settings.optimizer)?;
let include_metadata_hash = match solc_input.settings.metadata {
Some(ref metadata) => {
metadata.bytecode_hash != Some(era_compiler_llvm_context::EraVMMetadataHash::None)
}
None => true,
};
let libraries = solc_input.settings.libraries.clone().unwrap_or_default();
let mut solc_output = solc.standard_json(
solc_input,
solc_pipeline,
base_path,
include_paths,
allow_paths,
)?;
if let Some(errors) = solc_output.errors.as_deref() {
for error in errors.iter() {
if error.severity.as_str() == "error" {
serde_json::to_writer(std::io::stdout(), &solc_output)?;
std::process::exit(0);
}
}
}
let project = solc_output.try_to_project(
source_code_files,
libraries,
solc_pipeline,
&solc_version,
debug_config.as_ref(),
)?;
if detect_missing_libraries {
let missing_libraries = project.get_missing_libraries();
missing_libraries.write_to_standard_json(
&mut solc_output,
&solc_version,
&zksolc_version,
)?;
} else {
let build = project.compile(
optimizer_settings,
is_system_mode,
include_metadata_hash,
false,
debug_config,
)?;
build.write_to_standard_json(&mut solc_output, &solc_version, &zksolc_version)?;
}
serde_json::to_writer(std::io::stdout(), &solc_output)?;
std::process::exit(0);
}
///
/// Runs the combined JSON mode.
///
#[allow(clippy::too_many_arguments)]
pub fn combined_json(
format: String,
input_files: &[PathBuf],
libraries: Vec<String>,
solc: &mut SolcCompiler,
evm_version: Option<era_compiler_common::EVMVersion>,
solc_optimizer_enabled: bool,
optimizer_settings: era_compiler_llvm_context::OptimizerSettings,
force_evmla: bool,
is_system_mode: bool,
include_metadata_hash: bool,
base_path: Option<String>,
include_paths: Vec<String>,
allow_paths: Option<String>,
remappings: Option<BTreeSet<String>>,
suppressed_warnings: Option<Vec<Warning>>,
debug_config: Option<era_compiler_llvm_context::DebugConfig>,
output_directory: Option<PathBuf>,
overwrite: bool,
) -> anyhow::Result<()> {
let zksolc_version = semver::Version::parse(env!("CARGO_PKG_VERSION")).expect("Always valid");
let build = standard_output(
input_files,
libraries,
solc,
evm_version,
solc_optimizer_enabled,
optimizer_settings,
force_evmla,
is_system_mode,
include_metadata_hash,
base_path,
include_paths,
allow_paths,
remappings,
suppressed_warnings,
debug_config,
)?;
let mut combined_json = solc.combined_json(input_files, format.as_str())?;
build.write_to_combined_json(&mut combined_json, &zksolc_version)?;
match output_directory {
Some(output_directory) => {
std::fs::create_dir_all(output_directory.as_path())?;
combined_json.write_to_directory(output_directory.as_path(), overwrite)?;
}
None => {
println!(
"{}",
serde_json::to_string(&combined_json).expect("Always valid")
);
}
}
std::process::exit(0);
}
+58
View File
@@ -0,0 +1,58 @@
//!
//! The missing Solidity libraries.
//!
use std::collections::BTreeMap;
use std::collections::HashSet;
use crate::solc::standard_json::output::Output as StandardJsonOutput;
use crate::solc::version::Version as SolcVersion;
///
/// The missing Solidity libraries.
///
pub struct MissingLibraries {
/// The missing libraries.
pub contract_libraries: BTreeMap<String, HashSet<String>>,
}
impl MissingLibraries {
///
/// A shortcut constructor.
///
pub fn new(contract_libraries: BTreeMap<String, HashSet<String>>) -> Self {
Self { contract_libraries }
}
///
/// Writes the missing libraries to the standard JSON.
///
pub fn write_to_standard_json(
mut self,
standard_json: &mut StandardJsonOutput,
solc_version: &SolcVersion,
zksolc_version: &semver::Version,
) -> anyhow::Result<()> {
let contracts = match standard_json.contracts.as_mut() {
Some(contracts) => contracts,
None => return Ok(()),
};
for (path, contracts) in contracts.iter_mut() {
for (name, contract) in contracts.iter_mut() {
let full_name = format!("{path}:{name}");
let missing_libraries = self.contract_libraries.remove(full_name.as_str());
if let Some(missing_libraries) = missing_libraries {
contract.missing_libraries = Some(missing_libraries);
}
}
}
standard_json.version = Some(solc_version.default.to_string());
standard_json.long_version = Some(solc_version.long.to_owned());
standard_json.zk_version = Some(zksolc_version.to_string());
Ok(())
}
}
+57
View File
@@ -0,0 +1,57 @@
//!
//! Process for compiling a single compilation unit.
//!
//! The input data.
//!
use serde::Deserialize;
use serde::Serialize;
use crate::project::contract::Contract;
use crate::project::Project;
///
/// The input data.
///
#[derive(Debug, Serialize, Deserialize)]
pub struct Input {
/// The contract representation.
pub contract: Contract,
/// The project representation.
pub project: Project,
/// The system mode flag.
pub is_system_mode: bool,
/// Whether to append the metadata hash.
pub include_metadata_hash: bool,
/// Enables the test bytecode encoding.
pub enable_test_encoding: bool,
/// The optimizer settings.
pub optimizer_settings: era_compiler_llvm_context::OptimizerSettings,
/// The debug output config.
pub debug_config: Option<era_compiler_llvm_context::DebugConfig>,
}
impl Input {
///
/// A shortcut constructor.
///
pub fn new(
contract: Contract,
project: Project,
is_system_mode: bool,
include_metadata_hash: bool,
enable_test_encoding: bool,
optimizer_settings: era_compiler_llvm_context::OptimizerSettings,
debug_config: Option<era_compiler_llvm_context::DebugConfig>,
) -> Self {
Self {
contract,
project,
is_system_mode,
include_metadata_hash,
enable_test_encoding,
optimizer_settings,
debug_config,
}
}
}
+108
View File
@@ -0,0 +1,108 @@
//!
//! Process for compiling a single compilation unit.
//!
pub mod input;
pub mod output;
use std::io::Read;
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
use once_cell::sync::OnceCell;
use self::input::Input;
use self::output::Output;
/// The overriden executable name used when the compiler is run as a library.
pub static EXECUTABLE: OnceCell<PathBuf> = OnceCell::new();
///
/// Read input from `stdin`, compile a contract, and write the output to `stdout`.
///
pub fn run() -> anyhow::Result<()> {
let mut stdin = std::io::stdin();
let mut stdout = std::io::stdout();
let mut stderr = std::io::stderr();
let mut buffer = Vec::with_capacity(16384);
stdin.read_to_end(&mut buffer).expect("Stdin reading error");
let input: Input = era_compiler_common::deserialize_from_slice(buffer.as_slice())?;
if input.enable_test_encoding {
todo!()
}
let result = input.contract.compile(
input.project,
input.optimizer_settings,
input.is_system_mode,
input.include_metadata_hash,
input.debug_config,
);
match result {
Ok(build) => {
let output = Output::new(build);
let json = serde_json::to_vec(&output).expect("Always valid");
stdout
.write_all(json.as_slice())
.expect("Stdout writing error");
Ok(())
}
Err(error) => {
let message = error.to_string();
stderr
.write_all(message.as_bytes())
.expect("Stderr writing error");
Err(error)
}
}
}
///
/// Runs this process recursively to compile a single contract.
///
pub fn call(input: Input) -> anyhow::Result<Output> {
let input_json = serde_json::to_vec(&input).expect("Always valid");
let executable = match EXECUTABLE.get() {
Some(executable) => executable.to_owned(),
None => std::env::current_exe()?,
};
let mut command = Command::new(executable.as_path());
command.stdin(std::process::Stdio::piped());
command.stdout(std::process::Stdio::piped());
command.stderr(std::process::Stdio::piped());
command.arg("--recursive-process");
let process = command.spawn().map_err(|error| {
anyhow::anyhow!("{:?} subprocess spawning error: {:?}", executable, error)
})?;
process
.stdin
.as_ref()
.ok_or_else(|| anyhow::anyhow!("{:?} stdin getting error", executable))?
.write_all(input_json.as_slice())
.map_err(|error| anyhow::anyhow!("{:?} stdin writing error: {:?}", executable, error))?;
let output = process.wait_with_output().map_err(|error| {
anyhow::anyhow!("{:?} subprocess output error: {:?}", executable, error)
})?;
if !output.status.success() {
anyhow::bail!(
"{}",
String::from_utf8_lossy(output.stderr.as_slice()).to_string(),
);
}
let output: Output = era_compiler_common::deserialize_from_slice(output.stdout.as_slice())
.map_err(|error| {
anyhow::anyhow!(
"{:?} subprocess output parsing error: {}",
executable,
error,
)
})?;
Ok(output)
}
+28
View File
@@ -0,0 +1,28 @@
//!
//! Process for compiling a single compilation unit.
//!
//! The output data.
//!
use serde::Deserialize;
use serde::Serialize;
use crate::build::contract::Contract as ContractBuild;
///
/// The output data.
///
#[derive(Debug, Serialize, Deserialize)]
pub struct Output {
/// The contract build.
pub build: ContractBuild,
}
impl Output {
///
/// A shortcut constructor.
///
pub fn new(build: ContractBuild) -> Self {
Self { build }
}
}
@@ -0,0 +1,58 @@
//!
//! The contract EVM legacy assembly source code.
//!
use std::collections::HashSet;
use serde::Deserialize;
use serde::Serialize;
use crate::evmla::assembly::Assembly;
use crate::solc::standard_json::output::contract::evm::extra_metadata::ExtraMetadata;
///
/// The contract EVM legacy assembly source code.
///
#[derive(Debug, Serialize, Deserialize, Clone)]
#[allow(non_camel_case_types)]
#[allow(clippy::upper_case_acronyms)]
pub struct EVMLA {
/// The EVM legacy assembly source code.
pub assembly: Assembly,
}
impl EVMLA {
///
/// A shortcut constructor.
///
pub fn new(mut assembly: Assembly, extra_metadata: ExtraMetadata) -> Self {
assembly.extra_metadata = Some(extra_metadata);
Self { assembly }
}
///
/// Get the list of missing deployable libraries.
///
pub fn get_missing_libraries(&self) -> HashSet<String> {
self.assembly.get_missing_libraries()
}
}
impl<D> era_compiler_llvm_context::EraVMWriteLLVM<D> for EVMLA
where
D: era_compiler_llvm_context::EraVMDependency + Clone,
{
fn declare(
&mut self,
context: &mut era_compiler_llvm_context::EraVMContext<D>,
) -> anyhow::Result<()> {
self.assembly.declare(context)
}
fn into_llvm(
self,
context: &mut era_compiler_llvm_context::EraVMContext<D>,
) -> anyhow::Result<()> {
self.assembly.into_llvm(context)
}
}
@@ -0,0 +1,27 @@
//!
//! The contract LLVM IR source code.
//!
use serde::Deserialize;
use serde::Serialize;
///
/// The contract LLVM IR source code.
///
#[derive(Debug, Serialize, Deserialize, Clone)]
#[allow(clippy::upper_case_acronyms)]
pub struct LLVMIR {
/// The LLVM IR file path.
pub path: String,
/// The LLVM IR source code.
pub source: String,
}
impl LLVMIR {
///
/// A shortcut constructor.
///
pub fn new(path: String, source: String) -> Self {
Self { path, source }
}
}
@@ -0,0 +1,111 @@
//!
//! The contract source code.
//!
pub mod evmla;
pub mod llvm_ir;
pub mod yul;
pub mod zkasm;
use std::collections::HashSet;
use serde::Deserialize;
use serde::Serialize;
use crate::evmla::assembly::Assembly;
use crate::solc::standard_json::output::contract::evm::extra_metadata::ExtraMetadata;
use crate::yul::parser::statement::object::Object;
use self::evmla::EVMLA;
use self::llvm_ir::LLVMIR;
use self::yul::Yul;
use self::zkasm::ZKASM;
///
/// The contract source code.
///
#[derive(Debug, Serialize, Deserialize, Clone)]
#[allow(non_camel_case_types)]
#[allow(clippy::upper_case_acronyms)]
#[allow(clippy::enum_variant_names)]
pub enum IR {
/// The Yul source code.
Yul(Yul),
/// The EVM legacy assembly source code.
EVMLA(EVMLA),
/// The LLVM IR source code.
LLVMIR(LLVMIR),
/// The EraVM assembly source code.
ZKASM(ZKASM),
}
impl IR {
///
/// A shortcut constructor.
///
pub fn new_yul(source_code: String, object: Object) -> Self {
Self::Yul(Yul::new(source_code, object))
}
///
/// A shortcut constructor.
///
pub fn new_evmla(assembly: Assembly, extra_metadata: ExtraMetadata) -> Self {
Self::EVMLA(EVMLA::new(assembly, extra_metadata))
}
///
/// A shortcut constructor.
///
pub fn new_llvm_ir(path: String, source: String) -> Self {
Self::LLVMIR(LLVMIR::new(path, source))
}
///
/// A shortcut constructor.
///
pub fn new_zkasm(path: String, source: String) -> Self {
Self::ZKASM(ZKASM::new(path, source))
}
///
/// Get the list of missing deployable libraries.
///
pub fn get_missing_libraries(&self) -> HashSet<String> {
match self {
Self::Yul(inner) => inner.get_missing_libraries(),
Self::EVMLA(inner) => inner.get_missing_libraries(),
Self::LLVMIR(_inner) => HashSet::new(),
Self::ZKASM(_inner) => HashSet::new(),
}
}
}
impl<D> era_compiler_llvm_context::EraVMWriteLLVM<D> for IR
where
D: era_compiler_llvm_context::EraVMDependency + Clone,
{
fn declare(
&mut self,
context: &mut era_compiler_llvm_context::EraVMContext<D>,
) -> anyhow::Result<()> {
match self {
Self::Yul(inner) => inner.declare(context),
Self::EVMLA(inner) => inner.declare(context),
Self::LLVMIR(_inner) => Ok(()),
Self::ZKASM(_inner) => Ok(()),
}
}
fn into_llvm(
self,
context: &mut era_compiler_llvm_context::EraVMContext<D>,
) -> anyhow::Result<()> {
match self {
Self::Yul(inner) => inner.into_llvm(context),
Self::EVMLA(inner) => inner.into_llvm(context),
Self::LLVMIR(_inner) => Ok(()),
Self::ZKASM(_inner) => Ok(()),
}
}
}
@@ -0,0 +1,59 @@
//!
//! The contract Yul source code.
//!
use std::collections::HashSet;
use serde::Deserialize;
use serde::Serialize;
use crate::yul::parser::statement::object::Object;
///
/// The contract Yul source code.
///
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Yul {
/// The Yul source code.
pub source_code: String,
/// The Yul AST object.
pub object: Object,
}
impl Yul {
///
/// A shortcut constructor.
///
pub fn new(source_code: String, object: Object) -> Self {
Self {
source_code,
object,
}
}
///
/// Get the list of missing deployable libraries.
///
pub fn get_missing_libraries(&self) -> HashSet<String> {
self.object.get_missing_libraries()
}
}
impl<D> era_compiler_llvm_context::EraVMWriteLLVM<D> for Yul
where
D: era_compiler_llvm_context::EraVMDependency + Clone,
{
fn declare(
&mut self,
context: &mut era_compiler_llvm_context::EraVMContext<D>,
) -> anyhow::Result<()> {
self.object.declare(context)
}
fn into_llvm(
self,
context: &mut era_compiler_llvm_context::EraVMContext<D>,
) -> anyhow::Result<()> {
self.object.into_llvm(context)
}
}
@@ -0,0 +1,27 @@
//!
//! The contract EraVM assembly source code.
//!
use serde::Deserialize;
use serde::Serialize;
///
/// The contract EraVM assembly source code.
///
#[derive(Debug, Serialize, Deserialize, Clone)]
#[allow(clippy::upper_case_acronyms)]
pub struct ZKASM {
/// The EraVM assembly file path.
pub path: String,
/// The EraVM assembly source code.
pub source: String,
}
impl ZKASM {
///
/// A shortcut constructor.
///
pub fn new(path: String, source: String) -> Self {
Self { path, source }
}
}
@@ -0,0 +1,45 @@
//!
//! The Solidity contract metadata.
//!
use serde::Serialize;
///
/// The Solidity contract metadata.
///
/// Is used to append the metadata hash to the contract bytecode.
///
#[derive(Debug, Serialize)]
pub struct Metadata {
/// The `solc` metadata.
pub solc_metadata: serde_json::Value,
/// The `solc` version.
pub solc_version: semver::Version,
/// The zkVM `solc` edition.
pub solc_zkvm_edition: Option<semver::Version>,
/// The EraVM compiler version.
pub zk_version: semver::Version,
/// The EraVM compiler optimizer settings.
pub optimizer_settings: era_compiler_llvm_context::OptimizerSettings,
}
impl Metadata {
///
/// A shortcut constructor.
///
pub fn new(
solc_metadata: serde_json::Value,
solc_version: semver::Version,
solc_zkvm_edition: Option<semver::Version>,
zk_version: semver::Version,
optimizer_settings: era_compiler_llvm_context::OptimizerSettings,
) -> Self {
Self {
solc_metadata,
solc_version,
solc_zkvm_edition,
zk_version,
optimizer_settings,
}
}
}
+224
View File
@@ -0,0 +1,224 @@
//!
//! The contract data.
//!
pub mod ir;
pub mod metadata;
use std::collections::HashSet;
use serde::Deserialize;
use serde::Serialize;
use sha3::Digest;
use era_compiler_llvm_context::EraVMWriteLLVM;
use crate::build::contract::Contract as ContractBuild;
use crate::project::Project;
use crate::solc::version::Version as SolcVersion;
use self::ir::IR;
use self::metadata::Metadata;
///
/// The contract data.
///
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Contract {
/// The absolute file path.
pub path: String,
/// The IR source code data.
pub ir: IR,
/// The metadata JSON.
pub metadata_json: serde_json::Value,
}
impl Contract {
///
/// A shortcut constructor.
///
pub fn new(
path: String,
source_hash: [u8; era_compiler_common::BYTE_LENGTH_FIELD],
source_version: SolcVersion,
ir: IR,
metadata_json: Option<serde_json::Value>,
) -> Self {
let metadata_json = metadata_json.unwrap_or_else(|| {
serde_json::json!({
"source_hash": hex::encode(source_hash.as_slice()),
"source_version": serde_json::to_value(&source_version).expect("Always valid"),
})
});
Self {
path,
ir,
metadata_json,
}
}
///
/// Returns the contract identifier, which is:
/// - the Yul object identifier for Yul
/// - the full contract path for EVM legacy assembly
/// - the module name for LLVM IR
///
pub fn identifier(&self) -> &str {
match self.ir {
IR::Yul(ref yul) => yul.object.identifier.as_str(),
IR::EVMLA(ref evm) => evm.assembly.full_path(),
IR::LLVMIR(ref llvm_ir) => llvm_ir.path.as_str(),
IR::ZKASM(ref zkasm) => zkasm.path.as_str(),
}
}
///
/// Extract factory dependencies.
///
pub fn drain_factory_dependencies(&mut self) -> HashSet<String> {
match self.ir {
IR::Yul(ref mut yul) => yul.object.factory_dependencies.drain().collect(),
IR::EVMLA(ref mut evm) => evm.assembly.factory_dependencies.drain().collect(),
IR::LLVMIR(_) => HashSet::new(),
IR::ZKASM(_) => HashSet::new(),
}
}
///
/// Compiles the specified contract, setting its build artifacts.
///
pub fn compile(
mut self,
project: Project,
optimizer_settings: era_compiler_llvm_context::OptimizerSettings,
is_system_mode: bool,
include_metadata_hash: bool,
debug_config: Option<era_compiler_llvm_context::DebugConfig>,
) -> anyhow::Result<ContractBuild> {
let llvm = inkwell::context::Context::create();
let optimizer = era_compiler_llvm_context::Optimizer::new(optimizer_settings);
let version = project.version.clone();
let identifier = self.identifier().to_owned();
let metadata = Metadata::new(
self.metadata_json.take(),
version.default.clone(),
version.l2_revision.clone(),
semver::Version::parse(env!("CARGO_PKG_VERSION")).expect("Always valid"),
optimizer.settings().to_owned(),
);
let metadata_json = serde_json::to_value(&metadata).expect("Always valid");
let metadata_hash: Option<[u8; era_compiler_common::BYTE_LENGTH_FIELD]> =
if include_metadata_hash {
let metadata_string = serde_json::to_string(&metadata).expect("Always valid");
Some(sha3::Keccak256::digest(metadata_string.as_bytes()).into())
} else {
None
};
let module = match self.ir {
IR::LLVMIR(ref llvm_ir) => {
let memory_buffer =
inkwell::memory_buffer::MemoryBuffer::create_from_memory_range_copy(
llvm_ir.source.as_bytes(),
self.path.as_str(),
);
llvm.create_module_from_ir(memory_buffer)
.map_err(|error| anyhow::anyhow!(error.to_string()))?
}
IR::ZKASM(ref zkasm) => {
let build = era_compiler_llvm_context::eravm_build_assembly_text(
self.path.as_str(),
zkasm.source.as_str(),
metadata_hash,
debug_config.as_ref(),
)?;
return Ok(ContractBuild::new(
self.path,
identifier,
build,
metadata_json,
HashSet::new(),
));
}
_ => llvm.create_module(self.path.as_str()),
};
let mut context = era_compiler_llvm_context::EraVMContext::new(
&llvm,
module,
optimizer,
Some(project),
include_metadata_hash,
debug_config,
);
context.set_solidity_data(era_compiler_llvm_context::EraVMContextSolidityData::default());
match self.ir {
IR::Yul(_) => {
let yul_data = era_compiler_llvm_context::EraVMContextYulData::new(is_system_mode);
context.set_yul_data(yul_data);
}
IR::EVMLA(_) => {
let evmla_data =
era_compiler_llvm_context::EraVMContextEVMLAData::new(version.default);
context.set_evmla_data(evmla_data);
}
IR::LLVMIR(_) => {}
IR::ZKASM(_) => {}
}
let factory_dependencies = self.drain_factory_dependencies();
self.ir.declare(&mut context).map_err(|error| {
anyhow::anyhow!(
"The contract `{}` LLVM IR generator declaration pass error: {}",
self.path,
error
)
})?;
self.ir.into_llvm(&mut context).map_err(|error| {
anyhow::anyhow!(
"The contract `{}` LLVM IR generator definition pass error: {}",
self.path,
error
)
})?;
let build = context.build(self.path.as_str(), metadata_hash)?;
Ok(ContractBuild::new(
self.path,
identifier,
build,
metadata_json,
factory_dependencies,
))
}
///
/// Get the list of missing deployable libraries.
///
pub fn get_missing_libraries(&self) -> HashSet<String> {
self.ir.get_missing_libraries()
}
}
impl<D> EraVMWriteLLVM<D> for Contract
where
D: era_compiler_llvm_context::EraVMDependency + Clone,
{
fn declare(
&mut self,
context: &mut era_compiler_llvm_context::EraVMContext<D>,
) -> anyhow::Result<()> {
self.ir.declare(context)
}
fn into_llvm(
self,
context: &mut era_compiler_llvm_context::EraVMContext<D>,
) -> anyhow::Result<()> {
self.ir.into_llvm(context)
}
}
+351
View File
@@ -0,0 +1,351 @@
//!
//! The processed input data.
//!
pub mod contract;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::Path;
use rayon::iter::IntoParallelIterator;
use rayon::iter::ParallelIterator;
use serde::Deserialize;
use serde::Serialize;
use sha3::Digest;
use crate::build::contract::Contract as ContractBuild;
use crate::build::Build;
use crate::missing_libraries::MissingLibraries;
use crate::process::input::Input as ProcessInput;
use crate::project::contract::ir::IR;
use crate::solc::version::Version as SolcVersion;
use crate::solc::Compiler as SolcCompiler;
use crate::yul::lexer::Lexer;
use crate::yul::parser::statement::object::Object;
use self::contract::Contract;
///
/// The processes input data.
///
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Project {
/// The source code version.
pub version: SolcVersion,
/// The project contracts,
pub contracts: BTreeMap<String, Contract>,
/// The mapping of auxiliary identifiers, e.g. Yul object names, to full contract paths.
pub identifier_paths: BTreeMap<String, String>,
/// The library addresses.
pub libraries: BTreeMap<String, BTreeMap<String, String>>,
}
impl Project {
///
/// A shortcut constructor.
///
pub fn new(
version: SolcVersion,
contracts: BTreeMap<String, Contract>,
libraries: BTreeMap<String, BTreeMap<String, String>>,
) -> Self {
let mut identifier_paths = BTreeMap::new();
for (path, contract) in contracts.iter() {
identifier_paths.insert(contract.identifier().to_owned(), path.to_owned());
}
Self {
version,
contracts,
identifier_paths,
libraries,
}
}
///
/// Compiles all contracts, returning their build artifacts.
///
pub fn compile(
self,
optimizer_settings: era_compiler_llvm_context::OptimizerSettings,
is_system_mode: bool,
include_metadata_hash: bool,
bytecode_encoding_testing: bool,
debug_config: Option<era_compiler_llvm_context::DebugConfig>,
) -> anyhow::Result<Build> {
let project = self.clone();
let results: BTreeMap<String, anyhow::Result<ContractBuild>> = self
.contracts
.into_par_iter()
.map(|(full_path, contract)| {
let process_output = crate::process::call(ProcessInput::new(
contract,
project.clone(),
is_system_mode,
include_metadata_hash,
bytecode_encoding_testing,
optimizer_settings.clone(),
debug_config.clone(),
));
(full_path, process_output.map(|output| output.build))
})
.collect();
let mut build = Build::default();
let mut hashes = HashMap::with_capacity(results.len());
for (path, result) in results.iter() {
match result {
Ok(contract) => {
hashes.insert(path.to_owned(), contract.build.bytecode_hash.to_owned());
}
Err(error) => {
anyhow::bail!("Contract `{}` compiling error: {:?}", path, error);
}
}
}
for (path, result) in results.into_iter() {
match result {
Ok(mut contract) => {
for dependency in contract.factory_dependencies.drain() {
let dependency_path = project
.identifier_paths
.get(dependency.as_str())
.cloned()
.unwrap_or_else(|| {
panic!("Dependency `{dependency}` full path not found")
});
let hash = match hashes.get(dependency_path.as_str()) {
Some(hash) => hash.to_owned(),
None => anyhow::bail!(
"Dependency contract `{}` not found in the project",
dependency_path
),
};
contract
.build
.factory_dependencies
.insert(hash, dependency_path);
}
build.contracts.insert(path, contract);
}
Err(error) => {
anyhow::bail!("Contract `{}` compiling error: {:?}", path, error);
}
}
}
Ok(build)
}
///
/// Get the list of missing deployable libraries.
///
pub fn get_missing_libraries(&self) -> MissingLibraries {
let deployed_libraries = self
.libraries
.iter()
.flat_map(|(file, names)| {
names
.iter()
.map(|(name, _address)| format!("{file}:{name}"))
.collect::<HashSet<String>>()
})
.collect::<HashSet<String>>();
let mut missing_deployable_libraries = BTreeMap::new();
for (contract_path, contract) in self.contracts.iter() {
let missing_libraries = contract
.get_missing_libraries()
.into_iter()
.filter(|library| !deployed_libraries.contains(library))
.collect::<HashSet<String>>();
missing_deployable_libraries.insert(contract_path.to_owned(), missing_libraries);
}
MissingLibraries::new(missing_deployable_libraries)
}
///
/// Parses the Yul source code file and returns the source data.
///
pub fn try_from_yul_path(
path: &Path,
solc_validator: Option<&SolcCompiler>,
) -> anyhow::Result<Self> {
let source_code = std::fs::read_to_string(path)
.map_err(|error| anyhow::anyhow!("Yul file {:?} reading error: {}", path, error))?;
Self::try_from_yul_string(path, source_code.as_str(), solc_validator)
}
///
/// Parses the test Yul source code string and returns the source data.
///
/// Only for integration testing purposes.
///
pub fn try_from_yul_string(
path: &Path,
source_code: &str,
solc_validator: Option<&SolcCompiler>,
) -> anyhow::Result<Self> {
if let Some(solc) = solc_validator {
solc.validate_yul(path)?;
}
let source_version = SolcVersion::new_simple(SolcCompiler::LAST_SUPPORTED_VERSION);
let path = path.to_string_lossy().to_string();
let source_hash = sha3::Keccak256::digest(source_code.as_bytes()).into();
let mut lexer = Lexer::new(source_code.to_owned());
let object = Object::parse(&mut lexer, None)
.map_err(|error| anyhow::anyhow!("Yul object `{}` parsing error: {}", path, error))?;
let mut project_contracts = BTreeMap::new();
project_contracts.insert(
path.to_owned(),
Contract::new(
path,
source_hash,
source_version.clone(),
IR::new_yul(source_code.to_owned(), object),
None,
),
);
Ok(Self::new(
source_version,
project_contracts,
BTreeMap::new(),
))
}
///
/// Parses the LLVM IR source code file and returns the source data.
///
pub fn try_from_llvm_ir_path(path: &Path) -> anyhow::Result<Self> {
let source_code = std::fs::read_to_string(path)
.map_err(|error| anyhow::anyhow!("LLVM IR file {:?} reading error: {}", path, error))?;
let source_hash = sha3::Keccak256::digest(source_code.as_bytes()).into();
let source_version =
SolcVersion::new_simple(era_compiler_llvm_context::eravm_const::LLVM_VERSION);
let path = path.to_string_lossy().to_string();
let mut project_contracts = BTreeMap::new();
project_contracts.insert(
path.clone(),
Contract::new(
path.clone(),
source_hash,
source_version.clone(),
IR::new_llvm_ir(path, source_code),
None,
),
);
Ok(Self::new(
source_version,
project_contracts,
BTreeMap::new(),
))
}
///
/// Parses the EraVM assembly source code file and returns the source data.
///
pub fn try_from_zkasm_path(path: &Path) -> anyhow::Result<Self> {
let source_code = std::fs::read_to_string(path).map_err(|error| {
anyhow::anyhow!("EraVM assembly file {:?} reading error: {}", path, error)
})?;
let source_hash = sha3::Keccak256::digest(source_code.as_bytes()).into();
let source_version =
SolcVersion::new_simple(era_compiler_llvm_context::eravm_const::ZKEVM_VERSION);
let path = path.to_string_lossy().to_string();
let mut project_contracts = BTreeMap::new();
project_contracts.insert(
path.clone(),
Contract::new(
path.clone(),
source_hash,
source_version.clone(),
IR::new_zkasm(path, source_code),
None,
),
);
Ok(Self::new(
source_version,
project_contracts,
BTreeMap::new(),
))
}
}
impl era_compiler_llvm_context::EraVMDependency for Project {
fn compile(
project: Self,
identifier: &str,
optimizer_settings: era_compiler_llvm_context::OptimizerSettings,
is_system_mode: bool,
include_metadata_hash: bool,
debug_config: Option<era_compiler_llvm_context::DebugConfig>,
) -> anyhow::Result<String> {
let contract_path = project.resolve_path(identifier)?;
let contract = project
.contracts
.get(contract_path.as_str())
.cloned()
.ok_or_else(|| {
anyhow::anyhow!(
"Dependency contract `{}` not found in the project",
contract_path
)
})?;
contract
.compile(
project,
optimizer_settings,
is_system_mode,
include_metadata_hash,
debug_config,
)
.map_err(|error| {
anyhow::anyhow!(
"Dependency contract `{}` compiling error: {}",
identifier,
error
)
})
.map(|contract| contract.build.bytecode_hash)
}
fn resolve_path(&self, identifier: &str) -> anyhow::Result<String> {
self.identifier_paths
.get(identifier.strip_suffix("_deployed").unwrap_or(identifier))
.cloned()
.ok_or_else(|| {
anyhow::anyhow!(
"Contract with identifier `{}` not found in the project",
identifier
)
})
}
fn resolve_library(&self, path: &str) -> anyhow::Result<String> {
for (file_path, contracts) in self.libraries.iter() {
for (contract_name, address) in contracts.iter() {
let key = format!("{file_path}:{contract_name}");
if key.as_str() == path {
return Ok(address["0x".len()..].to_owned());
}
}
}
anyhow::bail!("Library `{}` not found in the project", path);
}
}
@@ -0,0 +1,79 @@
//!
//! The `solc --combined-json` contract.
//!
use std::collections::BTreeMap;
use std::collections::HashSet;
use serde::Deserialize;
use serde::Serialize;
///
/// The contract.
///
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Contract {
/// The `solc` hashes output.
#[serde(skip_serializing_if = "Option::is_none")]
pub hashes: Option<BTreeMap<String, String>>,
/// The `solc` ABI output.
#[serde(skip_serializing_if = "Option::is_none")]
pub abi: Option<serde_json::Value>,
/// The `solc` metadata output.
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<String>,
/// The `solc` developer documentation output.
#[serde(skip_serializing_if = "Option::is_none")]
pub devdoc: Option<serde_json::Value>,
/// The `solc` user documentation output.
#[serde(skip_serializing_if = "Option::is_none")]
pub userdoc: Option<serde_json::Value>,
/// The `solc` storage layout output.
#[serde(skip_serializing_if = "Option::is_none")]
pub storage_layout: Option<serde_json::Value>,
/// The `solc` AST output.
#[serde(skip_serializing_if = "Option::is_none")]
pub ast: Option<serde_json::Value>,
/// The `solc` assembly output.
#[serde(skip_serializing_if = "Option::is_none")]
pub asm: Option<serde_json::Value>,
/// The `solc` hexadecimal binary output.
#[serde(skip_serializing_if = "Option::is_none")]
pub bin: Option<String>,
/// The `solc` hexadecimal binary runtime part output.
#[serde(skip_serializing_if = "Option::is_none")]
pub bin_runtime: Option<String>,
/// The factory dependencies.
#[serde(skip_serializing_if = "Option::is_none")]
pub factory_deps: Option<BTreeMap<String, String>>,
/// The missing libraries.
#[serde(skip_serializing_if = "Option::is_none")]
pub missing_libraries: Option<HashSet<String>>,
}
impl Contract {
///
/// Returns the signature hash of the specified contract entry.
///
/// # Panics
/// If the hashes have not been requested in the `solc` call.
///
pub fn entry(&self, entry: &str) -> u32 {
self.hashes
.as_ref()
.expect("Always exists")
.iter()
.find_map(|(contract_entry, hash)| {
if contract_entry.starts_with(entry) {
Some(
u32::from_str_radix(hash.as_str(), era_compiler_common::BASE_HEXADECIMAL)
.expect("Test hash is always valid"),
)
} else {
None
}
})
.unwrap_or_else(|| panic!("Entry `{entry}` not found"))
}
}
@@ -0,0 +1,108 @@
//!
//! The `solc --combined-json` output.
//!
pub mod contract;
use std::collections::BTreeMap;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use serde::Deserialize;
use serde::Serialize;
use self::contract::Contract;
///
/// The `solc --combined-json` output.
///
#[derive(Debug, Serialize, Deserialize)]
pub struct CombinedJson {
/// The contract entries.
pub contracts: BTreeMap<String, Contract>,
/// The list of source files.
#[serde(rename = "sourceList")]
#[serde(skip_serializing_if = "Option::is_none")]
pub source_list: Option<Vec<String>>,
/// The source code extra data, including the AST.
#[serde(skip_serializing_if = "Option::is_none")]
pub sources: Option<serde_json::Value>,
/// The `solc` compiler version.
pub version: String,
/// The `zksolc` compiler version.
#[serde(skip_serializing_if = "Option::is_none")]
pub zk_version: Option<String>,
}
impl CombinedJson {
///
/// Returns the signature hash of the specified contract and entry.
///
pub fn entry(&self, path: &str, entry: &str) -> u32 {
self.contracts
.iter()
.find_map(|(name, contract)| {
if name.starts_with(path) {
Some(contract)
} else {
None
}
})
.expect("Always exists")
.entry(entry)
}
///
/// Returns the full contract path which can be found in `combined-json` output.
///
pub fn get_full_path(&self, name: &str) -> Option<String> {
self.contracts.iter().find_map(|(path, _value)| {
if let Some(last_slash_position) = path.rfind('/') {
if let Some(colon_position) = path.rfind(':') {
if &path[last_slash_position + 1..colon_position] == name {
return Some(path.to_owned());
}
}
}
None
})
}
///
/// Removes EVM artifacts to prevent their accidental usage.
///
pub fn remove_evm(&mut self) {
for (_, contract) in self.contracts.iter_mut() {
contract.bin = None;
contract.bin_runtime = None;
}
}
///
/// Writes the JSON to the specified directory.
///
pub fn write_to_directory(
self,
output_directory: &Path,
overwrite: bool,
) -> anyhow::Result<()> {
let mut file_path = output_directory.to_owned();
file_path.push(format!("combined.{}", era_compiler_common::EXTENSION_JSON));
if file_path.exists() && !overwrite {
eprintln!(
"Refusing to overwrite an existing file {file_path:?} (use --overwrite to force)."
);
return Ok(());
}
File::create(&file_path)
.map_err(|error| anyhow::anyhow!("File {:?} creating error: {}", file_path, error))?
.write_all(serde_json::to_vec(&self).expect("Always valid").as_slice())
.map_err(|error| anyhow::anyhow!("File {:?} writing error: {}", file_path, error))?;
Ok(())
}
}
+315
View File
@@ -0,0 +1,315 @@
//!
//! The Solidity compiler.
//!
pub mod combined_json;
pub mod pipeline;
pub mod standard_json;
pub mod version;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use self::combined_json::CombinedJson;
use self::pipeline::Pipeline;
use self::standard_json::input::Input as StandardJsonInput;
use self::standard_json::output::Output as StandardJsonOutput;
use self::version::Version;
///
/// The Solidity compiler.
///
pub struct Compiler {
/// The binary executable name.
pub executable: String,
/// The lazily-initialized compiler version.
pub version: Option<Version>,
}
impl Compiler {
/// The default executable name.
pub const DEFAULT_EXECUTABLE_NAME: &'static str = "solc";
/// The first version of `solc` with the support of standard JSON interface.
pub const FIRST_SUPPORTED_VERSION: semver::Version = semver::Version::new(0, 4, 12);
/// The first version of `solc`, where Yul codegen is considered robust enough.
pub const FIRST_YUL_VERSION: semver::Version = semver::Version::new(0, 8, 0);
/// The first version of `solc`, where `--via-ir` codegen mode is supported.
pub const FIRST_VIA_IR_VERSION: semver::Version = semver::Version::new(0, 8, 13);
/// The last supported version of `solc`.
pub const LAST_SUPPORTED_VERSION: semver::Version = semver::Version::new(0, 8, 24);
///
/// A shortcut constructor.
///
/// Different tools may use different `executable` names. For example, the integration tester
/// uses `solc-<version>` format.
///
pub fn new(executable: String) -> anyhow::Result<Self> {
if let Err(error) = which::which(executable.as_str()) {
anyhow::bail!(
"The `{executable}` executable not found in ${{PATH}}: {}",
error
);
}
Ok(Self {
executable,
version: None,
})
}
///
/// Compiles the Solidity `--standard-json` input into Yul IR.
///
pub fn standard_json(
&mut self,
mut input: StandardJsonInput,
pipeline: Pipeline,
base_path: Option<String>,
include_paths: Vec<String>,
allow_paths: Option<String>,
) -> anyhow::Result<StandardJsonOutput> {
let version = self.version()?;
let mut command = std::process::Command::new(self.executable.as_str());
command.stdin(std::process::Stdio::piped());
command.stdout(std::process::Stdio::piped());
command.arg("--standard-json");
if let Some(base_path) = base_path {
command.arg("--base-path");
command.arg(base_path);
}
for include_path in include_paths.into_iter() {
command.arg("--include-path");
command.arg(include_path);
}
if let Some(allow_paths) = allow_paths {
command.arg("--allow-paths");
command.arg(allow_paths);
}
input.normalize(&version.default);
let suppressed_warnings = input.suppressed_warnings.take().unwrap_or_default();
let input_json = serde_json::to_vec(&input).expect("Always valid");
let process = command.spawn().map_err(|error| {
anyhow::anyhow!("{} subprocess spawning error: {:?}", self.executable, error)
})?;
process
.stdin
.as_ref()
.ok_or_else(|| anyhow::anyhow!("{} stdin getting error", self.executable))?
.write_all(input_json.as_slice())
.map_err(|error| {
anyhow::anyhow!("{} stdin writing error: {:?}", self.executable, error)
})?;
let output = process.wait_with_output().map_err(|error| {
anyhow::anyhow!("{} subprocess output error: {:?}", self.executable, error)
})?;
if !output.status.success() {
anyhow::bail!(
"{} error: {}",
self.executable,
String::from_utf8_lossy(output.stderr.as_slice()).to_string()
);
}
let mut output: StandardJsonOutput = era_compiler_common::deserialize_from_slice(
output.stdout.as_slice(),
)
.map_err(|error| {
anyhow::anyhow!(
"{} subprocess output parsing error: {}\n{}",
self.executable,
error,
era_compiler_common::deserialize_from_slice::<serde_json::Value>(
output.stdout.as_slice()
)
.map(|json| serde_json::to_string_pretty(&json).expect("Always valid"))
.unwrap_or_else(|_| String::from_utf8_lossy(output.stdout.as_slice()).to_string()),
)
})?;
output.preprocess_ast(&version, pipeline, suppressed_warnings.as_slice())?;
output.remove_evm();
Ok(output)
}
///
/// The `solc --combined-json abi,hashes...` mirror.
///
pub fn combined_json(
&self,
paths: &[PathBuf],
combined_json_argument: &str,
) -> anyhow::Result<CombinedJson> {
let mut command = std::process::Command::new(self.executable.as_str());
command.args(paths);
let mut combined_json_flags = Vec::new();
let mut combined_json_fake_flag_pushed = false;
let mut filtered_flags = Vec::with_capacity(3);
for flag in combined_json_argument.split(',') {
match flag {
flag @ "asm" | flag @ "bin" | flag @ "bin-runtime" => filtered_flags.push(flag),
flag => combined_json_flags.push(flag),
}
}
if combined_json_flags.is_empty() {
combined_json_flags.push("ast");
combined_json_fake_flag_pushed = true;
}
command.arg("--combined-json");
command.arg(combined_json_flags.join(","));
let output = command.output().map_err(|error| {
anyhow::anyhow!("{} subprocess error: {:?}", self.executable, error)
})?;
if !output.status.success() {
println!("{}", String::from_utf8_lossy(output.stdout.as_slice()));
println!("{}", String::from_utf8_lossy(output.stderr.as_slice()));
anyhow::bail!(
"{} error: {}",
self.executable,
String::from_utf8_lossy(output.stdout.as_slice()).to_string()
);
}
let mut combined_json: CombinedJson = era_compiler_common::deserialize_from_slice(
output.stdout.as_slice(),
)
.map_err(|error| {
anyhow::anyhow!(
"{} subprocess output parsing error: {}\n{}",
self.executable,
error,
era_compiler_common::deserialize_from_slice::<serde_json::Value>(
output.stdout.as_slice()
)
.map(|json| serde_json::to_string_pretty(&json).expect("Always valid"))
.unwrap_or_else(|_| String::from_utf8_lossy(output.stdout.as_slice()).to_string()),
)
})?;
for filtered_flag in filtered_flags.into_iter() {
for (_path, contract) in combined_json.contracts.iter_mut() {
match filtered_flag {
"asm" => contract.asm = Some(serde_json::Value::Null),
"bin" => contract.bin = Some("".to_owned()),
"bin-runtime" => contract.bin_runtime = Some("".to_owned()),
_ => continue,
}
}
}
if combined_json_fake_flag_pushed {
combined_json.source_list = None;
combined_json.sources = None;
}
combined_json.remove_evm();
Ok(combined_json)
}
///
/// The `solc` Yul validator.
///
pub fn validate_yul(&self, path: &Path) -> anyhow::Result<()> {
let mut command = std::process::Command::new(self.executable.as_str());
command.arg("--strict-assembly");
command.arg(path);
let output = command.output().map_err(|error| {
anyhow::anyhow!("{} subprocess error: {:?}", self.executable, error)
})?;
if !output.status.success() {
anyhow::bail!(
"{} error: {}",
self.executable,
String::from_utf8_lossy(output.stderr.as_slice()).to_string()
);
}
Ok(())
}
///
/// The `solc --version` mini-parser.
///
pub fn version(&mut self) -> anyhow::Result<Version> {
if let Some(version) = self.version.as_ref() {
return Ok(version.to_owned());
}
let mut command = std::process::Command::new(self.executable.as_str());
command.arg("--version");
let output = command.output().map_err(|error| {
anyhow::anyhow!("{} subprocess error: {:?}", self.executable, error)
})?;
if !output.status.success() {
anyhow::bail!(
"{} error: {}",
self.executable,
String::from_utf8_lossy(output.stderr.as_slice()).to_string()
);
}
let stdout = String::from_utf8_lossy(output.stdout.as_slice());
let long = stdout
.lines()
.nth(1)
.ok_or_else(|| {
anyhow::anyhow!("{} version parsing: not enough lines", self.executable)
})?
.split(' ')
.nth(1)
.ok_or_else(|| {
anyhow::anyhow!(
"{} version parsing: not enough words in the 2nd line",
self.executable
)
})?
.to_owned();
let default: semver::Version = long
.split('+')
.next()
.ok_or_else(|| {
anyhow::anyhow!("{} version parsing: metadata dropping", self.executable)
})?
.parse()
.map_err(|error| anyhow::anyhow!("{} version parsing: {}", self.executable, error))?;
let l2_revision: Option<semver::Version> = stdout
.lines()
.nth(2)
.and_then(|line| line.split(' ').nth(1))
.and_then(|line| line.split('-').nth(1))
.and_then(|version| version.parse().ok());
let version = Version::new(long, default, l2_revision);
if version.default < Self::FIRST_SUPPORTED_VERSION {
anyhow::bail!(
"`solc` versions <{} are not supported, found {}",
Self::FIRST_SUPPORTED_VERSION,
version.default
);
}
if version.default > Self::LAST_SUPPORTED_VERSION {
anyhow::bail!(
"`solc` versions >{} are not supported, found {}",
Self::LAST_SUPPORTED_VERSION,
version.default
);
}
self.version = Some(version.clone());
Ok(version)
}
}
+32
View File
@@ -0,0 +1,32 @@
//!
//! The Solidity compiler pipeline type.
//!
use crate::solc::version::Version as SolcVersion;
use crate::solc::Compiler as SolcCompiler;
///
/// The Solidity compiler pipeline type.
///
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[allow(non_camel_case_types)]
#[allow(clippy::upper_case_acronyms)]
pub enum Pipeline {
/// The Yul IR.
Yul,
/// The EVM legacy assembly IR.
EVMLA,
}
impl Pipeline {
///
/// We always use EVMLA for Solidity <=0.7, or if the user does not want to compile via Yul.
///
pub fn new(solc_version: &SolcVersion, force_evmla: bool) -> Self {
if solc_version.default < SolcCompiler::FIRST_YUL_VERSION || force_evmla {
Self::EVMLA
} else {
Self::Yul
}
}
}
@@ -0,0 +1,26 @@
//!
//! The `solc --standard-json` input language.
//!
use serde::Deserialize;
use serde::Serialize;
///
/// The `solc --standard-json` input language.
///
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Language {
/// The Solidity language.
Solidity,
/// The Yul IR.
Yul,
}
impl std::fmt::Display for Language {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Solidity => write!(f, "Solidity"),
Self::Yul => write!(f, "Yul"),
}
}
}
@@ -0,0 +1,147 @@
//!
//! The `solc --standard-json` input.
//!
pub mod language;
pub mod settings;
pub mod source;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::path::PathBuf;
use rayon::iter::IntoParallelIterator;
use rayon::iter::ParallelIterator;
use serde::Deserialize;
use serde::Serialize;
use crate::solc::pipeline::Pipeline as SolcPipeline;
use crate::solc::standard_json::input::settings::metadata::Metadata as SolcStandardJsonInputSettingsMetadata;
use crate::solc::standard_json::input::settings::optimizer::Optimizer as SolcStandardJsonInputSettingsOptimizer;
use crate::solc::standard_json::input::settings::selection::Selection as SolcStandardJsonInputSettingsSelection;
use crate::warning::Warning;
use self::language::Language;
use self::settings::Settings;
use self::source::Source;
///
/// The `solc --standard-json` input.
///
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Input {
/// The input language.
pub language: Language,
/// The input source code files hashmap.
pub sources: BTreeMap<String, Source>,
/// The compiler settings.
pub settings: Settings,
/// The suppressed warnings.
#[serde(skip_serializing)]
pub suppressed_warnings: Option<Vec<Warning>>,
}
impl Input {
///
/// A shortcut constructor from stdin.
///
pub fn try_from_stdin(solc_pipeline: SolcPipeline) -> anyhow::Result<Self> {
let mut input: Self = serde_json::from_reader(std::io::BufReader::new(std::io::stdin()))?;
input
.settings
.output_selection
.get_or_insert_with(SolcStandardJsonInputSettingsSelection::default)
.extend_with_required(solc_pipeline);
Ok(input)
}
///
/// A shortcut constructor from paths.
///
#[allow(clippy::too_many_arguments)]
pub fn try_from_paths(
language: Language,
evm_version: Option<era_compiler_common::EVMVersion>,
paths: &[PathBuf],
library_map: Vec<String>,
remappings: Option<BTreeSet<String>>,
output_selection: SolcStandardJsonInputSettingsSelection,
optimizer: SolcStandardJsonInputSettingsOptimizer,
metadata: Option<SolcStandardJsonInputSettingsMetadata>,
via_ir: bool,
suppressed_warnings: Option<Vec<Warning>>,
) -> anyhow::Result<Self> {
let sources = paths
.into_par_iter()
.map(|path| {
let source = Source::try_from(path.as_path()).unwrap_or_else(|error| {
panic!("Source code file {path:?} reading error: {error}")
});
(path.to_string_lossy().to_string(), source)
})
.collect();
let libraries = Settings::parse_libraries(library_map)?;
Ok(Self {
language,
sources,
settings: Settings::new(
evm_version,
libraries,
remappings,
output_selection,
via_ir,
optimizer,
metadata,
),
suppressed_warnings,
})
}
///
/// A shortcut constructor from source code.
///
/// Only for the integration test purposes.
///
#[allow(clippy::too_many_arguments)]
pub fn try_from_sources(
evm_version: Option<era_compiler_common::EVMVersion>,
sources: BTreeMap<String, String>,
libraries: BTreeMap<String, BTreeMap<String, String>>,
remappings: Option<BTreeSet<String>>,
output_selection: SolcStandardJsonInputSettingsSelection,
optimizer: SolcStandardJsonInputSettingsOptimizer,
metadata: Option<SolcStandardJsonInputSettingsMetadata>,
via_ir: bool,
suppressed_warnings: Option<Vec<Warning>>,
) -> anyhow::Result<Self> {
let sources = sources
.into_par_iter()
.map(|(path, content)| (path, Source::from(content)))
.collect();
Ok(Self {
language: Language::Solidity,
sources,
settings: Settings::new(
evm_version,
libraries,
remappings,
output_selection,
via_ir,
optimizer,
metadata,
),
suppressed_warnings,
})
}
///
/// Sets the necessary defaults.
///
pub fn normalize(&mut self, version: &semver::Version) {
self.settings.normalize(version);
}
}
@@ -0,0 +1,28 @@
//!
//! The `solc --standard-json` input settings metadata.
//!
use serde::Deserialize;
use serde::Serialize;
///
/// The `solc --standard-json` input settings metadata.
///
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Metadata {
/// The bytecode hash mode.
#[serde(skip_serializing_if = "Option::is_none")]
pub bytecode_hash: Option<era_compiler_llvm_context::EraVMMetadataHash>,
}
impl Metadata {
///
/// A shortcut constructor.
///
pub fn new(bytecode_hash: era_compiler_llvm_context::EraVMMetadataHash) -> Self {
Self {
bytecode_hash: Some(bytecode_hash),
}
}
}
@@ -0,0 +1,111 @@
//!
//! The `solc --standard-json` input settings.
//!
pub mod metadata;
pub mod optimizer;
pub mod selection;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use serde::Deserialize;
use serde::Serialize;
use self::metadata::Metadata;
use self::optimizer::Optimizer;
use self::selection::Selection;
///
/// The `solc --standard-json` input settings.
///
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Settings {
/// The target EVM version.
#[serde(skip_serializing_if = "Option::is_none")]
pub evm_version: Option<era_compiler_common::EVMVersion>,
/// The linker library addresses.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub libraries: Option<BTreeMap<String, BTreeMap<String, String>>>,
/// The sorted list of remappings.
#[serde(skip_serializing_if = "Option::is_none")]
pub remappings: Option<BTreeSet<String>>,
/// The output selection filters.
#[serde(skip_serializing_if = "Option::is_none")]
pub output_selection: Option<Selection>,
/// Whether to compile via IR. Only for testing with solc >=0.8.13.
#[serde(
rename = "viaIR",
skip_serializing_if = "Option::is_none",
skip_deserializing
)]
pub via_ir: Option<bool>,
/// The optimizer settings.
pub optimizer: Optimizer,
/// The metadata settings.
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Metadata>,
}
impl Settings {
///
/// A shortcut constructor.
///
pub fn new(
evm_version: Option<era_compiler_common::EVMVersion>,
libraries: BTreeMap<String, BTreeMap<String, String>>,
remappings: Option<BTreeSet<String>>,
output_selection: Selection,
via_ir: bool,
optimizer: Optimizer,
metadata: Option<Metadata>,
) -> Self {
Self {
evm_version,
libraries: Some(libraries),
remappings,
output_selection: Some(output_selection),
via_ir: if via_ir { Some(true) } else { None },
optimizer,
metadata,
}
}
///
/// Sets the necessary defaults.
///
pub fn normalize(&mut self, version: &semver::Version) {
self.optimizer.normalize(version);
}
///
/// Parses the library list and returns their double hashmap with path and name as keys.
///
pub fn parse_libraries(
input: Vec<String>,
) -> anyhow::Result<BTreeMap<String, BTreeMap<String, String>>> {
let mut libraries = BTreeMap::new();
for (index, library) in input.into_iter().enumerate() {
let mut path_and_address = library.split('=');
let path = path_and_address
.next()
.ok_or_else(|| anyhow::anyhow!("The library #{} path is missing", index))?;
let mut file_and_contract = path.split(':');
let file = file_and_contract
.next()
.ok_or_else(|| anyhow::anyhow!("The library `{}` file name is missing", path))?;
let contract = file_and_contract.next().ok_or_else(|| {
anyhow::anyhow!("The library `{}` contract name is missing", path)
})?;
let address = path_and_address
.next()
.ok_or_else(|| anyhow::anyhow!("The library `{}` address is missing", path))?;
libraries
.entry(file.to_owned())
.or_insert_with(BTreeMap::new)
.insert(contract.to_owned(), address.to_owned());
}
Ok(libraries)
}
}
@@ -0,0 +1,67 @@
//!
//! The `solc --standard-json` input settings optimizer details.
//!
use serde::Deserialize;
use serde::Serialize;
///
/// The `solc --standard-json` input settings optimizer details.
///
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Details {
/// Whether the pass is enabled.
pub peephole: bool,
/// Whether the pass is enabled.
#[serde(skip_serializing_if = "Option::is_none")]
pub inliner: Option<bool>,
/// Whether the pass is enabled.
pub jumpdest_remover: bool,
/// Whether the pass is enabled.
pub order_literals: bool,
/// Whether the pass is enabled.
pub deduplicate: bool,
/// Whether the pass is enabled.
pub cse: bool,
/// Whether the pass is enabled.
pub constant_optimizer: bool,
}
impl Details {
///
/// A shortcut constructor.
///
pub fn new(
peephole: bool,
inliner: Option<bool>,
jumpdest_remover: bool,
order_literals: bool,
deduplicate: bool,
cse: bool,
constant_optimizer: bool,
) -> Self {
Self {
peephole,
inliner,
jumpdest_remover,
order_literals,
deduplicate,
cse,
constant_optimizer,
}
}
///
/// Creates a set of disabled optimizations.
///
pub fn disabled(version: &semver::Version) -> Self {
let inliner = if version >= &semver::Version::new(0, 8, 5) {
Some(false)
} else {
None
};
Self::new(false, inliner, false, false, false, false, false)
}
}
@@ -0,0 +1,82 @@
//!
//! The `solc --standard-json` input settings optimizer.
//!
pub mod details;
use serde::Deserialize;
use serde::Serialize;
use self::details::Details;
///
/// The `solc --standard-json` input settings optimizer.
///
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Optimizer {
/// Whether the optimizer is enabled.
pub enabled: bool,
/// The optimization mode string.
#[serde(skip_serializing)]
pub mode: Option<char>,
/// The `solc` optimizer details.
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<Details>,
/// Whether to try to recompile with -Oz if the bytecode is too large.
#[serde(skip_serializing)]
pub fallback_to_optimizing_for_size: Option<bool>,
/// Whether to disable the system request memoization.
#[serde(skip_serializing)]
pub disable_system_request_memoization: Option<bool>,
}
impl Optimizer {
///
/// A shortcut constructor.
///
pub fn new(
enabled: bool,
mode: Option<char>,
version: &semver::Version,
fallback_to_optimizing_for_size: bool,
disable_system_request_memoization: bool,
) -> Self {
Self {
enabled,
mode,
details: Some(Details::disabled(version)),
fallback_to_optimizing_for_size: Some(fallback_to_optimizing_for_size),
disable_system_request_memoization: Some(disable_system_request_memoization),
}
}
///
/// Sets the necessary defaults.
///
pub fn normalize(&mut self, version: &semver::Version) {
self.details = if version >= &semver::Version::new(0, 5, 5) {
Some(Details::disabled(version))
} else {
None
};
}
}
impl TryFrom<&Optimizer> for era_compiler_llvm_context::OptimizerSettings {
type Error = anyhow::Error;
fn try_from(value: &Optimizer) -> Result<Self, Self::Error> {
let mut result = match value.mode {
Some(mode) => Self::try_from_cli(mode)?,
None => Self::cycles(),
};
if value.fallback_to_optimizing_for_size.unwrap_or_default() {
result.enable_fallback_to_size();
}
if value.disable_system_request_memoization.unwrap_or_default() {
result.disable_system_request_memoization();
}
Ok(result)
}
}
@@ -0,0 +1,69 @@
//!
//! The `solc --standard-json` expected output selection flag.
//!
use serde::Deserialize;
use serde::Serialize;
use crate::solc::pipeline::Pipeline as SolcPipeline;
///
/// The `solc --standard-json` expected output selection flag.
///
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
#[allow(non_camel_case_types)]
#[allow(clippy::upper_case_acronyms)]
pub enum Flag {
/// The ABI JSON.
#[serde(rename = "abi")]
ABI,
/// The metadata.
#[serde(rename = "metadata")]
Metadata,
/// The developer documentation.
#[serde(rename = "devdoc")]
Devdoc,
/// The user documentation.
#[serde(rename = "userdoc")]
Userdoc,
/// The function signature hashes JSON.
#[serde(rename = "evm.methodIdentifiers")]
MethodIdentifiers,
/// The storage layout.
#[serde(rename = "storageLayout")]
StorageLayout,
/// The AST JSON.
#[serde(rename = "ast")]
AST,
/// The Yul IR.
#[serde(rename = "irOptimized")]
Yul,
/// The EVM legacy assembly JSON.
#[serde(rename = "evm.legacyAssembly")]
EVMLA,
}
impl From<SolcPipeline> for Flag {
fn from(pipeline: SolcPipeline) -> Self {
match pipeline {
SolcPipeline::Yul => Self::Yul,
SolcPipeline::EVMLA => Self::EVMLA,
}
}
}
impl std::fmt::Display for Flag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ABI => write!(f, "abi"),
Self::Metadata => write!(f, "metadata"),
Self::Devdoc => write!(f, "devdoc"),
Self::Userdoc => write!(f, "userdoc"),
Self::MethodIdentifiers => write!(f, "evm.methodIdentifiers"),
Self::StorageLayout => write!(f, "storageLayout"),
Self::AST => write!(f, "ast"),
Self::Yul => write!(f, "irOptimized"),
Self::EVMLA => write!(f, "evm.legacyAssembly"),
}
}
}
@@ -0,0 +1,58 @@
//!
//! The `solc --standard-json` output file selection.
//!
pub mod flag;
use std::collections::HashSet;
use serde::Deserialize;
use serde::Serialize;
use crate::solc::pipeline::Pipeline as SolcPipeline;
use self::flag::Flag as SelectionFlag;
///
/// The `solc --standard-json` output file selection.
///
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct File {
/// The per-file output selections.
#[serde(rename = "", skip_serializing_if = "Option::is_none")]
pub per_file: Option<HashSet<SelectionFlag>>,
/// The per-contract output selections.
#[serde(rename = "*", skip_serializing_if = "Option::is_none")]
pub per_contract: Option<HashSet<SelectionFlag>>,
}
impl File {
///
/// Creates the selection required by our compilation process.
///
pub fn new_required(pipeline: SolcPipeline) -> Self {
Self {
per_file: Some(HashSet::from_iter([SelectionFlag::AST])),
per_contract: Some(HashSet::from_iter([
SelectionFlag::MethodIdentifiers,
SelectionFlag::Metadata,
SelectionFlag::from(pipeline),
])),
}
}
///
/// Extends the user's output selection with flag required by our compilation process.
///
pub fn extend_with_required(&mut self, pipeline: SolcPipeline) -> &mut Self {
let required = Self::new_required(pipeline);
self.per_file
.get_or_insert_with(HashSet::default)
.extend(required.per_file.unwrap_or_default());
self.per_contract
.get_or_insert_with(HashSet::default)
.extend(required.per_contract.unwrap_or_default());
self
}
}
@@ -0,0 +1,43 @@
//!
//! The `solc --standard-json` output selection.
//!
pub mod file;
use serde::Deserialize;
use serde::Serialize;
use crate::solc::pipeline::Pipeline as SolcPipeline;
use self::file::File as FileSelection;
///
/// The `solc --standard-json` output selection.
///
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Selection {
/// Only the 'all' wildcard is available for robustness reasons.
#[serde(rename = "*", skip_serializing_if = "Option::is_none")]
pub all: Option<FileSelection>,
}
impl Selection {
///
/// Creates the selection required by our compilation process.
///
pub fn new_required(pipeline: SolcPipeline) -> Self {
Self {
all: Some(FileSelection::new_required(pipeline)),
}
}
///
/// Extends the user's output selection with flag required by our compilation process.
///
pub fn extend_with_required(&mut self, pipeline: SolcPipeline) -> &mut Self {
self.all
.get_or_insert_with(|| FileSelection::new_required(pipeline))
.extend_with_required(pipeline);
self
}
}
@@ -0,0 +1,44 @@
//!
//! The `solc --standard-json` input source.
//!
use std::io::Read;
use std::path::Path;
use serde::Deserialize;
use serde::Serialize;
///
/// The `solc --standard-json` input source.
///
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Source {
/// The source code file content.
pub content: String,
}
impl From<String> for Source {
fn from(content: String) -> Self {
Self { content }
}
}
impl TryFrom<&Path> for Source {
type Error = anyhow::Error;
fn try_from(path: &Path) -> Result<Self, Self::Error> {
let content = if path.to_string_lossy() == "-" {
let mut solidity_code = String::with_capacity(16384);
std::io::stdin()
.read_to_string(&mut solidity_code)
.map_err(|error| anyhow::anyhow!("<stdin> reading error: {}", error))?;
solidity_code
} else {
std::fs::read_to_string(path)
.map_err(|error| anyhow::anyhow!("File {:?} reading error: {}", path, error))?
};
Ok(Self { content })
}
}
@@ -0,0 +1,6 @@
//!
//! The `solc <input>.sol --standard-json`.
//!
pub mod input;
pub mod output;
@@ -0,0 +1,25 @@
//!
//! The `solc --standard-json` output contract EVM bytecode.
//!
use serde::Deserialize;
use serde::Serialize;
///
/// The `solc --standard-json` output contract EVM bytecode.
///
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Bytecode {
/// The bytecode object.
pub object: String,
}
impl Bytecode {
///
/// A shortcut constructor.
///
pub fn new(object: String) -> Self {
Self { object }
}
}
@@ -0,0 +1,52 @@
//!
//! The `solc --standard-json` output contract EVM extra metadata.
//!
pub mod recursive_function;
use serde::Deserialize;
use serde::Serialize;
use self::recursive_function::RecursiveFunction;
///
/// The `solc --standard-json` output contract EVM extra metadata.
///
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ExtraMetadata {
/// The list of recursive functions.
#[serde(default = "Vec::new")]
pub recursive_functions: Vec<RecursiveFunction>,
}
impl ExtraMetadata {
///
/// Returns the recursive function reference for the specified tag.
///
pub fn get(
&self,
block_key: &era_compiler_llvm_context::EraVMFunctionBlockKey,
) -> Option<&RecursiveFunction> {
for function in self.recursive_functions.iter() {
match block_key.code_type {
era_compiler_llvm_context::EraVMCodeType::Deploy => {
if let Some(creation_tag) = function.creation_tag {
if num::BigUint::from(creation_tag) == block_key.tag {
return Some(function);
}
}
}
era_compiler_llvm_context::EraVMCodeType::Runtime => {
if let Some(runtime_tag) = function.runtime_tag {
if num::BigUint::from(runtime_tag) == block_key.tag {
return Some(function);
}
}
}
}
}
None
}
}
@@ -0,0 +1,26 @@
//!
//! The `solc --standard-json` output contract EVM recursive function.
//!
use serde::Deserialize;
use serde::Serialize;
///
/// The `solc --standard-json` output contract EVM recursive function.
///
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct RecursiveFunction {
/// The function name.
pub name: String,
/// The creation code function block tag.
pub creation_tag: Option<usize>,
/// The runtime code function block tag.
pub runtime_tag: Option<usize>,
/// The number of input arguments.
#[serde(rename = "totalParamSize")]
pub input_size: usize,
/// The number of output arguments.
#[serde(rename = "totalRetParamSize")]
pub output_size: usize,
}
@@ -0,0 +1,51 @@
//!
//! The `solc --standard-json` output contract EVM data.
//!
pub mod bytecode;
pub mod extra_metadata;
use std::collections::BTreeMap;
use serde::Deserialize;
use serde::Serialize;
use crate::evmla::assembly::Assembly;
use self::bytecode::Bytecode;
use self::extra_metadata::ExtraMetadata;
///
/// The `solc --standard-json` output contract EVM data.
///
/// It is replaced by EraVM data after compiling.
///
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct EVM {
/// The contract EVM legacy assembly code.
#[serde(rename = "legacyAssembly")]
pub assembly: Option<Assembly>,
/// The contract EraVM assembly code.
#[serde(rename = "assembly")]
pub assembly_text: Option<String>,
/// The contract bytecode.
/// Is reset by that of EraVM before yielding the compiled project artifacts.
pub bytecode: Option<Bytecode>,
/// The contract function signatures.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub method_identifiers: Option<BTreeMap<String, String>>,
/// The extra EVMLA metadata.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extra_metadata: Option<ExtraMetadata>,
}
impl EVM {
///
/// Sets the EraVM assembly and bytecode.
///
pub fn modify(&mut self, assembly_text: String, bytecode: String) {
self.assembly_text = Some(assembly_text);
self.bytecode = Some(Bytecode::new(bytecode));
}
}
@@ -0,0 +1,51 @@
//!
//! The `solc --standard-json` output contract.
//!
pub mod evm;
use std::collections::BTreeMap;
use std::collections::HashSet;
use serde::Deserialize;
use serde::Serialize;
use self::evm::EVM;
///
/// The `solc --standard-json` output contract.
///
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Contract {
/// The contract ABI.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub abi: Option<serde_json::Value>,
/// The contract metadata.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
/// The contract developer documentation.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub devdoc: Option<serde_json::Value>,
/// The contract user documentation.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub userdoc: Option<serde_json::Value>,
/// The contract storage layout.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub storage_layout: Option<serde_json::Value>,
/// Contract's bytecode and related objects
#[serde(default, skip_serializing_if = "Option::is_none")]
pub evm: Option<EVM>,
/// The contract optimized IR code.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ir_optimized: Option<String>,
/// The contract EraVM bytecode hash.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hash: Option<String>,
/// The contract factory dependencies.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub factory_dependencies: Option<BTreeMap<String, String>>,
/// The contract missing libraries.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub missing_libraries: Option<HashSet<String>>,
}
@@ -0,0 +1,177 @@
//!
//! The `solc --standard-json` output error.
//!
pub mod source_location;
use std::str::FromStr;
use serde::Deserialize;
use serde::Serialize;
use self::source_location::SourceLocation;
///
/// The `solc --standard-json` output error.
///
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Error {
/// The component type.
pub component: String,
/// The error code.
pub error_code: Option<String>,
/// The formatted error message.
pub formatted_message: String,
/// The non-formatted error message.
pub message: String,
/// The error severity.
pub severity: String,
/// The error location data.
pub source_location: Option<SourceLocation>,
/// The error type.
pub r#type: String,
}
impl Error {
///
/// Returns the `ecrecover` function usage warning.
///
pub fn message_ecrecover(src: Option<&str>) -> Self {
let message = r#"
┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Warning: It looks like you are using 'ecrecover' to validate a signature of a user account. │
│ zkSync Era comes with native account abstraction support, therefore it is highly recommended NOT │
│ to rely on the fact that the account has an ECDSA private key attached to it since accounts might│
│ implement other signature schemes. │
│ Read more about Account Abstraction at https://v2-docs.zksync.io/dev/developer-guides/aa.html │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘"#
.to_owned();
Self {
component: "general".to_owned(),
error_code: None,
formatted_message: message.clone(),
message,
severity: "warning".to_owned(),
source_location: src.map(SourceLocation::from_str).and_then(Result::ok),
r#type: "Warning".to_owned(),
}
}
///
/// Returns the `<address payable>`'s `send` and `transfer` methods usage error.
///
pub fn message_send_and_transfer(src: Option<&str>) -> Self {
let message = r#"
┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Warning: It looks like you are using '<address payable>.send/transfer(<X>)' without providing │
│ the gas amount. Such calls will fail depending on the pubdata costs. │
│ This might be a false positive if you are using an interface (like IERC20) instead of the │
│ native Solidity `send/transfer`. │
│ Please use 'payable(<address>).call{value: <X>}("")' instead, but be careful with the reentrancy │
│ attack. `send` and `transfer` send limited amount of gas that prevents reentrancy, whereas │
│ `<address>.call{value: <X>}` sends all gas to the callee. Learn more on │
│ https://docs.soliditylang.org/en/latest/security-considerations.html#reentrancy │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘"#
.to_owned();
Self {
component: "general".to_owned(),
error_code: None,
formatted_message: message.clone(),
message,
severity: "warning".to_owned(),
source_location: src.map(SourceLocation::from_str).and_then(Result::ok),
r#type: "Warning".to_owned(),
}
}
///
/// Returns the `extcodesize` instruction usage warning.
///
pub fn message_extcodesize(src: Option<&str>) -> Self {
let message = r#"
┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Warning: Your code or one of its dependencies uses the 'extcodesize' instruction, which is │
│ usually needed in the following cases: │
│ 1. To detect whether an address belongs to a smart contract. │
│ 2. To detect whether the deploy code execution has finished. │
│ zkSync Era comes with native account abstraction support (so accounts are smart contracts, │
│ including private-key controlled EOAs), and you should avoid differentiating between contracts │
│ and non-contract addresses. │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘"#
.to_owned();
Self {
component: "general".to_owned(),
error_code: None,
formatted_message: message.clone(),
message,
severity: "warning".to_owned(),
source_location: src.map(SourceLocation::from_str).and_then(Result::ok),
r#type: "Warning".to_owned(),
}
}
///
/// Returns the `origin` instruction usage warning.
///
pub fn message_tx_origin(src: Option<&str>) -> Self {
let message = r#"
┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Warning: You are checking for 'tx.origin' in your code, which might lead to unexpected behavior. │
│ zkSync Era comes with native account abstraction support, and therefore the initiator of a │
│ transaction might be different from the contract calling your code. It is highly recommended NOT │
│ to rely on tx.origin, but use msg.sender instead. │
│ Read more about Account Abstraction at https://v2-docs.zksync.io/dev/developer-guides/aa.html │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘"#
.to_owned();
Self {
component: "general".to_owned(),
error_code: None,
formatted_message: message.clone(),
message,
severity: "warning".to_owned(),
source_location: src.map(SourceLocation::from_str).and_then(Result::ok),
r#type: "Warning".to_owned(),
}
}
///
/// Returns the internal function pointer usage error.
///
pub fn message_internal_function_pointer(src: Option<&str>) -> Self {
let message = r#"
┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Error: Internal function pointers are not supported in EVM legacy assembly pipeline. │
│ Please use the Yul IR codegen instead. │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘"#
.to_owned();
Self {
component: "general".to_owned(),
error_code: None,
formatted_message: message.clone(),
message,
severity: "error".to_owned(),
source_location: src.map(SourceLocation::from_str).and_then(Result::ok),
r#type: "Error".to_owned(),
}
}
///
/// Appends the contract path to the message..
///
pub fn push_contract_path(&mut self, path: &str) {
self.formatted_message
.push_str(format!("\n--> {path}\n").as_str());
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.formatted_message)
}
}
@@ -0,0 +1,47 @@
//!
//! The `solc --standard-json` output error source location.
//!
use std::str::FromStr;
use serde::Deserialize;
use serde::Serialize;
///
/// The `solc --standard-json` output error source location.
///
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SourceLocation {
/// The source file path.
pub file: String,
/// The start location.
pub start: isize,
/// The end location.
pub end: isize,
}
impl FromStr for SourceLocation {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<Self, Self::Err> {
let mut parts = string.split(':');
let start = parts
.next()
.map(|string| string.parse::<isize>())
.and_then(Result::ok)
.unwrap_or_default();
let length = parts
.next()
.map(|string| string.parse::<isize>())
.and_then(Result::ok)
.unwrap_or_default();
let file = parts.next().unwrap_or_default().to_owned();
Ok(Self {
file,
start,
end: start + length,
})
}
}
@@ -0,0 +1,281 @@
//!
//! The `solc --standard-json` output.
//!
pub mod contract;
pub mod error;
pub mod source;
use std::collections::BTreeMap;
use serde::Deserialize;
use serde::Serialize;
use sha3::Digest;
use crate::evmla::assembly::instruction::Instruction;
use crate::evmla::assembly::Assembly;
use crate::project::contract::ir::IR as ProjectContractIR;
use crate::project::contract::Contract as ProjectContract;
use crate::project::Project;
use crate::solc::pipeline::Pipeline as SolcPipeline;
use crate::solc::version::Version as SolcVersion;
use crate::warning::Warning;
use crate::yul::lexer::Lexer;
use crate::yul::parser::statement::object::Object;
use self::contract::Contract;
use self::error::Error as SolcStandardJsonOutputError;
use self::source::Source;
///
/// The `solc --standard-json` output.
///
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Output {
/// The file-contract hashmap.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub contracts: Option<BTreeMap<String, BTreeMap<String, Contract>>>,
/// The source code mapping data.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sources: Option<BTreeMap<String, Source>>,
/// The compilation errors and warnings.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub errors: Option<Vec<SolcStandardJsonOutputError>>,
/// The `solc` compiler version.
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
/// The `solc` compiler long version.
#[serde(skip_serializing_if = "Option::is_none")]
pub long_version: Option<String>,
/// The `zksolc` compiler version.
#[serde(skip_serializing_if = "Option::is_none")]
pub zk_version: Option<String>,
}
impl Output {
///
/// Converts the `solc` JSON output into a convenient project.
///
pub fn try_to_project(
&mut self,
source_code_files: BTreeMap<String, String>,
libraries: BTreeMap<String, BTreeMap<String, String>>,
pipeline: SolcPipeline,
solc_version: &SolcVersion,
debug_config: Option<&era_compiler_llvm_context::DebugConfig>,
) -> anyhow::Result<Project> {
if let SolcPipeline::EVMLA = pipeline {
self.preprocess_dependencies()?;
}
let files = match self.contracts.as_ref() {
Some(files) => files,
None => {
anyhow::bail!(
"{}",
self.errors
.as_ref()
.map(|errors| serde_json::to_string_pretty(errors).expect("Always valid"))
.unwrap_or_else(|| "Unknown project assembling error".to_owned())
);
}
};
let mut project_contracts = BTreeMap::new();
for (path, contracts) in files.iter() {
for (name, contract) in contracts.iter() {
let full_path = format!("{path}:{name}");
let source = match pipeline {
SolcPipeline::Yul => {
let ir_optimized = match contract.ir_optimized.to_owned() {
Some(ir_optimized) => ir_optimized,
None => continue,
};
if ir_optimized.is_empty() {
continue;
}
if let Some(debug_config) = debug_config {
debug_config.dump_yul(full_path.as_str(), ir_optimized.as_str())?;
}
let mut lexer = Lexer::new(ir_optimized.to_owned());
let object = Object::parse(&mut lexer, None).map_err(|error| {
anyhow::anyhow!("Contract `{}` parsing error: {:?}", full_path, error)
})?;
ProjectContractIR::new_yul(ir_optimized.to_owned(), object)
}
SolcPipeline::EVMLA => {
let evm = contract.evm.as_ref();
let assembly = match evm.and_then(|evm| evm.assembly.to_owned()) {
Some(assembly) => assembly.to_owned(),
None => continue,
};
let extra_metadata = evm
.and_then(|evm| evm.extra_metadata.to_owned())
.unwrap_or_default();
ProjectContractIR::new_evmla(assembly, extra_metadata)
}
};
let source_code = source_code_files
.get(path.as_str())
.ok_or_else(|| anyhow::anyhow!("Source code for path `{}` not found", path))?;
let source_hash = sha3::Keccak256::digest(source_code.as_bytes()).into();
let project_contract = ProjectContract::new(
full_path.clone(),
source_hash,
solc_version.to_owned(),
source,
contract.metadata.to_owned(),
);
project_contracts.insert(full_path, project_contract);
}
}
Ok(Project::new(
solc_version.to_owned(),
project_contracts,
libraries,
))
}
///
/// Removes EVM artifacts to prevent their accidental usage.
///
pub fn remove_evm(&mut self) {
if let Some(files) = self.contracts.as_mut() {
for (_, file) in files.iter_mut() {
for (_, contract) in file.iter_mut() {
if let Some(evm) = contract.evm.as_mut() {
evm.bytecode = None;
}
}
}
}
}
///
/// Traverses the AST and returns the list of additional errors and warnings.
///
pub fn preprocess_ast(
&mut self,
version: &SolcVersion,
pipeline: SolcPipeline,
suppressed_warnings: &[Warning],
) -> anyhow::Result<()> {
let sources = match self.sources.as_ref() {
Some(sources) => sources,
None => return Ok(()),
};
let mut messages = Vec::new();
for (path, source) in sources.iter() {
if let Some(ast) = source.ast.as_ref() {
let mut eravm_messages =
Source::get_messages(ast, version, pipeline, suppressed_warnings);
for message in eravm_messages.iter_mut() {
message.push_contract_path(path.as_str());
}
messages.extend(eravm_messages);
}
}
self.errors = match self.errors.take() {
Some(mut errors) => {
errors.extend(messages);
Some(errors)
}
None => Some(messages),
};
Ok(())
}
///
/// The pass, which replaces with dependency indexes with actual data.
///
fn preprocess_dependencies(&mut self) -> anyhow::Result<()> {
let files = match self.contracts.as_mut() {
Some(files) => files,
None => return Ok(()),
};
let mut hash_path_mapping = BTreeMap::new();
for (path, contracts) in files.iter() {
for (name, contract) in contracts.iter() {
let full_path = format!("{path}:{name}");
let hash = match contract
.evm
.as_ref()
.and_then(|evm| evm.assembly.as_ref())
.map(|assembly| assembly.keccak256())
{
Some(hash) => hash,
None => continue,
};
hash_path_mapping.insert(hash, full_path);
}
}
for (path, contracts) in files.iter_mut() {
for (name, contract) in contracts.iter_mut() {
let assembly = match contract.evm.as_mut().and_then(|evm| evm.assembly.as_mut()) {
Some(assembly) => assembly,
None => continue,
};
let full_path = format!("{path}:{name}");
Self::preprocess_dependency_level(
full_path.as_str(),
assembly,
&hash_path_mapping,
)?;
}
}
Ok(())
}
///
/// Preprocesses an assembly JSON structure dependency data map.
///
fn preprocess_dependency_level(
full_path: &str,
assembly: &mut Assembly,
hash_path_mapping: &BTreeMap<String, String>,
) -> anyhow::Result<()> {
assembly.set_full_path(full_path.to_owned());
let deploy_code_index_path_mapping =
assembly.deploy_dependencies_pass(full_path, hash_path_mapping)?;
if let Some(deploy_code_instructions) = assembly.code.as_deref_mut() {
Instruction::replace_data_aliases(
deploy_code_instructions,
&deploy_code_index_path_mapping,
)?;
};
let runtime_code_index_path_mapping =
assembly.runtime_dependencies_pass(full_path, hash_path_mapping)?;
if let Some(runtime_code_instructions) = assembly
.data
.as_mut()
.and_then(|data_map| data_map.get_mut("0"))
.and_then(|data| data.get_assembly_mut())
.and_then(|assembly| assembly.code.as_deref_mut())
{
Instruction::replace_data_aliases(
runtime_code_instructions,
&runtime_code_index_path_mapping,
)?;
}
Ok(())
}
}
@@ -0,0 +1,265 @@
//!
//! The `solc --standard-json` output source.
//!
use serde::Deserialize;
use serde::Serialize;
use crate::solc::pipeline::Pipeline as SolcPipeline;
use crate::solc::standard_json::output::error::Error as SolcStandardJsonOutputError;
use crate::solc::version::Version as SolcVersion;
use crate::warning::Warning;
///
/// The `solc --standard-json` output source.
///
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Source {
/// The source code ID.
pub id: usize,
/// The source code AST.
pub ast: Option<serde_json::Value>,
}
impl Source {
///
/// Checks the AST node for the `ecrecover` function usage.
///
pub fn check_ecrecover(ast: &serde_json::Value) -> Option<SolcStandardJsonOutputError> {
let ast = ast.as_object()?;
if ast.get("nodeType")?.as_str()? != "FunctionCall" {
return None;
}
let expression = ast.get("expression")?.as_object()?;
if expression.get("nodeType")?.as_str()? != "Identifier" {
return None;
}
if expression.get("name")?.as_str()? != "ecrecover" {
return None;
}
Some(SolcStandardJsonOutputError::message_ecrecover(
ast.get("src")?.as_str(),
))
}
///
/// Checks the AST node for the `<address payable>`'s `send` and `transfer` methods usage.
///
pub fn check_send_and_transfer(ast: &serde_json::Value) -> Option<SolcStandardJsonOutputError> {
let ast = ast.as_object()?;
if ast.get("nodeType")?.as_str()? != "FunctionCall" {
return None;
}
let expression = ast.get("expression")?.as_object()?;
if expression.get("nodeType")?.as_str()? != "MemberAccess" {
return None;
}
let member_name = expression.get("memberName")?.as_str()?;
if member_name != "send" && member_name != "transfer" {
return None;
}
Some(SolcStandardJsonOutputError::message_send_and_transfer(
ast.get("src")?.as_str(),
))
}
///
/// Checks the AST node for the `extcodesize` assembly instruction usage.
///
pub fn check_assembly_extcodesize(
ast: &serde_json::Value,
) -> Option<SolcStandardJsonOutputError> {
let ast = ast.as_object()?;
if ast.get("nodeType")?.as_str()? != "YulFunctionCall" {
return None;
}
if ast
.get("functionName")?
.as_object()?
.get("name")?
.as_str()?
!= "extcodesize"
{
return None;
}
Some(SolcStandardJsonOutputError::message_extcodesize(
ast.get("src")?.as_str(),
))
}
///
/// Checks the AST node for the `origin` assembly instruction usage.
///
pub fn check_assembly_origin(ast: &serde_json::Value) -> Option<SolcStandardJsonOutputError> {
let ast = ast.as_object()?;
if ast.get("nodeType")?.as_str()? != "YulFunctionCall" {
return None;
}
if ast
.get("functionName")?
.as_object()?
.get("name")?
.as_str()?
!= "origin"
{
return None;
}
Some(SolcStandardJsonOutputError::message_tx_origin(
ast.get("src")?.as_str(),
))
}
///
/// Checks the AST node for the `tx.origin` value usage.
///
pub fn check_tx_origin(ast: &serde_json::Value) -> Option<SolcStandardJsonOutputError> {
let ast = ast.as_object()?;
if ast.get("nodeType")?.as_str()? != "MemberAccess" {
return None;
}
if ast.get("memberName")?.as_str()? != "origin" {
return None;
}
let expression = ast.get("expression")?.as_object()?;
if expression.get("nodeType")?.as_str()? != "Identifier" {
return None;
}
if expression.get("name")?.as_str()? != "tx" {
return None;
}
Some(SolcStandardJsonOutputError::message_tx_origin(
ast.get("src")?.as_str(),
))
}
///
/// Checks the AST node for the internal function pointers value usage.
///
pub fn check_internal_function_pointer(
ast: &serde_json::Value,
) -> Option<SolcStandardJsonOutputError> {
let ast = ast.as_object()?;
if ast.get("nodeType")?.as_str()? != "VariableDeclaration" {
return None;
}
let type_descriptions = ast.get("typeDescriptions")?.as_object()?;
if !type_descriptions
.get("typeIdentifier")?
.as_str()?
.contains("function_internal")
{
return None;
}
Some(
SolcStandardJsonOutputError::message_internal_function_pointer(
ast.get("src")?.as_str(),
),
)
}
///
/// Returns the list of messages for some specific parts of the AST.
///
pub fn get_messages(
ast: &serde_json::Value,
version: &SolcVersion,
pipeline: SolcPipeline,
suppressed_warnings: &[Warning],
) -> Vec<SolcStandardJsonOutputError> {
let mut messages = Vec::new();
if !suppressed_warnings.contains(&Warning::EcRecover) {
if let Some(message) = Self::check_ecrecover(ast) {
messages.push(message);
}
}
if !suppressed_warnings.contains(&Warning::SendTransfer) {
if let Some(message) = Self::check_send_and_transfer(ast) {
messages.push(message);
}
}
if !suppressed_warnings.contains(&Warning::ExtCodeSize) {
if let Some(message) = Self::check_assembly_extcodesize(ast) {
messages.push(message);
}
}
if !suppressed_warnings.contains(&Warning::TxOrigin) {
if let Some(message) = Self::check_assembly_origin(ast) {
messages.push(message);
}
if let Some(message) = Self::check_tx_origin(ast) {
messages.push(message);
}
}
if SolcPipeline::EVMLA == pipeline && version.l2_revision.is_none() {
if let Some(message) = Self::check_internal_function_pointer(ast) {
messages.push(message);
}
}
match ast {
serde_json::Value::Array(array) => {
for element in array.iter() {
messages.extend(Self::get_messages(
element,
version,
pipeline,
suppressed_warnings,
));
}
}
serde_json::Value::Object(object) => {
for (_key, value) in object.iter() {
messages.extend(Self::get_messages(
value,
version,
pipeline,
suppressed_warnings,
));
}
}
_ => {}
}
messages
}
///
/// Returns the name of the last contract.
///
pub fn last_contract_name(&self) -> anyhow::Result<String> {
self.ast
.as_ref()
.ok_or_else(|| anyhow::anyhow!("The AST is empty"))?
.get("nodes")
.and_then(|value| value.as_array())
.ok_or_else(|| {
anyhow::anyhow!("The last contract cannot be found in an empty list of nodes")
})?
.iter()
.filter_map(
|node| match node.get("nodeType").and_then(|node| node.as_str()) {
Some("ContractDefinition") => Some(node.get("name")?.as_str()?.to_owned()),
_ => None,
},
)
.last()
.ok_or_else(|| anyhow::anyhow!("The last contract not found in the AST"))
}
}
+47
View File
@@ -0,0 +1,47 @@
//!
//! The Solidity compiler version.
//!
use serde::Deserialize;
use serde::Serialize;
///
/// The Solidity compiler version.
///
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Version {
/// The long version string.
pub long: String,
/// The short `semver`.
pub default: semver::Version,
/// The L2 revision additional versioning.
pub l2_revision: Option<semver::Version>,
}
impl Version {
///
/// A shortcut constructor.
///
pub fn new(
long: String,
default: semver::Version,
l2_revision: Option<semver::Version>,
) -> Self {
Self {
long,
default,
l2_revision,
}
}
///
/// A shortcut constructor for a simple version.
///
pub fn new_simple(version: semver::Version) -> Self {
Self {
long: version.to_string(),
default: version,
l2_revision: None,
}
}
}
+197
View File
@@ -0,0 +1,197 @@
//! Common utility used for in frontend and integration tests.
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::path::PathBuf;
use std::str::FromStr;
use crate::project::Project;
use crate::solc::pipeline::Pipeline as SolcPipeline;
use crate::solc::standard_json::input::settings::optimizer::Optimizer as SolcStandardJsonInputSettingsOptimizer;
use crate::solc::standard_json::input::settings::selection::Selection as SolcStandardJsonInputSettingsSelection;
use crate::solc::standard_json::input::Input as SolcStandardJsonInput;
use crate::solc::standard_json::output::Output as SolcStandardJsonOutput;
use crate::solc::Compiler as SolcCompiler;
use crate::warning::Warning;
///
/// 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}}"
);
}
}
///
/// Builds the Solidity project and returns the standard JSON output.
///
pub fn build_solidity(
sources: BTreeMap<String, String>,
libraries: BTreeMap<String, BTreeMap<String, String>>,
remappings: Option<BTreeSet<String>>,
pipeline: SolcPipeline,
optimizer_settings: era_compiler_llvm_context::OptimizerSettings,
) -> anyhow::Result<SolcStandardJsonOutput> {
check_dependencies();
inkwell::support::enable_llvm_pretty_stack_trace();
era_compiler_llvm_context::initialize_target(era_compiler_llvm_context::Target::PVM);
let _ = crate::process::EXECUTABLE.set(PathBuf::from(crate::r#const::DEFAULT_EXECUTABLE_NAME));
let mut solc = SolcCompiler::new(SolcCompiler::DEFAULT_EXECUTABLE_NAME.to_owned())?;
let solc_version = solc.version()?;
let input = SolcStandardJsonInput::try_from_sources(
None,
sources.clone(),
libraries.clone(),
remappings,
SolcStandardJsonInputSettingsSelection::new_required(pipeline),
SolcStandardJsonInputSettingsOptimizer::new(
true,
None,
&solc_version.default,
false,
false,
),
None,
pipeline == SolcPipeline::Yul,
None,
)?;
let mut output = solc.standard_json(input, pipeline, None, vec![], None)?;
let project = output.try_to_project(sources, libraries, pipeline, &solc_version, None)?;
let build: crate::Build = project.compile(optimizer_settings, false, false, false, None)?;
build.write_to_standard_json(
&mut output,
&solc_version,
&semver::Version::from_str(env!("CARGO_PKG_VERSION"))?,
)?;
Ok(output)
}
///
/// Builds the Solidity project and returns the standard JSON output.
///
pub fn build_solidity_and_detect_missing_libraries(
sources: BTreeMap<String, String>,
libraries: BTreeMap<String, BTreeMap<String, String>>,
pipeline: SolcPipeline,
) -> anyhow::Result<SolcStandardJsonOutput> {
check_dependencies();
inkwell::support::enable_llvm_pretty_stack_trace();
era_compiler_llvm_context::initialize_target(era_compiler_llvm_context::Target::PVM);
let _ = crate::process::EXECUTABLE.set(PathBuf::from(crate::r#const::DEFAULT_EXECUTABLE_NAME));
let mut solc = SolcCompiler::new(SolcCompiler::DEFAULT_EXECUTABLE_NAME.to_owned())?;
let solc_version = solc.version()?;
let input = SolcStandardJsonInput::try_from_sources(
None,
sources.clone(),
libraries.clone(),
None,
SolcStandardJsonInputSettingsSelection::new_required(pipeline),
SolcStandardJsonInputSettingsOptimizer::new(
true,
None,
&solc_version.default,
false,
false,
),
None,
pipeline == SolcPipeline::Yul,
None,
)?;
let mut output = solc.standard_json(input, pipeline, None, vec![], None)?;
let project = output.try_to_project(sources, libraries, pipeline, &solc_version, None)?;
let missing_libraries = project.get_missing_libraries();
missing_libraries.write_to_standard_json(
&mut output,
&solc.version()?,
&semver::Version::from_str(env!("CARGO_PKG_VERSION"))?,
)?;
Ok(output)
}
///
/// Checks if the Yul project can be built without errors.
///
pub fn build_yul(source_code: &str) -> anyhow::Result<()> {
check_dependencies();
inkwell::support::enable_llvm_pretty_stack_trace();
era_compiler_llvm_context::initialize_target(era_compiler_llvm_context::Target::PVM);
let optimizer_settings = era_compiler_llvm_context::OptimizerSettings::none();
let project =
Project::try_from_yul_string(PathBuf::from("test.yul").as_path(), source_code, None)?;
let _build = project.compile(optimizer_settings, false, false, false, None)?;
Ok(())
}
///
/// Checks if the built Solidity project contains the given warning.
///
pub fn check_solidity_warning(
source_code: &str,
warning_substring: &str,
libraries: BTreeMap<String, BTreeMap<String, String>>,
pipeline: SolcPipeline,
skip_for_zkvm_edition: bool,
suppressed_warnings: Option<Vec<Warning>>,
) -> anyhow::Result<bool> {
check_dependencies();
let mut solc = SolcCompiler::new(SolcCompiler::DEFAULT_EXECUTABLE_NAME.to_owned())?;
let solc_version = solc.version()?;
if skip_for_zkvm_edition && solc_version.l2_revision.is_some() {
return Ok(true);
}
let mut sources = BTreeMap::new();
sources.insert("test.sol".to_string(), source_code.to_string());
let input = SolcStandardJsonInput::try_from_sources(
None,
sources.clone(),
libraries,
None,
SolcStandardJsonInputSettingsSelection::new_required(pipeline),
SolcStandardJsonInputSettingsOptimizer::new(
true,
None,
&solc_version.default,
false,
false,
),
None,
pipeline == SolcPipeline::Yul,
suppressed_warnings,
)?;
let output = solc.standard_json(input, pipeline, None, vec![], None)?;
let contains_warning = output
.errors
.ok_or_else(|| anyhow::anyhow!("Solidity compiler messages not found"))?
.iter()
.any(|error| error.formatted_message.contains(warning_substring));
Ok(contains_warning)
}
@@ -0,0 +1,4 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,24 @@
{
"name": "cli-tests",
"version": "1.0.0",
"title": "zksolc CLI Tests",
"description": "Auto tests for verifying zksolc CLI",
"repository": "https://github.com/matter-labs/era_compiler-solidity",
"main": "index.js",
"private": true,
"scripts": {
"test": "npx jest --verbose --testPathPattern="
},
"keywords": [],
"author": "Matter Labs",
"license": "MIT",
"devDependencies": {
"@types/jest": "^29.5.11",
"@types/shelljs": "^0.8.15",
"jest": "^29.7.0",
"shelljs": "^0.8.5",
"ts-jest": "^29.1.1",
"typescript": "^5.3.3"
}
}
@@ -0,0 +1,3 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.0;
contract C {}
@@ -0,0 +1,54 @@
object "Test" {
code {
{
mstore(64, 128)
if callvalue() { revert(0, 0) }
let _1 := datasize("Test_deployed")
codecopy(0, dataoffset("Test_deployed"), _1)
return(0, _1)
}
}
object "Test_deployed" {
code {
{
mstore(64, 128)
if iszero(lt(calldatasize(), 4))
{
let _1 := 0
switch shr(224, calldataload(_1))
case 0x3df4ddf4 {
if callvalue() { revert(_1, _1) }
if slt(add(calldatasize(), not(3)), _1) { revert(_1, _1) }
let memPos := allocate_memory(_1)
mstore(memPos, 0x2a)
return(memPos, 32)
}
case 0x5a8ac02d {
if callvalue() { revert(_1, _1) }
if slt(add(calldatasize(), not(3)), _1) { revert(_1, _1) }
let memPos_1 := allocate_memory(_1)
return(memPos_1, sub(abi_encode_uint256(memPos_1, 0x63), memPos_1))
}
}
revert(0, 0)
}
function abi_encode_uint256(headStart, value0) -> tail
{
tail := add(headStart, 32)
mstore(headStart, value0)
}
function allocate_memory(size) -> memPtr
{
memPtr := mload(64)
let newFreePtr := add(memPtr, and(add(size, 31), not(31)))
if or(gt(newFreePtr, 0xffffffffffffffff), lt(newFreePtr, memPtr))
{
mstore(0, shl(224, 0x4e487b71))
mstore(4, 0x41)
revert(0, 0x24)
}
mstore(64, newFreePtr)
}
}
}
}
@@ -0,0 +1,25 @@
.text
.file "main"
.globl __entry
__entry:
.func_begin0:
sub.s! 0, r2, r1
jump.eq @.BB0_2
add 32, r0, r1
st.1 r0, r1
st.1 r1, r0
add @CPI0_1[0], r0, r1
ret.ok.to_label r1, @DEFAULT_FAR_RETURN
.BB0_2:
add 42, r0, r1
st.1 r0, r1
add @CPI0_0[0], r0, r1
ret.ok.to_label r1, @DEFAULT_FAR_RETURN
.func_end0:
.note.GNU-stack
.rodata
CPI0_0:
.cell 2535301200456458802993406410752
CPI0_1:
.cell 5070602400912917605986812821504
@@ -0,0 +1,31 @@
import * as path from 'path';
const outputDir = 'artifacts';
const binExtension = ':C.zbin';
const asmExtension = ':C.zasm';
const contractSolFilename = 'contract.sol';
const contractYulFilename = 'contract.yul';
const contractZkasmFilename = 'contract.zkasm';
const pathToOutputDir = path.join( __dirname, '..', outputDir);
const pathToContracts = path.join( __dirname, '..', 'src', 'contracts');
const pathToBasicYulContract = path.join(pathToContracts, 'yul', contractYulFilename);
const pathToBasicZkasmContract = path.join(pathToContracts, 'zkasm', contractZkasmFilename);
const pathToBasicSolContract = path.join(pathToContracts, 'solidity', contractSolFilename);
const pathToSolBinOutputFile = path.join(pathToOutputDir, contractSolFilename + binExtension);
const pathToSolAsmOutputFile = path.join(pathToOutputDir, contractSolFilename + asmExtension);
export const paths = {
outputDir: outputDir,
binExtension: binExtension,
asmExtension: asmExtension,
contractSolFilename: contractSolFilename,
contractZkasmFilename: contractZkasmFilename,
contractYulFilename: contractYulFilename,
pathToOutputDir: pathToOutputDir,
pathToContracts: pathToContracts,
pathToBasicZkasmContract: pathToBasicZkasmContract,
pathToBasicSolContract: pathToBasicSolContract,
pathToBasicYulContract: pathToBasicYulContract,
pathToSolBinOutputFile: pathToSolBinOutputFile,
pathToSolAsmOutputFile: pathToSolAsmOutputFile,
};
@@ -0,0 +1,29 @@
import * as shell from 'shelljs';
import * as fs from 'fs';
interface CommandResult {
output: string;
exitCode: number;
}
export const executeCommand = (command: string): CommandResult => {
const result = shell.exec(command, {async: false});
return {
exitCode: result.code,
output: result.stdout.trim() || result.stderr.trim(),
};
};
export const isFolderExist = (folder: string): boolean => {
return shell.test('-d', folder);
};
export const isFileExist = (pathToFileDir: string, fileName: string, fileExtension:string): boolean => {
return shell.ls(pathToFileDir).stdout.includes(fileName + fileExtension);
};
export const isFileEmpty = (file: string): boolean => {
if (fs.existsSync(file)) {
return (fs.readFileSync(file).length === 0);
}
};
@@ -0,0 +1,39 @@
import {executeCommand} from "../src/helper";
import { paths } from '../src/entities';
//id1746
describe("Run with --asm by default", () => {
const command = `zksolc ${paths.pathToBasicSolContract} --asm`;
const result = executeCommand(command);
const commandInvalid = 'zksolc --asm';
const resultInvalid = executeCommand(commandInvalid);
it("Valid command exit code = 0", () => {
expect(result.exitCode).toBe(0);
});
it("--asm output is presented", () => {
expect(result.output).toMatch(/(__entry:)/i);
});
it("solc exit code == zksolc exit code", () => {
const command = `solc ${paths.pathToBasicSolContract} --asm`;
const solcResult = executeCommand(command);
expect(solcResult.exitCode).toBe(result.exitCode);
});
it("run invalid: zksolc --asm", () => {
expect(resultInvalid.output).toMatch(/(No input sources specified|Compilation aborted)/i);
});
it("Invalid command exit code = 1", () => {
expect(resultInvalid.exitCode).toBe(1);
});
it("Invalid solc exit code == Invalid zksolc exit code", () => {
const command = 'solc --asm';
const solcResult = executeCommand(command);
expect(solcResult.exitCode).toBe(resultInvalid.exitCode);
});
});
@@ -0,0 +1,78 @@
import {executeCommand, isFolderExist, isFileExist, isFileEmpty} from "../src/helper";
import { paths } from '../src/entities';
//id1762
describe("Run zksolc without any options", () => {
const command = 'zksolc';
const result = executeCommand(command);
it("Info with help is presented", () => {
expect(result.output).toMatch(/(No input sources specified|Error(s) found.)/i);
});
it("Exit code = 1", () => {
expect(result.exitCode).toBe(1);
});
it("solc exit code == zksolc exit code", () => {
const command = 'solc';
const solcResult = executeCommand(command);
expect(solcResult.exitCode).toBe(result.exitCode);
});
});
//#1713
describe("Default run a command from the help", () => {
const command = `zksolc ${paths.pathToBasicSolContract} -O3 --bin --output-dir "${paths.pathToOutputDir}"`; // potential issue on zksolc with full path on Windows cmd
const result = executeCommand(command);
it("Compiler run successful", () => {
expect(result.output).toMatch(/(Compiler run successful.)/i);
});
it("Exit code = 0", () => {
expect(result.exitCode).toBe(0);
});
it("Output dir is created", () => {
expect(isFolderExist(paths.pathToOutputDir)).toBe(true);
});
xit("Output file is created", () => { // a bug on windows
expect(isFileExist(paths.pathToOutputDir, paths.contractSolFilename, paths.binExtension)).toBe(true);
});
it("the output file is not empty", () => {
expect(isFileEmpty(paths.pathToSolBinOutputFile)).toBe(false);
});
it("No 'Error'/'Warning'/'Fail' in the output", () => {
expect(result.output).not.toMatch(/([Ee]rror|[Ww]arning|[Ff]ail)/i);
});
});
//#1818
describe("Default run a command from the help", () => {
const command = `zksolc ${paths.pathToBasicSolContract} -O3 --bin --asm --output-dir "${paths.pathToOutputDir}"`; // potential issue on zksolc with full path on Windows cmd
const result = executeCommand(command);
it("Compiler run successful", () => {
expect(result.output).toMatch(/(Compiler run successful.)/i);
});
it("Exit code = 0", () => {
expect(result.exitCode).toBe(0);
});
it("Output dir is created", () => {
expect(isFolderExist(paths.pathToOutputDir)).toBe(true);
});
xit("Output files are created", () => { // a bug on windows
expect(isFileExist(paths.pathToOutputDir, paths.contractSolFilename, paths.binExtension)).toBe(true);
expect(isFileExist(paths.pathToOutputDir, paths.contractSolFilename, paths.asmExtension)).toBe(true);
});
it("the output files are not empty", () => {
expect(isFileEmpty(paths.pathToSolBinOutputFile)).toBe(false);
expect(isFileEmpty(paths.pathToSolAsmOutputFile)).toBe(false);
});
it("No 'Error'/'Warning'/'Fail' in the output", () => {
expect(result.output).not.toMatch(/([Ee]rror|[Ww]arning|[Ff]ail)/i);
});
});
@@ -0,0 +1,40 @@
import {executeCommand} from "../src/helper";
import { paths } from '../src/entities';
//id1743
describe("Run with --yul by default", () => {
const command = `zksolc ${paths.pathToBasicYulContract} --yul`;
const result = executeCommand(command);
const commandInvalid = 'zksolc --yul';
const resultInvalid = executeCommand(commandInvalid);
it("Valid command exit code = 0", () => {
expect(result.exitCode).toBe(0);
});
it("--yul output is presented", () => {
expect(result.output).toMatch(/(Compiler run successful)/i);
expect(result.output).toMatch(/(No output requested)/i);
});
xit("solc exit code == zksolc exit code", () => { // unknown solc issue for datatype of the contract
const command = `solc ${paths.pathToBasicSolContract} --yul`;
const solcResult = executeCommand(command);
expect(solcResult.exitCode).toBe(result.exitCode);
});
it("run invalid: zksolc --yul", () => {
expect(resultInvalid.output).toMatch(/(The input file is missing)/i);
});
it("Invalid command exit code = 1", () => {
expect(resultInvalid.exitCode).toBe(1);
});
it("Invalid solc exit code == Invalid zksolc exit code", () => {
const command = 'solc --yul';
const solcResult = executeCommand(command);
expect(solcResult.exitCode).toBe(resultInvalid.exitCode);
});
});
@@ -0,0 +1,18 @@
import {executeCommand} from "../src/helper";
import { paths } from '../src/entities';
//id1745
describe("Run with --zkasm by default", () => {
const command = `zksolc ${paths.pathToBasicZkasmContract} --zkasm`;
const result = executeCommand(command);
it("Valid command exit code = 0", () => {
expect(result.exitCode).toBe(0);
});
it("--zkasm output is presented", () => {
expect(result.output).toMatch(/(Compiler run successful)/i);
expect(result.output).toMatch(/(No output requested)/i);
});
});
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"target": "ES6",
"module": "CommonJS",
"outDir": "./dist",
"rootDir": "./src"
}
}
@@ -0,0 +1,93 @@
//!
//! The Solidity compiler unit tests for factory dependencies.
//!
#![cfg(test)]
use std::collections::BTreeMap;
use crate::solc::pipeline::Pipeline as SolcPipeline;
pub const MAIN_CODE: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.16;
import "./callable.sol";
contract Main {
function main() external returns(uint256) {
Callable callable = new Callable();
callable.set(10);
return callable.get();
}
}
"#;
pub const CALLABLE_CODE: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.16;
contract Callable {
uint256 value;
function set(uint256 x) external {
value = x;
}
function get() external view returns(uint256) {
return value;
}
}
"#;
#[test]
fn default() {
let mut sources = BTreeMap::new();
sources.insert("main.sol".to_owned(), MAIN_CODE.to_owned());
sources.insert("callable.sol".to_owned(), CALLABLE_CODE.to_owned());
let output = super::build_solidity(
sources,
BTreeMap::new(),
None,
SolcPipeline::Yul,
era_compiler_llvm_context::OptimizerSettings::cycles(),
)
.expect("Build failure");
assert_eq!(
output
.contracts
.as_ref()
.expect("Missing field `contracts`")
.get("main.sol")
.expect("Missing file `main.sol`")
.get("Main")
.expect("Missing contract `main.sol:Main`")
.factory_dependencies
.as_ref()
.expect("Missing field `factory_dependencies`")
.len(),
1,
"Expected 1 factory dependency in `main.sol:Main`"
);
assert_eq!(
output
.contracts
.as_ref()
.expect("Missing field `contracts`")
.get("callable.sol")
.expect("Missing file `callable.sol`")
.get("Callable")
.expect("Missing contract `callable.sol:Callable`")
.factory_dependencies
.as_ref()
.expect("Missing field `factory_dependencies`")
.len(),
0,
"Expected 0 factory dependencies in `callable.sol:Callable`"
);
}
+122
View File
@@ -0,0 +1,122 @@
//!
//! The Solidity compiler unit tests for IR artifacts.
//!
//! The tests check if the IR artifacts are kept in the final output.
//!
#![cfg(test)]
use std::collections::BTreeMap;
use crate::solc::pipeline::Pipeline as SolcPipeline;
#[test]
fn yul() {
let source_code = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Test {
function main() public view returns (uint) {
return 42;
}
}
"#;
let mut sources = BTreeMap::new();
sources.insert("test.sol".to_owned(), source_code.to_owned());
let build = super::build_solidity(
sources,
BTreeMap::new(),
None,
SolcPipeline::Yul,
era_compiler_llvm_context::OptimizerSettings::cycles(),
)
.expect("Test failure");
assert!(
build
.contracts
.as_ref()
.expect("Always exists")
.get("test.sol")
.expect("Always exists")
.get("Test")
.expect("Always exists")
.ir_optimized
.is_some(),
"Yul IR is missing"
);
assert!(
build
.contracts
.as_ref()
.expect("Always exists")
.get("test.sol")
.expect("Always exists")
.get("Test")
.expect("Always exists")
.evm
.as_ref()
.expect("EVM object is missing")
.assembly
.is_none(),
"EVMLA IR is present although not requested"
);
}
#[test]
fn evmla() {
let source_code = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Test {
function main() public view returns (uint) {
return 42;
}
}
"#;
let mut sources = BTreeMap::new();
sources.insert("test.sol".to_owned(), source_code.to_owned());
let build = super::build_solidity(
sources,
BTreeMap::new(),
None,
SolcPipeline::EVMLA,
era_compiler_llvm_context::OptimizerSettings::cycles(),
)
.expect("Test failure");
assert!(
build
.contracts
.as_ref()
.expect("Always exists")
.get("test.sol")
.expect("Always exists")
.get("Test")
.expect("Always exists")
.evm
.as_ref()
.expect("EVM object is missing")
.assembly
.is_some(),
"EVMLA IR is missing",
);
assert!(
build
.contracts
.as_ref()
.expect("Always exists")
.get("test.sol")
.expect("Always exists")
.get("Test")
.expect("Always exists")
.ir_optimized
.is_none(),
"Yul IR is present although not requested",
);
}
+104
View File
@@ -0,0 +1,104 @@
//!
//! The Solidity compiler unit tests for libraries.
//!
#![cfg(test)]
use std::collections::BTreeMap;
use crate::solc::pipeline::Pipeline as SolcPipeline;
pub const LIBRARY_TEST_SOURCE: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// A simple library with at least one external method
library SimpleLibrary {
function add(uint256 a, uint256 b) external pure returns (uint256) {
return a + b;
}
}
// A contract calling that library
contract SimpleContract {
using SimpleLibrary for uint256;
function performAlgorithm(uint256 a, uint256 b) public pure returns (uint256) {
uint sum = 0;
if (a > b) {
while (true) {
sum += a.add(b);
}
}
return sum;
}
}
"#;
#[test]
fn not_specified() {
let mut sources = BTreeMap::new();
sources.insert("test.sol".to_owned(), LIBRARY_TEST_SOURCE.to_owned());
for pipeline in [SolcPipeline::EVMLA, SolcPipeline::Yul] {
let output = super::build_solidity_and_detect_missing_libraries(
sources.clone(),
BTreeMap::new(),
pipeline,
)
.expect("Test failure");
assert!(
output
.contracts
.as_ref()
.expect("Always exists")
.get("test.sol")
.expect("Always exists")
.get("SimpleContract")
.expect("Always exists")
.missing_libraries
.as_ref()
.expect("Always exists")
.contains("test.sol:SimpleLibrary"),
"Missing library not detected"
);
}
}
#[test]
fn specified() {
let mut sources = BTreeMap::new();
sources.insert("test.sol".to_owned(), LIBRARY_TEST_SOURCE.to_owned());
let mut libraries = BTreeMap::new();
libraries
.entry("test.sol".to_string())
.or_insert_with(BTreeMap::new)
.entry("SimpleLibrary".to_string())
.or_insert("0x00000000000000000000000000000000DEADBEEF".to_string());
for pipeline in [SolcPipeline::EVMLA, SolcPipeline::Yul] {
let output = super::build_solidity_and_detect_missing_libraries(
sources.clone(),
libraries.clone(),
pipeline,
)
.expect("Test failure");
assert!(
output
.contracts
.as_ref()
.expect("Always exists")
.get("test.sol")
.expect("Always exists")
.get("SimpleContract")
.expect("Always exists")
.missing_libraries
.as_ref()
.cloned()
.unwrap_or_default()
.is_empty(),
"The list of missing libraries must be empty"
);
}
}
+394
View File
@@ -0,0 +1,394 @@
//!
//! The Solidity compiler unit tests for messages.
//!
#![cfg(test)]
use std::collections::BTreeMap;
use crate::solc::pipeline::Pipeline as SolcPipeline;
use crate::warning::Warning;
pub const ECRECOVER_TEST_SOURCE: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ECRecoverExample {
function recoverAddress(
bytes32 messageHash,
uint8 v,
bytes32 r,
bytes32 s
) public pure returns (address) {
return ecrecover(messageHash, v, r, s);
}
}
"#;
#[test]
fn ecrecover() {
assert!(
super::check_solidity_warning(
ECRECOVER_TEST_SOURCE,
"Warning: It looks like you are using 'ecrecover' to validate a signature of a user account.",
BTreeMap::new(),
SolcPipeline::Yul,
false,
None,
).expect("Test failure")
);
}
#[test]
fn ecrecover_suppressed() {
assert!(
!super::check_solidity_warning(
ECRECOVER_TEST_SOURCE,
"Warning: It looks like you are using 'ecrecover' to validate a signature of a user account.",
BTreeMap::new(),
SolcPipeline::Yul,
false,
Some(vec![Warning::EcRecover]),
).expect("Test failure")
);
}
pub const SEND_TEST_SOURCE: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SendExample {
address payable public recipient;
constructor(address payable _recipient) {
recipient = _recipient;
}
function forwardEther() external payable {
bool success = recipient.send(msg.value);
require(success, "Failed to send Ether");
}
}
"#;
#[test]
fn send() {
assert!(
super::check_solidity_warning(
SEND_TEST_SOURCE,
"Warning: It looks like you are using '<address payable>.send/transfer(<X>)' without providing",
BTreeMap::new(),
SolcPipeline::Yul,
false,
None,
).expect("Test failure")
);
}
#[test]
fn send_suppressed() {
assert!(
!super::check_solidity_warning(
SEND_TEST_SOURCE,
"Warning: It looks like you are using '<address payable>.send/transfer(<X>)' without providing",
BTreeMap::new(),
SolcPipeline::Yul,
false,
Some(vec![Warning::SendTransfer]),
).expect("Test failure")
);
}
pub const TRANSFER_TEST_SOURCE: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract TransferExample {
address payable public recipient;
constructor(address payable _recipient) {
recipient = _recipient;
}
function forwardEther() external payable {
recipient.transfer(msg.value);
}
}
"#;
#[test]
fn transfer() {
assert!(
super::check_solidity_warning(
TRANSFER_TEST_SOURCE,
"Warning: It looks like you are using '<address payable>.send/transfer(<X>)' without providing",
BTreeMap::new(),
SolcPipeline::Yul,
false,
None,
).expect("Test failure")
);
}
#[test]
fn transfer_suppressed() {
assert!(
!super::check_solidity_warning(
TRANSFER_TEST_SOURCE,
"Warning: It looks like you are using '<address payable>.send/transfer(<X>)' without providing",
BTreeMap::new(),
SolcPipeline::Yul,
false,
Some(vec![Warning::SendTransfer]),
).expect("Test failure")
);
}
pub const EXTCODESIZE_TEST_SOURCE: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ExternalCodeSize {
function getExternalCodeSize(address target) public view returns (uint256) {
uint256 codeSize;
assembly {
codeSize := extcodesize(target)
}
return codeSize;
}
}
"#;
#[test]
fn extcodesize() {
assert!(super::check_solidity_warning(
EXTCODESIZE_TEST_SOURCE,
"Warning: Your code or one of its dependencies uses the 'extcodesize' instruction,",
BTreeMap::new(),
SolcPipeline::Yul,
false,
None,
)
.expect("Test failure"));
}
#[test]
fn extcodesize_suppressed() {
assert!(!super::check_solidity_warning(
EXTCODESIZE_TEST_SOURCE,
"Warning: Your code or one of its dependencies uses the 'extcodesize' instruction,",
BTreeMap::new(),
SolcPipeline::Yul,
false,
Some(vec![Warning::ExtCodeSize]),
)
.expect("Test failure"));
}
pub const TX_ORIGIN_TEST_SOURCE: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract TxOriginExample {
function isOriginSender() public view returns (bool) {
return tx.origin == msg.sender;
}
}
"#;
#[test]
fn tx_origin() {
assert!(super::check_solidity_warning(
TX_ORIGIN_TEST_SOURCE,
"Warning: You are checking for 'tx.origin' in your code, which might lead to",
BTreeMap::new(),
SolcPipeline::Yul,
false,
None,
)
.expect("Test failure"));
}
#[test]
fn tx_origin_suppressed() {
assert!(!super::check_solidity_warning(
TX_ORIGIN_TEST_SOURCE,
"Warning: You are checking for 'tx.origin' in your code, which might lead to",
BTreeMap::new(),
SolcPipeline::Yul,
false,
Some(vec![Warning::TxOrigin]),
)
.expect("Test failure"));
}
pub const TX_ORIGIN_ASSEMBLY_TEST_SOURCE: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract TxOriginExample {
function isOriginSender() public view returns (bool) {
address txOrigin;
address sender = msg.sender;
assembly {
txOrigin := origin() // Get the transaction origin using the 'origin' instruction
}
return txOrigin == sender;
}
}
"#;
#[test]
fn tx_origin_assembly() {
assert!(super::check_solidity_warning(
TX_ORIGIN_ASSEMBLY_TEST_SOURCE,
"Warning: You are checking for 'tx.origin' in your code, which might lead to",
BTreeMap::new(),
SolcPipeline::Yul,
false,
None,
)
.expect("Test failure"));
}
#[test]
fn tx_origin_assembly_suppressed() {
assert!(!super::check_solidity_warning(
TX_ORIGIN_ASSEMBLY_TEST_SOURCE,
"Warning: You are checking for 'tx.origin' in your code, which might lead to",
BTreeMap::new(),
SolcPipeline::Yul,
false,
Some(vec![Warning::TxOrigin]),
)
.expect("Test failure"));
}
#[test]
fn internal_function_pointer_argument() {
let source_code = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract InternalFunctionPointerExample {
function add(uint256 a, uint256 b) internal pure returns (uint256) {
return a + b;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
return a - b;
}
function executeOperation(
function (uint256, uint256) internal pure returns (uint256) operation,
uint256 a,
uint256 b
) private pure returns (uint256) {
return operation(a, b);
}
function testAdd(uint256 a, uint256 b) public pure returns (uint256) {
return executeOperation(add, a, b);
}
function testSub(uint256 a, uint256 b) public pure returns (uint256) {
return executeOperation(sub, a, b);
}
}
"#;
assert!(super::check_solidity_warning(
source_code,
"Error: Internal function pointers are not supported in EVM legacy assembly pipeline.",
BTreeMap::new(),
SolcPipeline::EVMLA,
true,
None,
)
.expect("Test failure"));
}
#[test]
fn internal_function_pointer_stack() {
let source_code = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract StackFunctionPointerExample {
function add(uint256 a, uint256 b) internal pure returns (uint256) {
return a + b;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
return a - b;
}
function testAdd(uint256 a, uint256 b) public pure returns (uint256) {
function (uint256, uint256) internal pure returns (uint256) operation = add;
return operation(a, b);
}
function testSub(uint256 a, uint256 b) public pure returns (uint256) {
function (uint256, uint256) internal pure returns (uint256) operation = sub;
return operation(a, b);
}
}
"#;
assert!(super::check_solidity_warning(
source_code,
"Error: Internal function pointers are not supported in EVM legacy assembly pipeline.",
BTreeMap::new(),
SolcPipeline::EVMLA,
true,
None,
)
.expect("Test failure"));
}
#[test]
fn internal_function_pointer_storage() {
let source_code = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract StorageFunctionPointerExample {
function add(uint256 a, uint256 b) internal pure returns (uint256) {
return a + b;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
return a - b;
}
function (uint256, uint256) internal pure returns (uint256) operation;
bool private isOperationSet = false;
function setOperation(bool isAdd) public {
if (isAdd) {
operation = add;
} else {
operation = sub;
}
isOperationSet = true;
}
function executeOperation(uint256 a, uint256 b) public view returns (uint256) {
require(isOperationSet, "Operation not set");
return operation(a, b);
}
}
"#;
assert!(super::check_solidity_warning(
source_code,
"Error: Internal function pointers are not supported in EVM legacy assembly pipeline.",
BTreeMap::new(),
SolcPipeline::EVMLA,
true,
None,
)
.expect("Test failure"));
}
+16
View File
@@ -0,0 +1,16 @@
//!
//! The Solidity compiler unit tests.
//!
#![cfg(test)]
mod factory_dependency;
mod ir_artifacts;
mod libraries;
mod messages;
mod optimizer;
mod remappings;
mod runtime_code;
mod unsupported_opcodes;
pub(crate) use super::test_utils::*;
+137
View File
@@ -0,0 +1,137 @@
//!
//! The Solidity compiler unit tests for the optimizer.
//!
#![cfg(test)]
use std::collections::BTreeMap;
use crate::solc::pipeline::Pipeline as SolcPipeline;
pub const SOURCE_CODE: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity >=0.5.0;
contract Test {
uint8 constant ARRAY_SIZE = 40;
uint128 constant P = 257;
uint128 constant MODULO = 1000000007;
function complex() public pure returns(uint64) {
uint8[ARRAY_SIZE] memory array;
// generate array where first half equals second
for(uint8 i = 0; i < ARRAY_SIZE; i++) {
array[i] = (i % (ARRAY_SIZE / 2)) * (255 / (ARRAY_SIZE / 2 - 1));
}
bool result = true;
for(uint8 i = 0; i < ARRAY_SIZE/2; i++) {
result = result && hash(array, 0, i + 1) == hash(array, ARRAY_SIZE/2, ARRAY_SIZE/2 + i + 1)
&& hash(array, i, ARRAY_SIZE/2) == hash(array, i + ARRAY_SIZE/2, ARRAY_SIZE);
}
if (result) {
return 1;
} else {
return 0;
}
}
function hash(uint8[ARRAY_SIZE] memory array, uint8 begin, uint8 end) private pure returns(uint128) {
uint128 h = 0;
for(uint8 i = begin; i < end; i++) {
h = (h * P + array[i]) % MODULO;
}
return h;
}
}
"#;
#[test]
fn optimizer() {
let mut sources = BTreeMap::new();
sources.insert("test.sol".to_owned(), SOURCE_CODE.to_owned());
let build_unoptimized = super::build_solidity(
sources.clone(),
BTreeMap::new(),
None,
SolcPipeline::Yul,
era_compiler_llvm_context::OptimizerSettings::none(),
)
.expect("Build failure");
let build_optimized_for_cycles = super::build_solidity(
sources.clone(),
BTreeMap::new(),
None,
SolcPipeline::Yul,
era_compiler_llvm_context::OptimizerSettings::cycles(),
)
.expect("Build failure");
let build_optimized_for_size = super::build_solidity(
sources,
BTreeMap::new(),
None,
SolcPipeline::Yul,
era_compiler_llvm_context::OptimizerSettings::size(),
)
.expect("Build failure");
let size_when_unoptimized = build_unoptimized
.contracts
.as_ref()
.expect("Missing field `contracts`")
.get("test.sol")
.expect("Missing file `test.sol`")
.get("Test")
.expect("Missing contract `test.sol:Test`")
.evm
.as_ref()
.expect("Missing EVM data")
.bytecode
.as_ref()
.expect("Missing bytecode")
.object
.len();
let size_when_optimized_for_cycles = build_optimized_for_cycles
.contracts
.as_ref()
.expect("Missing field `contracts`")
.get("test.sol")
.expect("Missing file `test.sol`")
.get("Test")
.expect("Missing contract `test.sol:Test`")
.evm
.as_ref()
.expect("Missing EVM data")
.bytecode
.as_ref()
.expect("Missing bytecode")
.object
.len();
let size_when_optimized_for_size = build_optimized_for_size
.contracts
.as_ref()
.expect("Missing field `contracts`")
.get("test.sol")
.expect("Missing file `test.sol`")
.get("Test")
.expect("Missing contract `test.sol:Test`")
.evm
.as_ref()
.expect("Missing EVM data")
.bytecode
.as_ref()
.expect("Missing bytecode")
.object
.len();
assert!(
size_when_optimized_for_cycles < size_when_unoptimized,
"Expected the cycles-optimized bytecode to be smaller than the unoptimized. Optimized: {}B, Unoptimized: {}B", size_when_optimized_for_cycles, size_when_unoptimized,
);
assert!(
size_when_optimized_for_size < size_when_unoptimized,
"Expected the size-optimized bytecode to be smaller than the unoptimized. Optimized: {}B, Unoptimized: {}B", size_when_optimized_for_size, size_when_unoptimized,
);
}
+55
View File
@@ -0,0 +1,55 @@
//!
//! The Solidity compiler unit tests for remappings.
//!
#![cfg(test)]
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use crate::solc::pipeline::Pipeline as SolcPipeline;
pub const CALLEE_TEST_SOURCE: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.16;
contract Callable {
function f(uint a) public pure returns(uint) {
return a * 2;
}
}
"#;
pub const CALLER_TEST_SOURCE: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.16;
import "libraries/default/callable.sol";
contract Main {
function main(Callable callable) public returns(uint) {
return callable.f(5);
}
}
"#;
#[test]
fn default() {
let mut sources = BTreeMap::new();
sources.insert("./test.sol".to_owned(), CALLER_TEST_SOURCE.to_owned());
sources.insert("./callable.sol".to_owned(), CALLEE_TEST_SOURCE.to_owned());
let mut remappings = BTreeSet::new();
remappings.insert("libraries/default/=./".to_owned());
super::build_solidity(
sources,
BTreeMap::new(),
Some(remappings),
SolcPipeline::Yul,
era_compiler_llvm_context::OptimizerSettings::cycles(),
)
.expect("Test failure");
}
+38
View File
@@ -0,0 +1,38 @@
//!
//! The Solidity compiler unit tests for runtime code.
//!
#![cfg(test)]
use std::collections::BTreeMap;
use crate::solc::pipeline::Pipeline as SolcPipeline;
#[test]
#[should_panic(expected = "runtimeCode is not supported")]
fn default() {
let source_code = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract A {}
contract Test {
function main() public pure returns(bytes memory) {
return type(A).runtimeCode;
}
}
"#;
let mut sources = BTreeMap::new();
sources.insert("test.sol".to_owned(), source_code.to_owned());
super::build_solidity(
sources,
BTreeMap::new(),
None,
SolcPipeline::Yul,
era_compiler_llvm_context::OptimizerSettings::cycles(),
)
.expect("Test failure");
}
@@ -0,0 +1,222 @@
//!
//! The Solidity compiler unit tests for unsupported opcodes.
//!
#![cfg(test)]
use std::collections::BTreeMap;
use crate::solc::pipeline::Pipeline as SolcPipeline;
#[test]
#[should_panic(expected = "The `CODECOPY` instruction is not supported")]
fn codecopy_yul_runtime() {
let source_code = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract FixedCodeCopy {
function copyCode() public view returns (bytes memory) {
uint256 fixedCodeSize = 64;
bytes memory code = new bytes(fixedCodeSize);
assembly {
codecopy(add(code, 0x20), 0, fixedCodeSize)
}
return code;
}
}
"#;
let mut sources = BTreeMap::new();
sources.insert("test.sol".to_owned(), source_code.to_owned());
super::build_solidity(
sources,
BTreeMap::new(),
None,
SolcPipeline::Yul,
era_compiler_llvm_context::OptimizerSettings::cycles(),
)
.expect("Test failure");
}
pub const CALLCODE_TEST_SOURCE: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract CallcodeTest {
function testCallcode(address target, bytes4 signature, uint256 inputValue) public returns (bool) {
bool success;
assembly {
let input := mload(0x40)
mstore(input, signature)
mstore(add(input, 0x04), inputValue)
let callResult := callcode(gas(), target, 0, input, 0x24, 0, 0)
success := and(callResult, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)
}
return success;
}
}
"#;
#[test]
#[should_panic(expected = "The `CALLCODE` instruction is not supported")]
fn callcode_evmla() {
let mut sources = BTreeMap::new();
sources.insert("test.sol".to_owned(), CALLCODE_TEST_SOURCE.to_owned());
super::build_solidity(
sources,
BTreeMap::new(),
None,
SolcPipeline::EVMLA,
era_compiler_llvm_context::OptimizerSettings::cycles(),
)
.expect("Test failure");
}
#[test]
#[should_panic(expected = "The `CALLCODE` instruction is not supported")]
fn callcode_yul() {
let mut sources = BTreeMap::new();
sources.insert("test.sol".to_owned(), CALLCODE_TEST_SOURCE.to_owned());
super::build_solidity(
sources,
BTreeMap::new(),
None,
SolcPipeline::Yul,
era_compiler_llvm_context::OptimizerSettings::cycles(),
)
.expect("Test failure");
}
#[test]
#[should_panic(expected = "The `PC` instruction is not supported")]
fn pc_yul() {
let source_code = r#"
object "ProgramCounter" {
code {
datacopy(0, dataoffset("ProgramCounter_deployed"), datasize("ProgramCounter_deployed"))
return(0, datasize("ProgramCounter_deployed"))
}
object "ProgramCounter_deployed" {
code {
function getPC() -> programCounter {
programCounter := pc()
}
let pcValue := getPC()
sstore(0, pcValue)
}
}
}
"#;
super::build_yul(source_code).expect("Test failure");
}
pub const EXTCODECOPY_TEST_SOURCE: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ExternalCodeCopy {
function copyExternalCode(address target, uint256 codeSize) public view returns (bytes memory) {
bytes memory code = new bytes(codeSize);
assembly {
extcodecopy(target, add(code, 0x20), 0, codeSize)
}
return code;
}
}
"#;
#[test]
#[should_panic(expected = "The `EXTCODECOPY` instruction is not supported")]
fn extcodecopy_evmla() {
let mut sources = BTreeMap::new();
sources.insert("test.sol".to_owned(), EXTCODECOPY_TEST_SOURCE.to_owned());
super::build_solidity(
sources,
BTreeMap::new(),
None,
SolcPipeline::EVMLA,
era_compiler_llvm_context::OptimizerSettings::cycles(),
)
.expect("Test failure");
}
#[test]
#[should_panic(expected = "The `EXTCODECOPY` instruction is not supported")]
fn extcodecopy_yul() {
let mut sources = BTreeMap::new();
sources.insert("test.sol".to_owned(), EXTCODECOPY_TEST_SOURCE.to_owned());
super::build_solidity(
sources,
BTreeMap::new(),
None,
SolcPipeline::Yul,
era_compiler_llvm_context::OptimizerSettings::cycles(),
)
.expect("Test failure");
}
pub const SELFDESTRUCT_TEST_SOURCE: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MinimalDestructible {
address payable public owner;
constructor() {
owner = payable(msg.sender);
}
function destroy() public {
require(msg.sender == owner, "Only the owner can call this function.");
selfdestruct(owner);
}
}
"#;
#[test]
#[should_panic(expected = "The `SELFDESTRUCT` instruction is not supported")]
fn selfdestruct_evmla() {
let mut sources = BTreeMap::new();
sources.insert("test.sol".to_owned(), SELFDESTRUCT_TEST_SOURCE.to_owned());
super::build_solidity(
sources,
BTreeMap::new(),
None,
SolcPipeline::EVMLA,
era_compiler_llvm_context::OptimizerSettings::cycles(),
)
.expect("Test failure");
}
#[test]
#[should_panic(expected = "The `SELFDESTRUCT` instruction is not supported")]
fn selfdestruct_yul() {
let mut sources = BTreeMap::new();
sources.insert("test.sol".to_owned(), SELFDESTRUCT_TEST_SOURCE.to_owned());
super::build_solidity(
sources,
BTreeMap::new(),
None,
SolcPipeline::Yul,
era_compiler_llvm_context::OptimizerSettings::cycles(),
)
.expect("Test failure");
}
+58
View File
@@ -0,0 +1,58 @@
//!
//! The compiler warning.
//!
use std::str::FromStr;
use serde::Deserialize;
use serde::Serialize;
///
/// The compiler warning.
///
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Warning {
/// The warning for eponymous feature.
EcRecover,
/// The warning for eponymous feature.
SendTransfer,
/// The warning for eponymous feature.
ExtCodeSize,
/// The warning for eponymous feature.
TxOrigin,
/// The warning for eponymous feature.
BlockTimestamp,
/// The warning for eponymous feature.
BlockNumber,
/// The warning for eponymous feature.
BlockHash,
}
impl Warning {
///
/// Converts string arguments into an array of warnings.
///
pub fn try_from_strings(strings: &[String]) -> Result<Vec<Self>, anyhow::Error> {
strings
.iter()
.map(|string| Self::from_str(string))
.collect()
}
}
impl FromStr for Warning {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<Self, Self::Err> {
match string {
"ecrecover" => Ok(Self::EcRecover),
"sendtransfer" => Ok(Self::SendTransfer),
"extcodesize" => Ok(Self::ExtCodeSize),
"txorigin" => Ok(Self::TxOrigin),
"blocktimestamp" => Ok(Self::BlockTimestamp),
"blocknumber" => Ok(Self::BlockNumber),
"blockhash" => Ok(Self::BlockHash),
_ => Err(anyhow::anyhow!("Invalid warning: {}", string)),
}
}
}
+19
View File
@@ -0,0 +1,19 @@
//!
//! The Yul IR error.
//!
use crate::yul::lexer::error::Error as LexerError;
use crate::yul::parser::error::Error as ParserError;
///
/// The Yul IR error.
///
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum Error {
/// The lexer error.
#[error("Lexical error: {0}")]
Lexer(#[from] LexerError),
/// The parser error.
#[error("Syntax error: {0}")]
Parser(#[from] ParserError),
}
+20
View File
@@ -0,0 +1,20 @@
//!
//! The Yul IR lexer error.
//!
use crate::yul::lexer::token::location::Location;
///
/// The Yul IR lexer error.
///
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum Error {
/// The invalid lexeme error.
#[error("{location} Invalid character sequence `{sequence}`")]
InvalidLexeme {
/// The lexeme location.
location: Location,
/// The invalid sequence of characters.
sequence: String,
},
}
+137
View File
@@ -0,0 +1,137 @@
//!
//! The compiler lexer.
//!
pub mod error;
pub mod token;
#[cfg(test)]
mod tests;
use self::error::Error;
use self::token::lexeme::comment::Comment;
use self::token::lexeme::identifier::Identifier;
use self::token::lexeme::literal::integer::Integer as IntegerLiteral;
use self::token::lexeme::literal::string::String as StringLiteral;
use self::token::lexeme::symbol::Symbol;
use self::token::lexeme::Lexeme;
use self::token::location::Location;
use self::token::Token;
///
/// The compiler lexer.
///
pub struct Lexer {
/// The input source code.
input: String,
/// The number of characters processed so far.
offset: usize,
/// The current location.
location: Location,
/// The peeked lexeme, waiting to be fetched.
peeked: Option<Token>,
}
impl Lexer {
///
/// A shortcut constructor.
///
pub fn new(mut input: String) -> Self {
input.push('\n');
Self {
input,
offset: 0,
location: Location::default(),
peeked: None,
}
}
///
/// Advances the lexer, returning the next lexeme.
///
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Result<Token, Error> {
if let Some(peeked) = self.peeked.take() {
return Ok(peeked);
}
while self.offset < self.input.len() {
let input = &self.input[self.offset..];
if input.starts_with(|character| char::is_ascii_whitespace(&character)) {
if input.starts_with('\n') {
self.location.line += 1;
self.location.column = 1;
} else if !input.starts_with('\r') {
self.location.column += 1;
}
self.offset += 1;
continue;
}
if let Some(token) = Comment::parse(input) {
self.offset += token.length;
self.location
.shift_down(token.location.line, token.location.column);
continue;
}
if let Some(mut token) = StringLiteral::parse(input) {
token.location = self.location;
self.offset += token.length;
self.location.shift_right(token.length);
return Ok(token);
}
if let Some(mut token) = IntegerLiteral::parse(input) {
token.location = self.location;
self.offset += token.length;
self.location.shift_right(token.length);
return Ok(token);
}
if let Some(mut token) = Identifier::parse(input) {
token.location = self.location;
self.offset += token.length;
self.location.shift_right(token.length);
return Ok(token);
}
if let Some(mut token) = Symbol::parse(input) {
token.location = self.location;
self.offset += token.length;
self.location.shift_right(token.length);
return Ok(token);
}
let end = self.input[self.offset..]
.find(char::is_whitespace)
.unwrap_or(self.input.len());
return Err(Error::InvalidLexeme {
location: self.location,
sequence: self.input[self.offset..self.offset + end].to_owned(),
});
}
Ok(Token::new(self.location, Lexeme::EndOfFile, 0))
}
///
/// Peeks the next lexeme without advancing the iterator.
///
pub fn peek(&mut self) -> Result<Token, Error> {
match self.peeked {
Some(ref peeked) => Ok(peeked.clone()),
None => {
let peeked = self.next()?;
self.peeked = Some(peeked.clone());
Ok(peeked)
}
}
}
}
+91
View File
@@ -0,0 +1,91 @@
//!
//! The Yul IR lexer tests.
//!
use crate::yul::lexer::error::Error;
use crate::yul::lexer::token::lexeme::Lexeme;
use crate::yul::lexer::token::location::Location;
use crate::yul::lexer::Lexer;
#[test]
fn default() {
let input = r#"
object "Test" {
code {
{
/*
The deploy code.
*/
mstore(64, 128)
if callvalue() { revert(0, 0) }
let _1 := datasize("Test_deployed")
codecopy(0, dataoffset("Test_deployed"), _1)
return(0, _1)
}
}
object "Test_deployed" {
code {
{
/*
The runtime code.
*/
mstore(64, 128)
if iszero(lt(calldatasize(), 4))
{
let _1 := 0
switch shr(224, calldataload(_1))
case 0x3df4ddf4 {
if callvalue() { revert(_1, _1) }
if slt(add(calldatasize(), not(3)), _1) { revert(_1, _1) }
let memPos := allocate_memory(_1)
mstore(memPos, 0x2a)
return(memPos, 32)
}
case 0x5a8ac02d {
if callvalue() { revert(_1, _1) }
if slt(add(calldatasize(), not(3)), _1) { revert(_1, _1) }
let memPos_1 := allocate_memory(_1)
return(memPos_1, sub(abi_encode_uint256(memPos_1, 0x63), memPos_1))
}
}
revert(0, 0)
}
function abi_encode_uint256(headStart, value0) -> tail
{
tail := add(headStart, 32)
mstore(headStart, value0)
}
function allocate_memory(size) -> memPtr
{
memPtr := mload(64)
let newFreePtr := add(memPtr, and(add(size, 31), not(31)))
if or(gt(newFreePtr, 0xffffffffffffffff)#, lt(newFreePtr, memPtr))
{
mstore(0, shl(224, 0x4e487b71))
mstore(4, 0x41)
revert(0, 0x24)
}
mstore(64, newFreePtr)
}
}
}
}
"#;
let mut lexer = Lexer::new(input.to_owned());
loop {
match lexer.next() {
Ok(token) => assert_ne!(token.lexeme, Lexeme::EndOfFile),
Err(error) => {
assert_eq!(
error,
Error::InvalidLexeme {
location: Location::new(51, 57),
sequence: "#,".to_owned(),
}
);
break;
}
}
}
}
@@ -0,0 +1,38 @@
//!
//! The comment lexeme.
//!
pub mod multi_line;
pub mod single_line;
use crate::yul::lexer::token::Token;
use self::multi_line::Comment as MultiLineComment;
use self::single_line::Comment as SingleLineComment;
///
/// The comment lexeme.
///
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(dead_code)]
pub enum Comment {
/// The single-line comment.
SingleLine(SingleLineComment),
/// The multi-line comment.
MultiLine(MultiLineComment),
}
impl Comment {
///
/// Returns the comment's length, including the trimmed whitespace around it.
///
pub fn parse(input: &str) -> Option<Token> {
if input.starts_with(SingleLineComment::START) {
Some(SingleLineComment::parse(input))
} else if input.starts_with(MultiLineComment::START) {
Some(MultiLineComment::parse(input))
} else {
None
}
}
}
@@ -0,0 +1,37 @@
//!
//! The multi-line comment lexeme.
//!
use crate::yul::lexer::token::lexeme::Lexeme;
use crate::yul::lexer::token::location::Location;
use crate::yul::lexer::token::Token;
///
/// The multi-line comment lexeme.
///
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Comment {}
impl Comment {
/// The start symbol.
pub const START: &'static str = "/*";
/// The end symbol.
pub const END: &'static str = "*/";
///
/// Returns the comment, including its length and number of lines.
///
pub fn parse(input: &str) -> Token {
let end_position = input.find(Self::END).unwrap_or(input.len());
let input = &input[..end_position];
let length = end_position + Self::END.len();
let lines = input.matches('\n').count();
let columns = match input.rfind('\n') {
Some(new_line) => end_position - (new_line + 1),
None => end_position,
};
Token::new(Location::new(lines, columns), Lexeme::Comment, length)
}
}
@@ -0,0 +1,30 @@
//!
//! The single-line comment lexeme.
//!
use crate::yul::lexer::token::lexeme::Lexeme;
use crate::yul::lexer::token::location::Location;
use crate::yul::lexer::token::Token;
///
/// The single-line comment lexeme.
///
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Comment {}
impl Comment {
/// The start symbol.
pub const START: &'static str = "//";
/// The end symbol.
pub const END: &'static str = "\n";
///
/// Returns the comment's length, including the trimmed whitespace around it.
///
pub fn parse(input: &str) -> Token {
let end_position = input.find(Self::END).unwrap_or(input.len());
let length = end_position + Self::END.len();
Token::new(Location::new(1, 1), Lexeme::Comment, length)
}
}
@@ -0,0 +1,80 @@
//!
//! The identifier lexeme.
//!
use crate::yul::lexer::token::lexeme::keyword::Keyword;
use crate::yul::lexer::token::lexeme::Lexeme;
use crate::yul::lexer::token::location::Location;
use crate::yul::lexer::token::Token;
///
/// The identifier lexeme.
///
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Identifier {
/// The inner string.
pub inner: String,
}
impl Identifier {
///
/// A shortcut constructor.
///
pub fn new(inner: String) -> Self {
Self { inner }
}
///
/// Parses the identifier, returning it as a token.
///
pub fn parse(input: &str) -> Option<Token> {
if !input.starts_with(Self::can_begin) {
return None;
}
let end = input.find(Self::cannot_continue).unwrap_or(input.len());
let inner = input[..end].to_string();
let length = inner.len();
if let Some(token) = Keyword::parse(inner.as_str()) {
return Some(token);
}
Some(Token::new(
Location::new(0, length),
Lexeme::Identifier(Self::new(inner)),
length,
))
}
///
/// Checks whether the character can begin an identifier.
///
pub fn can_begin(character: char) -> bool {
character.is_alphabetic() || character == '_' || character == '$'
}
///
/// Checks whether the character can continue an identifier.
///
pub fn can_continue(character: char) -> bool {
Self::can_begin(character)
|| character.is_numeric()
|| character == '_'
|| character == '$'
|| character == '.'
}
///
/// Checks whether the character cannot continue an identifier.
///
pub fn cannot_continue(character: char) -> bool {
!Self::can_continue(character)
}
}
impl std::fmt::Display for Identifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.inner)
}
}
@@ -0,0 +1,158 @@
//!
//! The keyword lexeme.
//!
use crate::yul::lexer::token::lexeme::literal::boolean::Boolean as BooleanLiteral;
use crate::yul::lexer::token::lexeme::literal::Literal;
use crate::yul::lexer::token::lexeme::Lexeme;
use crate::yul::lexer::token::location::Location;
use crate::yul::lexer::token::Token;
///
/// The keyword lexeme.
///
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Keyword {
/// The `object` keyword.
Object,
/// The `code` keyword.
Code,
/// The `function` keyword.
Function,
/// The `let` keyword.
Let,
/// The `if` keyword.
If,
/// The `switch` keyword.
Switch,
/// The `case` keyword.
Case,
/// The `default` keyword.
Default,
/// The `for` keyword.
For,
/// The `break` keyword.
Break,
/// The `continue` keyword.
Continue,
/// The `leave` keyword.
Leave,
/// The `true` keyword.
True,
/// The `false` keyword.
False,
/// The `bool` keyword.
Bool,
/// The `int{N}` keyword.
Int(usize),
/// The `uint{N}` keyword.
Uint(usize),
}
impl Keyword {
///
/// Parses the keyword, returning it as a token.
///
pub fn parse(input: &str) -> Option<Token> {
let keyword = Self::parse_keyword(input)?;
let lexeme = match BooleanLiteral::try_from(keyword) {
Ok(literal) => Lexeme::Literal(Literal::Boolean(literal)),
Err(keyword) => Lexeme::Keyword(keyword),
};
let length = lexeme.to_string().len();
if length != input.len() {
return None;
}
Some(Token::new(Location::new(0, length), lexeme, length))
}
///
/// Parses the keyword itself.
///
fn parse_keyword(input: &str) -> Option<Self> {
if !input.starts_with(Self::can_begin) {
return None;
}
let end = input.find(Self::cannot_continue).unwrap_or(input.len());
let input = &input[..end];
if let Some(input) = input.strip_prefix("int") {
if let Ok(bitlength) = input.parse::<usize>() {
return Some(Self::Int(bitlength));
}
}
if let Some(input) = input.strip_prefix("uint") {
if let Ok(bitlength) = input.parse::<usize>() {
return Some(Self::Uint(bitlength));
}
}
Some(match input {
"object" => Self::Object,
"code" => Self::Code,
"function" => Self::Function,
"let" => Self::Let,
"if" => Self::If,
"switch" => Self::Switch,
"case" => Self::Case,
"default" => Self::Default,
"for" => Self::For,
"break" => Self::Break,
"continue" => Self::Continue,
"leave" => Self::Leave,
"true" => Self::True,
"false" => Self::False,
"bool" => Self::Bool,
_ => return None,
})
}
///
/// Checks whether the character can begin a keyword.
///
pub fn can_begin(character: char) -> bool {
character.is_alphabetic()
}
///
/// Checks whether the character can continue a keyword.
///
pub fn can_continue(character: char) -> bool {
Self::can_begin(character) || character.is_numeric()
}
///
/// Checks whether the character cannot continue a keyword.
///
pub fn cannot_continue(character: char) -> bool {
!Self::can_continue(character)
}
}
impl std::fmt::Display for Keyword {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Object => write!(f, "object"),
Self::Code => write!(f, "code"),
Self::Function => write!(f, "function"),
Self::Let => write!(f, "let"),
Self::If => write!(f, "if"),
Self::Switch => write!(f, "switch"),
Self::Case => write!(f, "case"),
Self::Default => write!(f, "default"),
Self::For => write!(f, "for"),
Self::Break => write!(f, "break"),
Self::Continue => write!(f, "continue"),
Self::Leave => write!(f, "leave"),
Self::True => write!(f, "true"),
Self::False => write!(f, "false"),
Self::Bool => write!(f, "bool"),
Self::Int(bitlength) => write!(f, "int{bitlength}"),
Self::Uint(bitlength) => write!(f, "uint{bitlength}"),
}
}
}
@@ -0,0 +1,66 @@
//!
//! The boolean literal lexeme.
//!
use serde::Deserialize;
use serde::Serialize;
use crate::yul::lexer::token::lexeme::keyword::Keyword;
///
/// The boolean literal lexeme.
///
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub enum Boolean {
/// Created from the `false` keyword.
False,
/// Created from the `true` keyword.
True,
}
impl Boolean {
///
/// Creates a `false` value.
///
pub fn r#false() -> Self {
Self::False
}
///
/// Creates a `true` value.
///
pub fn r#true() -> Self {
Self::True
}
}
impl TryFrom<Keyword> for Boolean {
type Error = Keyword;
fn try_from(keyword: Keyword) -> Result<Self, Self::Error> {
Ok(match keyword {
Keyword::False => Self::False,
Keyword::True => Self::True,
unknown => return Err(unknown),
})
}
}
impl From<bool> for Boolean {
fn from(value: bool) -> Self {
if value {
Self::True
} else {
Self::False
}
}
}
impl std::fmt::Display for Boolean {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::False => write!(f, "false"),
Self::True => write!(f, "true"),
}
}
}
@@ -0,0 +1,118 @@
//!
//! The integer literal lexeme.
//!
use serde::Deserialize;
use serde::Serialize;
use crate::yul::lexer::token::lexeme::Lexeme;
use crate::yul::lexer::token::lexeme::Literal;
use crate::yul::lexer::token::location::Location;
use crate::yul::lexer::token::Token;
///
/// The integer literal lexeme.
///
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub enum Integer {
/// An integer literal, like `42`.
Decimal {
/// The inner literal contents.
inner: String,
},
/// A hexadecimal literal, like `0xffff`.
Hexadecimal {
/// The inner literal contents.
inner: String,
},
}
impl Integer {
///
/// Creates a decimal value.
///
pub fn new_decimal(inner: String) -> Self {
Self::Decimal { inner }
}
///
/// Creates a hexadecimal value.
///
pub fn new_hexadecimal(inner: String) -> Self {
Self::Hexadecimal { inner }
}
///
/// Parses the value from the source code slice.
///
pub fn parse(input: &str) -> Option<Token> {
let (value, length) = if let Some(body) = input.strip_prefix("0x") {
let end = body
.find(Self::cannot_continue_hexadecimal)
.unwrap_or(body.len());
let length = "0x".len() + end;
let value = Self::new_hexadecimal(input[..length].to_owned());
(value, length)
} else if input.starts_with(Self::can_begin_decimal) {
let end = input
.find(Self::cannot_continue_decimal)
.unwrap_or(input.len());
let length = end;
let value = Self::new_decimal(input[..length].to_owned());
(value, length)
} else {
return None;
};
let token = Token::new(
Location::new(0, length),
Lexeme::Literal(Literal::Integer(value)),
length,
);
Some(token)
}
///
/// Checks whether the character can begin a decimal number.
///
pub fn can_begin_decimal(character: char) -> bool {
Self::can_continue_decimal(character)
}
///
/// Checks whether the character can continue a decimal number.
///
pub fn can_continue_decimal(character: char) -> bool {
character.is_digit(era_compiler_common::BASE_DECIMAL)
}
///
/// Checks whether the character cannot continue a decimal number.
///
pub fn cannot_continue_decimal(character: char) -> bool {
!Self::can_continue_decimal(character)
}
///
/// Checks whether the character can continue a hexadecimal number.
///
pub fn can_continue_hexadecimal(character: char) -> bool {
character.is_digit(era_compiler_common::BASE_HEXADECIMAL)
}
///
/// Checks whether the character cannot continue a hexadecimal number.
///
pub fn cannot_continue_hexadecimal(character: char) -> bool {
!Self::can_continue_hexadecimal(character)
}
}
impl std::fmt::Display for Integer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Decimal { inner } => write!(f, "{inner}"),
Self::Hexadecimal { inner } => write!(f, "{inner}"),
}
}
}
@@ -0,0 +1,37 @@
//!
//! The literal lexeme.
//!
pub mod boolean;
pub mod integer;
pub mod string;
use serde::Deserialize;
use serde::Serialize;
use self::boolean::Boolean;
use self::integer::Integer;
use self::string::String;
///
/// The literal lexeme.
///
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub enum Literal {
/// A boolean literal, like `true`, or `false`.
Boolean(Boolean),
/// An integer literal, like `42`, or `0xff`.
Integer(Integer),
/// A string literal, like `"message"`.
String(String),
}
impl std::fmt::Display for Literal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Boolean(inner) => write!(f, "{inner}"),
Self::Integer(inner) => write!(f, "{inner}"),
Self::String(inner) => write!(f, "{inner}"),
}
}
}
@@ -0,0 +1,93 @@
//!
//! The string literal lexeme.
//!
use serde::Deserialize;
use serde::Serialize;
use crate::yul::lexer::token::lexeme::Lexeme;
use crate::yul::lexer::token::lexeme::Literal;
use crate::yul::lexer::token::location::Location;
use crate::yul::lexer::token::Token;
///
/// The string literal lexeme.
///
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct String {
/// The inner string contents.
pub inner: std::string::String,
/// Whether the string is hexadecimal.
pub is_hexadecimal: bool,
}
impl String {
///
/// Creates a string literal value.
///
pub fn new(inner: ::std::string::String, is_hexadecimal: bool) -> Self {
Self {
inner,
is_hexadecimal,
}
}
///
/// Parses the value from the source code slice.
///
pub fn parse(input: &str) -> Option<Token> {
let mut length = 0;
let is_string = input[length..].starts_with('"');
let is_hex_string = input[length..].starts_with(r#"hex""#);
if !is_string && !is_hex_string {
return None;
}
if is_string {
length += 1;
}
if is_hex_string {
length += r#"hex""#.len();
}
let mut string = std::string::String::new();
loop {
if input[length..].starts_with('\\') {
string.push(input.chars().nth(length).expect("Always exists"));
string.push(input.chars().nth(length + 1).expect("Always exists"));
length += 2;
continue;
}
if input[length..].starts_with('"') {
length += 1;
break;
}
string.push(input.chars().nth(length).expect("Always exists"));
length += 1;
}
let string = string
.strip_prefix('"')
.and_then(|string| string.strip_suffix('"'))
.unwrap_or(string.as_str())
.to_owned();
let literal = Self::new(string, is_hex_string);
Some(Token::new(
Location::new(0, length),
Lexeme::Literal(Literal::String(literal)),
length,
))
}
}
impl std::fmt::Display for String {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.inner)
}
}
@@ -0,0 +1,46 @@
//!
//! The lexeme.
//!
pub mod comment;
pub mod identifier;
pub mod keyword;
pub mod literal;
pub mod symbol;
use self::identifier::Identifier;
use self::keyword::Keyword;
use self::literal::Literal;
use self::symbol::Symbol;
///
/// The lexeme.
///
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Lexeme {
/// The keyword lexeme.
Keyword(Keyword),
/// The symbol lexeme.
Symbol(Symbol),
/// The identifier lexeme.
Identifier(Identifier),
/// The literal lexeme.
Literal(Literal),
/// The comment lexeme.
Comment,
/// The end-of-file lexeme.
EndOfFile,
}
impl std::fmt::Display for Lexeme {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Keyword(inner) => write!(f, "{inner}"),
Self::Symbol(inner) => write!(f, "{inner}"),
Self::Identifier(inner) => write!(f, "{inner}"),
Self::Literal(inner) => write!(f, "{inner}"),
Self::Comment => Ok(()),
Self::EndOfFile => write!(f, "EOF"),
}
}
}
@@ -0,0 +1,74 @@
//!
//! The symbol lexeme.
//!
use crate::yul::lexer::token::lexeme::Lexeme;
use crate::yul::lexer::token::location::Location;
use crate::yul::lexer::token::Token;
///
/// The symbol lexeme.
///
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Symbol {
/// The `:=` symbol.
Assignment,
/// The `->` symbol.
Arrow,
/// The `{` symbol.
BracketCurlyLeft,
/// The `}` symbol.
BracketCurlyRight,
/// The `(` symbol.
ParenthesisLeft,
/// The `)` symbol.
ParenthesisRight,
/// The `,` symbol.
Comma,
/// The `:` symbol.
Colon,
}
impl Symbol {
///
/// Parses the symbol, returning it as a token.
///
pub fn parse(input: &str) -> Option<Token> {
let (symbol, length) = match &input[..2] {
":=" => (Self::Assignment, 2),
"->" => (Self::Arrow, 2),
_ => match &input[..1] {
"{" => (Self::BracketCurlyLeft, 1),
"}" => (Self::BracketCurlyRight, 1),
"(" => (Self::ParenthesisLeft, 1),
")" => (Self::ParenthesisRight, 1),
"," => (Self::Comma, 1),
":" => (Self::Colon, 1),
_ => return None,
},
};
Some(Token::new(
Location::new(0, length),
Lexeme::Symbol(symbol),
length,
))
}
}
impl std::fmt::Display for Symbol {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Assignment => write!(f, ":="),
Self::Arrow => write!(f, "->"),
Self::BracketCurlyLeft => write!(f, "{{"),
Self::BracketCurlyRight => write!(f, "}}"),
Self::ParenthesisLeft => write!(f, "("),
Self::ParenthesisRight => write!(f, ")"),
Self::Comma => write!(f, ","),
Self::Colon => write!(f, ":"),
}
}
}
@@ -0,0 +1,65 @@
//!
//! The lexical token location.
//!
use serde::Deserialize;
use serde::Serialize;
///
/// The token location in the source code file.
///
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Eq)]
pub struct Location {
/// The line number, starting from 1.
pub line: usize,
/// The column number, starting from 1.
pub column: usize,
}
impl Default for Location {
fn default() -> Self {
Self { line: 1, column: 1 }
}
}
impl Location {
///
/// Creates a default location.
///
pub fn new(line: usize, column: usize) -> Self {
Self { line, column }
}
///
/// Mutates the location by shifting the original one down by `lines` and
/// setting the column to `column`.
///
pub fn shift_down(&mut self, lines: usize, column: usize) {
if lines == 0 {
self.shift_right(column);
return;
}
self.line += lines;
self.column = column;
}
///
/// Mutates the location by shifting the original one rightward by `columns`.
///
pub fn shift_right(&mut self, columns: usize) {
self.column += columns;
}
}
impl PartialEq for Location {
fn eq(&self, other: &Self) -> bool {
self.line == other.line && self.column == other.column
}
}
impl std::fmt::Display for Location {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.line, self.column)
}
}
@@ -0,0 +1,43 @@
//!
//! The token.
//!
pub mod lexeme;
pub mod location;
use self::lexeme::Lexeme;
use self::location::Location;
///
/// The token.
///
/// Contains a lexeme and its location.
///
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Token {
/// The token location.
pub location: Location,
/// The lexeme.
pub lexeme: Lexeme,
/// The token length, including whitespaces.
pub length: usize,
}
impl Token {
///
/// A shortcut constructor.
///
pub fn new(location: Location, lexeme: Lexeme, length: usize) -> Self {
Self {
location,
lexeme,
length,
}
}
}
impl std::fmt::Display for Token {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.location, self.lexeme)
}
}

Some files were not shown because too many files have changed in this diff Show More