Better Compiler Interface & Shared Compiler Objects (#156)

* Add leader and follower node assignment to test

* Update the compilers interface

* Fix Cargo machete

* Add reporting back to the compilers

* Remove the static testing target from the report

* Uncomment instrument macro

* Switch to a for loop when reporting cases

* Update compilers to use interior caching

* Update tests stream func

* Fix tests
This commit is contained in:
Omar
2025-08-28 18:03:45 +03:00
committed by GitHub
parent 8b1afc36a3
commit 7878f68c26
20 changed files with 600 additions and 948 deletions
-4
View File
@@ -1,4 +0,0 @@
use semver::Version;
/// This is the first version of solc that supports the `--via-ir` flag / "viaIR" input JSON.
pub const SOLC_VERSION_SUPPORTING_VIA_YUL_IR: Version = Version::new(0, 8, 13);
+30 -52
View File
@@ -3,8 +3,6 @@
//! - Polkadot revive resolc compiler
//! - Polkadot revive Wasm compiler
mod constants;
use std::{
collections::HashMap,
hash::Hash,
@@ -13,7 +11,7 @@ use std::{
use alloy::json_abi::JsonAbi;
use alloy_primitives::Address;
use anyhow::Context;
use anyhow::{Context, Result};
use semver::Version;
use serde::{Deserialize, Serialize};
@@ -30,36 +28,36 @@ pub mod revive_resolc;
pub mod solc;
/// A common interface for all supported Solidity compilers.
pub trait SolidityCompiler {
/// Extra options specific to the compiler.
type Options: Default + PartialEq + Eq + Hash;
pub trait SolidityCompiler: Sized {
/// Instantiates a new compiler object.
///
/// Based on the given [`Arguments`] and [`VersionOrRequirement`] this function instantiates a
/// new compiler object. Certain implementations of this trait might choose to cache cache the
/// compiler objects and return the same ones over and over again.
fn new(
config: &Arguments,
version: impl Into<Option<VersionOrRequirement>>,
) -> impl Future<Output = Result<Self>>;
/// Returns the version of the compiler.
fn version(&self) -> &Version;
/// Returns the path of the compiler executable.
fn path(&self) -> &Path;
/// The low-level compiler interface.
fn build(
&self,
input: CompilerInput,
additional_options: Self::Options,
) -> impl Future<Output = anyhow::Result<CompilerOutput>>;
fn build(&self, input: CompilerInput) -> impl Future<Output = Result<CompilerOutput>>;
fn new(solc_executable: PathBuf) -> Self;
fn get_compiler_executable(
config: &Arguments,
version: impl Into<VersionOrRequirement>,
) -> impl Future<Output = anyhow::Result<PathBuf>>;
fn version(&self) -> impl Future<Output = anyhow::Result<Version>>;
/// Does the compiler support the provided mode and version settings?
/// Does the compiler support the provided mode and version settings.
fn supports_mode(
compiler_version: &Version,
optimize_setting: ModeOptimizerSetting,
&self,
optimizer_setting: ModeOptimizerSetting,
pipeline: ModePipeline,
) -> bool;
}
/// The generic compilation input configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct CompilerInput {
pub pipeline: Option<ModePipeline>,
pub optimization: Option<ModeOptimizerSetting>,
@@ -80,21 +78,12 @@ pub struct CompilerOutput {
}
/// A generic builder style interface for configuring the supported compiler options.
pub struct Compiler<T: SolidityCompiler> {
#[derive(Default)]
pub struct Compiler {
input: CompilerInput,
additional_options: T::Options,
}
impl Default for Compiler<solc::Solc> {
fn default() -> Self {
Self::new()
}
}
impl<T> Compiler<T>
where
T: SolidityCompiler,
{
impl Compiler {
pub fn new() -> Self {
Self {
input: CompilerInput {
@@ -107,7 +96,6 @@ where
libraries: Default::default(),
revert_string_handling: Default::default(),
},
additional_options: T::Options::default(),
}
}
@@ -136,7 +124,7 @@ where
self
}
pub fn with_source(mut self, path: impl AsRef<Path>) -> anyhow::Result<Self> {
pub fn with_source(mut self, path: impl AsRef<Path>) -> Result<Self> {
self.input.sources.insert(
path.as_ref().to_path_buf(),
read_to_string(path.as_ref()).context("Failed to read the contract source")?,
@@ -166,11 +154,6 @@ where
self
}
pub fn with_additional_options(mut self, options: impl Into<T::Options>) -> Self {
self.additional_options = options.into();
self
}
pub fn then(self, callback: impl FnOnce(Self) -> Self) -> Self {
callback(self)
}
@@ -179,17 +162,12 @@ where
callback(self)
}
pub async fn try_build(
self,
compiler_path: impl AsRef<Path>,
) -> anyhow::Result<CompilerOutput> {
T::new(compiler_path.as_ref().to_path_buf())
.build(self.input, self.additional_options)
.await
pub async fn try_build(self, compiler: &impl SolidityCompiler) -> Result<CompilerOutput> {
compiler.build(self.input).await
}
pub fn input(&self) -> CompilerInput {
self.input.clone()
pub fn input(&self) -> &CompilerInput {
&self.input
}
}
+52 -115
View File
@@ -3,8 +3,8 @@
use std::{
path::PathBuf,
process::{Command, Stdio},
sync::LazyLock,
process::Stdio,
sync::{Arc, LazyLock},
};
use dashmap::DashMap;
@@ -16,26 +16,61 @@ use revive_solc_json_interface::{
SolcStandardJsonOutput,
};
use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler};
use crate::{
CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler, solc::Solc,
};
use alloy::json_abi::JsonAbi;
use anyhow::Context;
use anyhow::{Context, Result};
use semver::Version;
use tokio::{io::AsyncWriteExt, process::Command as AsyncCommand};
// TODO: I believe that we need to also pass the solc compiler to resolc so that resolc uses the
// specified solc compiler. I believe that currently we completely ignore the specified solc binary
// when invoking resolc which doesn't seem right if we're using solc as a compiler frontend.
/// A wrapper around the `resolc` binary, emitting PVM-compatible bytecode.
#[derive(Debug)]
pub struct Resolc {
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Resolc(Arc<ResolcInner>);
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct ResolcInner {
/// The internal solc compiler that the resolc compiler uses as a compiler frontend.
solc: Solc,
/// Path to the `resolc` executable
resolc_path: PathBuf,
}
impl SolidityCompiler for Resolc {
type Options = Vec<String>;
async fn new(
config: &Arguments,
version: impl Into<Option<VersionOrRequirement>>,
) -> Result<Self> {
/// This is a cache of all of the resolc compiler objects. Since we do not currently support
/// multiple resolc compiler versions, so our cache is just keyed by the solc compiler and
/// its version to the resolc compiler.
static COMPILERS_CACHE: LazyLock<DashMap<Solc, Resolc>> = LazyLock::new(Default::default);
let solc = Solc::new(config, version)
.await
.context("Failed to create the solc compiler frontend for resolc")?;
Ok(COMPILERS_CACHE
.entry(solc.clone())
.or_insert_with(|| {
Self(Arc::new(ResolcInner {
solc,
resolc_path: config.resolc.clone(),
}))
})
.clone())
}
fn version(&self) -> &Version {
// We currently return the solc compiler version since we do not support multiple resolc
// compiler versions.
self.0.solc.version()
}
fn path(&self) -> &std::path::Path {
&self.0.resolc_path
}
#[tracing::instrument(level = "debug", ret)]
async fn build(
@@ -52,8 +87,7 @@ impl SolidityCompiler for Resolc {
// resolc. So, we need to go back to this later once it's supported.
revert_string_handling: _,
}: CompilerInput,
additional_options: Self::Options,
) -> anyhow::Result<CompilerOutput> {
) -> Result<CompilerOutput> {
if !matches!(pipeline, None | Some(ModePipeline::ViaYulIR)) {
anyhow::bail!(
"Resolc only supports the Y (via Yul IR) pipeline, but the provided pipeline is {pipeline:?}"
@@ -100,7 +134,7 @@ impl SolidityCompiler for Resolc {
},
};
let mut command = AsyncCommand::new(&self.resolc_path);
let mut command = AsyncCommand::new(self.path());
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
@@ -121,7 +155,7 @@ impl SolidityCompiler for Resolc {
}
let mut child = command
.spawn()
.with_context(|| format!("Failed to spawn resolc at {}", self.resolc_path.display()))?;
.with_context(|| format!("Failed to spawn resolc at {}", self.path().display()))?;
let stdin_pipe = child.stdin.as_mut().expect("stdin must be piped");
let serialized_input = serde_json::to_vec(&input)
@@ -238,108 +272,11 @@ impl SolidityCompiler for Resolc {
Ok(compiler_output)
}
fn new(resolc_path: PathBuf) -> Self {
Resolc { resolc_path }
}
async fn get_compiler_executable(
config: &Arguments,
_version: impl Into<VersionOrRequirement>,
) -> anyhow::Result<PathBuf> {
if !config.resolc.as_os_str().is_empty() {
return Ok(config.resolc.clone());
}
Ok(PathBuf::from("resolc"))
}
async fn version(&self) -> anyhow::Result<semver::Version> {
/// This is a cache of the path of the compiler to the version number of the compiler. We
/// choose to cache the version in this way rather than through a field on the struct since
/// compiler objects are being created all the time from the path and the compiler object is
/// not reused over time.
static VERSION_CACHE: LazyLock<DashMap<PathBuf, Version>> = LazyLock::new(Default::default);
match VERSION_CACHE.entry(self.resolc_path.clone()) {
dashmap::Entry::Occupied(occupied_entry) => Ok(occupied_entry.get().clone()),
dashmap::Entry::Vacant(vacant_entry) => {
let output = Command::new(self.resolc_path.as_path())
.arg("--version")
.stdout(Stdio::piped())
.spawn()
.with_context(|| {
format!(
"Failed to spawn resolc at {} to get version",
self.resolc_path.display()
)
})?
.wait_with_output()
.with_context(|| {
format!(
"Failed waiting for resolc at {} to finish --version",
self.resolc_path.display()
)
})?
.stdout;
let output = String::from_utf8_lossy(&output);
let version_string = output
.split("version ")
.nth(1)
.context("Version parsing failed")?
.split("+")
.next()
.context("Version parsing failed")?;
let version = Version::parse(version_string).with_context(|| {
format!("Failed to parse resolc semver from '{version_string}'")
})?;
vacant_entry.insert(version.clone());
Ok(version)
}
}
}
fn supports_mode(
_compiler_version: &Version,
_optimize_setting: ModeOptimizerSetting,
&self,
optimize_setting: ModeOptimizerSetting,
pipeline: ModePipeline,
) -> bool {
// We only support the Y (IE compile via Yul IR) mode here, which also means that we can
// only use solc version 0.8.13 and above. We must always compile via Yul IR as resolc
// needs this to translate to LLVM IR and then RISCV.
// Note: the original implementation of this function looked like the following:
// ```
// pipeline == ModePipeline::ViaYulIR && compiler_version >= &SOLC_VERSION_SUPPORTING_VIA_YUL_IR
// ```
// However, that implementation is sadly incorrect since the version that's passed into this
// function is not the version of solc but the version of resolc. This is despite the fact
// that resolc depends on Solc for the initial Yul codegen. Therefore, we have skipped the
// version check until we do a better integrations between resolc and solc.
pipeline == ModePipeline::ViaYulIR
}
}
#[cfg(test)]
mod test {
use super::*;
#[tokio::test]
async fn compiler_version_can_be_obtained() {
// Arrange
let args = Arguments::default();
let path = Resolc::get_compiler_executable(&args, Version::new(0, 7, 6))
.await
.unwrap();
let compiler = Resolc::new(path);
// Act
let version = compiler.version().await;
// Assert
let _ = version.expect("Failed to get version");
pipeline == ModePipeline::ViaYulIR && self.0.solc.supports_mode(optimize_setting, pipeline)
}
}
+57 -128
View File
@@ -3,8 +3,8 @@
use std::{
path::PathBuf,
process::{Command, Stdio},
sync::LazyLock,
process::Stdio,
sync::{Arc, LazyLock},
};
use dashmap::DashMap;
@@ -12,10 +12,9 @@ use revive_dt_common::types::VersionOrRequirement;
use revive_dt_config::Arguments;
use revive_dt_solc_binaries::download_solc;
use super::constants::SOLC_VERSION_SUPPORTING_VIA_YUL_IR;
use crate::{CompilerInput, CompilerOutput, ModeOptimizerSetting, ModePipeline, SolidityCompiler};
use anyhow::Context;
use anyhow::{Context, Result};
use foundry_compilers_artifacts::{
output_selection::{
BytecodeOutputSelection, ContractOutputSelection, EvmOutputSelection, OutputSelection,
@@ -26,13 +25,54 @@ use foundry_compilers_artifacts::{
use semver::Version;
use tokio::{io::AsyncWriteExt, process::Command as AsyncCommand};
#[derive(Debug)]
pub struct Solc {
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Solc(Arc<SolcInner>);
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct SolcInner {
/// The path of the solidity compiler executable that this object uses.
solc_path: PathBuf,
/// The version of the solidity compiler executable that this object uses.
solc_version: Version,
}
impl SolidityCompiler for Solc {
type Options = ();
async fn new(
config: &Arguments,
version: impl Into<Option<VersionOrRequirement>>,
) -> Result<Self> {
// This is a cache for the compiler objects so that whenever the same compiler version is
// requested the same object is returned. We do this as we do not want to keep cloning the
// compiler around.
static COMPILERS_CACHE: LazyLock<DashMap<Version, Solc>> = LazyLock::new(Default::default);
// We attempt to download the solc binary. Note the following: this call does the version
// resolution for us. Therefore, even if the download didn't proceed, this function will
// resolve the version requirement into a canonical version of the compiler. It's then up
// to us to either use the provided path or not.
let version = version.into().unwrap_or_else(|| config.solc.clone().into());
let (version, path) = download_solc(config.directory(), version, false)
.await
.context("Failed to download/get path to solc binary")?;
Ok(COMPILERS_CACHE
.entry(version.clone())
.or_insert_with(|| {
Self(Arc::new(SolcInner {
solc_path: path,
solc_version: version,
}))
})
.clone())
}
fn version(&self) -> &Version {
&self.0.solc_version
}
fn path(&self) -> &std::path::Path {
&self.0.solc_path
}
#[tracing::instrument(level = "debug", ret)]
async fn build(
@@ -47,19 +87,12 @@ impl SolidityCompiler for Solc {
libraries,
revert_string_handling,
}: CompilerInput,
_: Self::Options,
) -> anyhow::Result<CompilerOutput> {
let compiler_supports_via_ir = self
.version()
.await
.context("Failed to query solc version to determine via-ir support")?
>= SOLC_VERSION_SUPPORTING_VIA_YUL_IR;
) -> Result<CompilerOutput> {
// Be careful to entirely omit the viaIR field if the compiler does not support it,
// as it will error if you provide fields it does not know about. Because
// `supports_mode` is called prior to instantiating a compiler, we should never
// ask for something which is invalid.
let via_ir = match (pipeline, compiler_supports_via_ir) {
let via_ir = match (pipeline, self.compiler_supports_yul()) {
(pipeline, true) => pipeline.map(|p| p.via_yul_ir()),
(_pipeline, false) => None,
};
@@ -119,7 +152,7 @@ impl SolidityCompiler for Solc {
},
};
let mut command = AsyncCommand::new(&self.solc_path);
let mut command = AsyncCommand::new(self.path());
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
@@ -140,7 +173,7 @@ impl SolidityCompiler for Solc {
}
let mut child = command
.spawn()
.with_context(|| format!("Failed to spawn solc at {}", self.solc_path.display()))?;
.with_context(|| format!("Failed to spawn solc at {}", self.path().display()))?;
let stdin = child.stdin.as_mut().expect("should be piped");
let serialized_input = serde_json::to_vec(&input)
@@ -220,125 +253,21 @@ impl SolidityCompiler for Solc {
Ok(compiler_output)
}
fn new(solc_path: PathBuf) -> Self {
Self { solc_path }
}
async fn get_compiler_executable(
config: &Arguments,
version: impl Into<VersionOrRequirement>,
) -> anyhow::Result<PathBuf> {
let path = download_solc(config.directory(), version, config.wasm)
.await
.context("Failed to download/get path to solc binary")?;
Ok(path)
}
async fn version(&self) -> anyhow::Result<semver::Version> {
/// This is a cache of the path of the compiler to the version number of the compiler. We
/// choose to cache the version in this way rather than through a field on the struct since
/// compiler objects are being created all the time from the path and the compiler object is
/// not reused over time.
static VERSION_CACHE: LazyLock<DashMap<PathBuf, Version>> = LazyLock::new(Default::default);
match VERSION_CACHE.entry(self.solc_path.clone()) {
dashmap::Entry::Occupied(occupied_entry) => Ok(occupied_entry.get().clone()),
dashmap::Entry::Vacant(vacant_entry) => {
// The following is the parsing code for the version from the solc version strings
// which look like the following:
// ```
// solc, the solidity compiler commandline interface
// Version: 0.8.30+commit.73712a01.Darwin.appleclang
// ```
let child = Command::new(self.solc_path.as_path())
.arg("--version")
.stdout(Stdio::piped())
.spawn()
.with_context(|| {
format!(
"Failed to spawn solc at {} to get version",
self.solc_path.display()
)
})?;
let output = child.wait_with_output().with_context(|| {
format!(
"Failed waiting for solc at {} to finish --version",
self.solc_path.display()
)
})?;
let output = String::from_utf8_lossy(&output.stdout);
let version_line = output
.split("Version: ")
.nth(1)
.context("Version parsing failed")?;
let version_string = version_line
.split("+")
.next()
.context("Version parsing failed")?;
let version = Version::parse(version_string).with_context(|| {
format!("Failed to parse solc semver from '{version_string}'")
})?;
vacant_entry.insert(version.clone());
Ok(version)
}
}
}
fn supports_mode(
compiler_version: &Version,
&self,
_optimize_setting: ModeOptimizerSetting,
pipeline: ModePipeline,
) -> bool {
// solc 0.8.13 and above supports --via-ir, and less than that does not. Thus, we support mode E
// (ie no Yul IR) in either case, but only support Y (via Yul IR) if the compiler is new enough.
pipeline == ModePipeline::ViaEVMAssembly
|| (pipeline == ModePipeline::ViaYulIR
&& compiler_version >= &SOLC_VERSION_SUPPORTING_VIA_YUL_IR)
|| (pipeline == ModePipeline::ViaYulIR && self.compiler_supports_yul())
}
}
#[cfg(test)]
mod test {
use super::*;
#[tokio::test]
async fn compiler_version_can_be_obtained() {
// Arrange
let args = Arguments::default();
let path = Solc::get_compiler_executable(&args, Version::new(0, 7, 6))
.await
.unwrap();
let compiler = Solc::new(path);
// Act
let version = compiler.version().await;
// Assert
assert_eq!(
version.expect("Failed to get version"),
Version::new(0, 7, 6)
)
}
#[tokio::test]
async fn compiler_version_can_be_obtained1() {
// Arrange
let args = Arguments::default();
let path = Solc::get_compiler_executable(&args, Version::new(0, 4, 21))
.await
.unwrap();
let compiler = Solc::new(path);
// Act
let version = compiler.version().await;
// Assert
assert_eq!(
version.expect("Failed to get version"),
Version::new(0, 4, 21)
)
impl Solc {
fn compiler_supports_yul(&self) -> bool {
const SOLC_VERSION_SUPPORTING_VIA_YUL_IR: Version = Version::new(0, 8, 13);
self.version() >= &SOLC_VERSION_SUPPORTING_VIA_YUL_IR
}
}