Repository: gakonst/ethers-rs
Pull request #1004 (2022-03-10 16:35:50)

fix(solc): use correct types
<!--
Thank you for your Pull Request. Please provide a description above and review
the requirements below.

Bug fixes and new features should include tests.

Contributors guide: https://github.com/gakonst/ethers-rs/blob/master/CONTRIBUTING.md

The contributors guide includes instructions for running rustfmt and building the
documentation.
-->

## Motivation
Fix https://github.com/gakonst/foundry/issues/897
<!--
Explain the context and why you're making that change. What is the problem
you're trying to solve? In some cases there is not a problem and this can be
thought of as being the motivation for your change.
-->

## Solution
Comply with solidity json spec
<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

## PR Checklist

- [ ] Added Tests
- [ ] Added Documentation
- [ ] Updated the changelog



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c44561a0..a9002f81 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -47,6 +47,7 @@
 - Add a getter to `ProjectCompileOutput` that returns a mapping of compiler
   versions to a vector of name + contract struct tuples
   [#908](https://github.com/gakonst/ethers-rs/pull/908)
+- Add Yul compilation [994](https://github.com/gakonst/ethers-rs/pull/994)
 - Enforce commutativity of ENS reverse resolution
   [#996](https://github.com/gakonst/ethers-rs/pull/996)
 
diff --git a/ethers-solc/src/artifacts/mod.rs b/ethers-solc/src/artifacts/mod.rs
index 0d5038a4..b35f3cdc 100644
--- a/ethers-solc/src/artifacts/mod.rs
+++ b/ethers-solc/src/artifacts/mod.rs
@@ -52,13 +52,37 @@ pub struct CompilerInput {
 
 impl CompilerInput {
     /// Reads all contracts found under the path
-    pub fn new(path: impl AsRef<Path>) -> Result<Self, SolcIoError> {
+    pub fn new(path: impl AsRef<Path>) -> Result<Vec<Self>, SolcIoError> {
         Source::read_all_from(path.as_ref()).map(Self::with_sources)
     }
 
     /// Creates a new Compiler input with default settings and the given sources
-    pub fn with_sources(sources: Sources) -> Self {
-        Self { language: "Solidity".to_string(), sources, settings: Default::default() }
+    pub fn with_sources(sources: Sources) -> Vec<Self> {
+        let mut solidity_sources = BTreeMap::new();
+        let mut yul_sources = BTreeMap::new();
+        for (path, source) in sources {
+            if path.extension() == Some(std::ffi::OsStr::new("yul")) {
+                yul_sources.insert(path, source);
+            } else {
+                solidity_sources.insert(path, source);
+            }
+        }
+        let mut res = Vec::new();
+        if !solidity_sources.is_empty() {
+            res.push(Self {
+                language: "Solidity".to_string(),
+                sources: solidity_sources,
+                settings: Default::default(),
+            });
+        }
+        if !yul_sources.is_empty() {
+            res.push(Self {
+                language: "Yul".to_string(),
+                sources: yul_sources,
+                settings: Default::default(),
+            });
+        }
+        res
     }
 
     /// Sets the settings for compilation
@@ -99,12 +123,6 @@ impl CompilerInput {
     }
 }
 
-impl Default for CompilerInput {
-    fn default() -> Self {
-        Self::with_sources(Default::default())
-    }
-}
-
 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
 pub struct Settings {
@@ -564,13 +582,16 @@ pub struct Output {
 
 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
 pub struct SolcAbi {
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
     pub inputs: Vec<Item>,
     #[serde(rename = "stateMutability")]
     pub state_mutability: Option<String>,
     #[serde(rename = "type")]
     pub abi_type: String,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
     pub name: Option<String>,
-    pub outputs: Option<Vec<Item>>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub outputs: Vec<Item>,
 }
 
 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -805,6 +826,12 @@ impl CompilerOutput {
             err.source_location.as_ref().map(|s| files.contains(s.file.as_str())).unwrap_or(true)
         });
     }
+
+    pub fn merge(&mut self, other: CompilerOutput) {
+        self.errors.extend(other.errors);
+        self.contracts.extend(other.contracts);
+        self.sources.extend(other.sources);
+    }
 }
 
 /// A wrapper helper type for the `Contracts` type alias
@@ -1353,7 +1380,7 @@ pub struct UserDoc {
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub kind: Option<String>,
     #[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")]
-    pub methods: BTreeMap<String, String>,
+    pub methods: BTreeMap<String, BTreeMap<String, String>>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub notice: Option<String>,
 }
@@ -1371,11 +1398,21 @@ pub struct DevDoc {
     #[serde(default, rename = "custom:experimental", skip_serializing_if = "Option::is_none")]
     pub custom_experimental: Option<String>,
     #[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")]
-    pub methods: BTreeMap<String, serde_json::Value>,
+    pub methods: BTreeMap<String, MethodDoc>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub title: Option<String>,
 }
 
+#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
+pub struct MethodDoc {
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub details: Option<String>,
+    #[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")]
+    pub params: BTreeMap<String, String>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub r#return: Option<String>,
+}
+
 #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
 #[serde(rename_all = "camelCase")]
 pub struct Evm {
diff --git a/ethers-solc/src/compile/mod.rs b/ethers-solc/src/compile/mod.rs
index 9841fcc6..7f4d48db 100644
--- a/ethers-solc/src/compile/mod.rs
+++ b/ethers-solc/src/compile/mod.rs
@@ -410,7 +410,12 @@ impl Solc {
     /// Convenience function for compiling all sources under the given path
     pub fn compile_source(&self, path: impl AsRef<Path>) -> Result<CompilerOutput> {
         let path = path.as_ref();
-        self.compile(&CompilerInput::new(path)?)
+        let mut res: CompilerOutput = Default::default();
+        for input in CompilerInput::new(path)? {
+            let output = self.compile(&input)?;
+            res.merge(output)
+        }
+        Ok(res)
     }
 
     /// Same as [`Self::compile()`], but only returns those files which are included in the
@@ -425,7 +430,7 @@ impl Solc {
     /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
     ///  use ethers_solc::{CompilerInput, Solc};
     /// let solc = Solc::default();
-    /// let input = CompilerInput::new("./contracts")?;
+    /// let input = CompilerInput::new("./contracts")?[0].clone();
     /// let output = solc.compile_exact(&input)?;
     /// # Ok(())
     /// # }
@@ -571,8 +576,8 @@ impl Solc {
     /// use ethers_solc::{CompilerInput, Solc};
     /// let solc1 = Solc::default();
     /// let solc2 = Solc::default();
-    /// let input1 = CompilerInput::new("contracts").unwrap();
-    /// let input2 = CompilerInput::new("src").unwrap();
+    /// let input1 = CompilerInput::new("contracts").unwrap()[0].clone();
+    /// let input2 = CompilerInput::new("src").unwrap()[0].clone();
     ///
     /// let outputs = Solc::compile_many([(solc1, input1), (solc2, input2)], 2).await.flattened().unwrap();
     /// # }
diff --git a/ethers-solc/src/compile/project.rs b/ethers-solc/src/compile/project.rs
index 91ba7d5c..19e36a3c 100644
--- a/ethers-solc/src/compile/project.rs
+++ b/ethers-solc/src/compile/project.rs
@@ -341,24 +341,23 @@ fn compile_sequential(
             solc.args
         );
 
-        let input = CompilerInput::with_sources(sources)
-            .settings(settings.clone())
-            .normalize_evm_version(&version)
-            .with_remappings(paths.remappings.clone());
-
-        tracing::trace!(
-            "calling solc `{}` with {} sources {:?}",
-            version,
-            input.sources.len(),
-            input.sources.keys()
-        );
-
-        report::solc_spawn(&solc, &version, &input);
-        let output = solc.compile_exact(&input)?;
-        report::solc_success(&solc, &version, &output);
-        tracing::trace!("compiled input, output has error: {}", output.has_error());
-
-        aggregated.extend(version, output);
+        for input in CompilerInput::with_sources(sources) {
+            let input = input
+                .settings(settings.clone())
+                .normalize_evm_version(&version)
+                .with_remappings(paths.remappings.clone());
+            tracing::trace!(
+                "calling solc `{}` with {} sources {:?}",
+                version,
+                input.sources.len(),
+                input.sources.keys()
+            );
+            report::solc_spawn(&solc, &version, &input);
+            let output = solc.compile_exact(&input)?;
+            report::solc_success(&solc, &version, &output);
+            tracing::trace!("compiled input, output has error: {}", output.has_error());
+            aggregated.extend(version.clone(), output);
+        }
     }
     Ok(aggregated)
 }
@@ -383,13 +382,14 @@ fn compile_parallel(
             // nothing to compile
             continue
         }
+        for input in CompilerInput::with_sources(sources) {
+            let job = input
+                .settings(settings.clone())
+                .normalize_evm_version(&version)
+                .with_remappings(paths.remappings.clone());
 
-        let job = CompilerInput::with_sources(sources)
-            .settings(settings.clone())
-            .normalize_evm_version(&version)
-            .with_remappings(paths.remappings.clone());
-
-        jobs.push((solc, version, job))
+            jobs.push((solc.clone(), version.clone(), job))
+        }
     }
 
     // start a rayon threadpool that will execute all `Solc::compile()` processes
diff --git a/ethers-solc/src/utils.rs b/ethers-solc/src/utils.rs
index c15a2697..b269d52f 100644
--- a/ethers-solc/src/utils.rs
+++ b/ethers-solc/src/utils.rs
@@ -67,7 +67,9 @@ pub fn source_files(root: impl AsRef<Path>) -> Vec<PathBuf> {
         .into_iter()
         .filter_map(Result::ok)
         .filter(|e| e.file_type().is_file())
-        .filter(|e| e.path().extension().map(|ext| ext == "sol").unwrap_or_default())
+        .filter(|e| {
+            e.path().extension().map(|ext| (ext == "sol") || (ext == "yul")).unwrap_or_default()
+        })
         .map(|e| e.path().into())
         .collect()
 }


Test patch:
diff --git a/ethers-solc/test-data/yul-sample/Dapp.sol b/ethers-solc/test-data/yul-sample/Dapp.sol
new file mode 100644
index 00000000..762db2c3
--- /dev/null
+++ b/ethers-solc/test-data/yul-sample/Dapp.sol
@@ -0,0 +1,7 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+pragma solidity >=0.6.6;
+
+contract Dapp {
+
+    function modified() public {}
+}
diff --git a/ethers-solc/test-data/yul-sample/SimpleStore.yul b/ethers-solc/test-data/yul-sample/SimpleStore.yul
new file mode 100644
index 00000000..aba14daa
--- /dev/null
+++ b/ethers-solc/test-data/yul-sample/SimpleStore.yul
@@ -0,0 +1,11 @@
+object "SimpleStore" {
+  code {
+    datacopy(0, dataoffset("Runtime"), datasize("Runtime"))
+    return(0, datasize("Runtime"))
+  }
+  object "Runtime" {
+    code {
+      calldatacopy(0, 0, 36) // write calldata to memory
+    }
+  }
+}
diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs
index 3129da7c..c1e7ee28 100644
--- a/ethers-solc/tests/project.rs
+++ b/ethers-solc/tests/project.rs
@@ -92,6 +92,36 @@ fn can_compile_dapp_sample() {
     assert_eq!(cache, updated_cache);
 }
 
+#[test]
+fn can_compile_yul_sample() {
+    let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/yul-sample");
+    let paths = ProjectPathsConfig::builder().sources(root);
+    let project = TempProject::<ConfigurableArtifacts>::new(paths).unwrap();
+
+    let compiled = project.compile().unwrap();
+    assert!(compiled.find("Dapp").is_some());
+    assert!(compiled.find("SimpleStore").is_some());
+    assert!(!compiled.has_compiler_errors());
+
+    // nothing to compile
+    let compiled = project.compile().unwrap();
+    assert!(compiled.find("Dapp").is_some());
+    assert!(compiled.find("SimpleStore").is_some());
+    assert!(compiled.is_unchanged());
+
+    let cache = SolFilesCache::read(project.cache_path()).unwrap();
+
+    // delete artifacts
+    std::fs::remove_dir_all(&project.paths().artifacts).unwrap();
+    let compiled = project.compile().unwrap();
+    assert!(compiled.find("Dapp").is_some());
+    assert!(compiled.find("SimpleStore").is_some());
+    assert!(!compiled.is_unchanged());
+
+    let updated_cache = SolFilesCache::read(project.cache_path()).unwrap();
+    assert_eq!(cache, updated_cache);
+}
+
 #[test]
 fn can_compile_configured() {
     let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/dapp-sample");