Repository: huff-language/huff-rs (rust)

Issue:
feat: Interface Output Location Override
## Overview

Currently the [huff_cli](./huff_cli/) `-g` flag tells the compiler to generate a solidity interface file at the same location as the source with an `I` prefix.

We should allow the `-g` flag to accept an optional `string` location of where to generate the output interfaces (with it by default being in the same location as each source file).



Pull request:
feat: Interface Overloading
## Overview

Allows the user to specify an optional string to the `-g` huff_cli flag.

Closes #165

#### Functionality

`huffc File.huff` -> Doesn't produce an interface.
`huffc File.huff -g` -> Outputs an `IFile.sol` interface.
`huffc File.huff -g MyInterface` -> Creates a `MyInterface.sol` interface.


Patch:
diff --git a/huff_cli/src/huffc.rs b/huff_cli/src/huffc.rs
index 12ed0f9..1eda490 100644
--- a/huff_cli/src/huffc.rs
+++ b/huff_cli/src/huffc.rs
@@ -7,7 +7,7 @@
 #![forbid(where_clauses_object_safety)]
 #![allow(deprecated)]
 
-use clap::Parser as ClapParser;
+use clap::{App, CommandFactory, Parser as ClapParser};
 use ethers_core::utils::hex;
 use huff_codegen::Codegen;
 use huff_core::Compiler;
@@ -56,8 +56,8 @@ struct Huff {
     optimize: bool,
 
     /// Generate solidity interface for a Huff artifact
-    #[clap(short = 'g', long = "interface")]
-    interface: bool,
+    #[clap(short = 'g', min_values = 0, long = "interface")]
+    interface: Option<String>,
 
     /// Generate and log bytecode.
     #[clap(short = 'b', long = "bytecode")]
@@ -89,6 +89,9 @@ pub(crate) fn get_input(prompt: &str) -> String {
 }
 
 fn main() {
+    // Into App
+    let app: App = Huff::into_app();
+
     // Parse the command line arguments
     let mut cli = Huff::parse();
 
@@ -204,9 +207,35 @@ fn main() {
                 std::process::exit(1);
             }
 
-            if cli.interface {
+            if app.get_matches().is_present("interface") {
+                let mut interface: Option<String> = None;
+                if artifacts.len() == 1 {
+                    let gen_interface: Option<String> = match artifacts[0]
+                        .file
+                        .path
+                        .split('/')
+                        .last()
+                    {
+                        Some(p) => match p.split('.').next() {
+                            Some(p) => Some(format!("I{}", p)),
+                            None => {
+                                tracing::warn!(target: "cli", "No file name found for artifact");
+                                None
+                            }
+                        },
+                        None => {
+                            tracing::warn!(target: "cli", "No trailing string");
+                            None
+                        }
+                    };
+                    interface = Some(cli.interface.unwrap_or_else(|| {
+                        gen_interface.unwrap_or_else(|| "Interface".to_string())
+                    }));
+                } else if cli.interface.is_some() {
+                    tracing::warn!(target: "cli", "Interface override ignored since multiple artifacts were generated");
+                }
                 tracing::info!(target: "cli", "GENERATING SOLIDITY INTERFACES FROM ARTIFACTS");
-                let interfaces = gen_sol_interfaces(&artifacts);
+                let interfaces = gen_sol_interfaces(&artifacts, interface);
                 if export_interfaces(&interfaces).is_ok() {
                     tracing::info!(target: "cli", "GENERATED SOLIDITY INTERFACES FROM ARTIFACTS SUCCESSFULLY");
                     println!(
@@ -214,7 +243,7 @@ fn main() {
                         Paint::blue(
                             interfaces
                                 .into_iter()
-                                .map(|(_, i, _)| format!("I{}.sol", i))
+                                .map(|(_, i, _)| format!("{}.sol", i))
                                 .collect::<Vec<_>>()
                                 .join(", ")
                         )
diff --git a/huff_parser/src/lib.rs b/huff_parser/src/lib.rs
index b617489..0331edf 100644
--- a/huff_parser/src/lib.rs
+++ b/huff_parser/src/lib.rs
@@ -405,18 +405,18 @@ impl Parser {
         };
 
         // Get arguments for signature
-        let inputs = self.parse_args(true, true, false, false)?;
+        let parameters = self.parse_args(true, true, false, false)?;
 
         let mut selector = [0u8; 4]; // Only keep first 4 bytes
         let input_types =
-            inputs.iter().map(|i| i.arg_type.as_ref().unwrap().clone()).collect::<Vec<_>>();
+            parameters.iter().map(|i| i.arg_type.as_ref().unwrap().clone()).collect::<Vec<_>>();
         hash_bytes(&mut selector, &format!("{}({})", name, input_types.join(",")));
 
         // Clone spans and set to nothing
         let new_spans = self.spans.clone();
         self.spans = vec![];
 
-        Ok(ErrorDefinition { name, selector, span: AstSpan(new_spans) })
+        Ok(ErrorDefinition { name, selector, parameters, span: AstSpan(new_spans) })
     }
 
     /// Parses a macro.
diff --git a/huff_utils/src/abi.rs b/huff_utils/src/abi.rs
index bc820d9..27d8fbb 100644
--- a/huff_utils/src/abi.rs
+++ b/huff_utils/src/abi.rs
@@ -53,6 +53,8 @@ pub struct Abi {
     pub functions: BTreeMap<String, Function>,
     /// A list of events and their definitions
     pub events: BTreeMap<String, Event>,
+    /// A list of errors and their definitions
+    pub errors: BTreeMap<String, Error>,
     /// If the contract defines receive logic
     pub receive: bool,
     /// If the contract defines fallback logic
@@ -113,72 +115,85 @@ impl From<ast::Contract> for Abi {
         // Instantiate functions and events
         let mut functions = BTreeMap::new();
         let mut events = BTreeMap::new();
+        let mut errors = BTreeMap::new();
 
         // Translate contract functions
         // Excluding constructor
-        contract
-            .functions
-            .iter()
-            .filter(|function: &&ast::Function| function.name != "CONSTRUCTOR")
-            .map(|function| {
-                (
-                    function.name.to_string(),
-                    Function {
-                        name: function.name.to_string(),
-                        inputs: function
-                            .inputs
-                            .iter()
-                            .map(|argument| FunctionParam {
-                                name: argument.name.clone().unwrap_or_default(),
-                                kind: argument.arg_type.clone().unwrap_or_default().into(),
-                                internal_type: None,
-                            })
-                            .collect(),
-                        outputs: function
-                            .outputs
-                            .iter()
-                            .map(|argument| FunctionParam {
-                                name: argument.name.clone().unwrap_or_default(),
-                                kind: argument.arg_type.clone().unwrap_or_default().into(),
-                                internal_type: None,
-                            })
-                            .collect(),
-                        constant: false,
-                        state_mutability: function.fn_type.clone(),
-                    },
-                )
-            })
-            .for_each(|val| {
-                let _ = functions.insert(val.0, val.1);
-            });
+        functions.extend(
+            contract
+                .functions
+                .iter()
+                .filter(|function: &&ast::Function| function.name != "CONSTRUCTOR")
+                .map(|function| {
+                    (
+                        function.name.to_string(),
+                        Function {
+                            name: function.name.to_string(),
+                            inputs: function
+                                .inputs
+                                .iter()
+                                .map(|argument| FunctionParam {
+                                    name: argument.name.clone().unwrap_or_default(),
+                                    kind: argument.arg_type.clone().unwrap_or_default().into(),
+                                    internal_type: None,
+                                })
+                                .collect(),
+                            outputs: function
+                                .outputs
+                                .iter()
+                                .map(|argument| FunctionParam {
+                                    name: argument.name.clone().unwrap_or_default(),
+                                    kind: argument.arg_type.clone().unwrap_or_default().into(),
+                                    internal_type: None,
+                                })
+                                .collect(),
+                            constant: false,
+                            state_mutability: function.fn_type.clone(),
+                        },
+                    )
+                }),
+        );
 
         // Translate contract events
-        contract
-            .events
-            .iter()
-            .map(|event| {
-                (
-                    event.name.to_string(),
-                    Event {
-                        name: event.name.to_string(),
-                        inputs: event
-                            .parameters
-                            .iter()
-                            .map(|argument| EventParam {
-                                name: argument.name.clone().unwrap_or_default(),
-                                kind: argument.arg_type.clone().unwrap_or_default().into(),
-                                indexed: argument.indexed,
-                            })
-                            .collect(),
-                        anonymous: false,
-                    },
-                )
-            })
-            .for_each(|val| {
-                let _ = events.insert(val.0, val.1);
-            });
+        events.extend(contract.events.iter().map(|event| {
+            (
+                event.name.to_string(),
+                Event {
+                    name: event.name.to_string(),
+                    inputs: event
+                        .parameters
+                        .iter()
+                        .map(|argument| EventParam {
+                            name: argument.name.clone().unwrap_or_default(),
+                            kind: argument.arg_type.clone().unwrap_or_default().into(),
+                            indexed: argument.indexed,
+                        })
+                        .collect(),
+                    anonymous: false,
+                },
+            )
+        }));
 
-        Self { constructor, functions, events, receive: false, fallback: false }
+        // Translate contract errors
+        errors.extend(contract.errors.iter().map(|error| {
+            (
+                error.name.to_string(),
+                Error {
+                    name: error.name.to_string(),
+                    inputs: error
+                        .parameters
+                        .iter()
+                        .map(|argument| FunctionParam {
+                            name: argument.name.clone().unwrap_or_default(),
+                            kind: argument.arg_type.clone().unwrap_or_default().into(),
+                            internal_type: None,
+                        })
+                        .collect(),
+                },
+            )
+        }));
+
+        Self { constructor, functions, events, errors, receive: false, fallback: false }
     }
 }
 
@@ -225,6 +240,17 @@ pub struct EventParam {
     pub indexed: bool,
 }
 
+/// #### Error
+///
+/// An Error definition.
+#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
+pub struct Error {
+    /// The error name
+    pub name: String,
+    /// The error inputs
+    pub inputs: Vec<FunctionParam>,
+}
+
 /// #### Constructor
 ///
 /// The contract constructor
diff --git a/huff_utils/src/ast.rs b/huff_utils/src/ast.rs
index 53c676d..34e9d6d 100644
--- a/huff_utils/src/ast.rs
+++ b/huff_utils/src/ast.rs
@@ -631,6 +631,8 @@ pub struct ErrorDefinition {
     pub name: String,
     /// The Error's selector
     pub selector: [u8; 4],
+    /// The parameters of the error
+    pub parameters: Vec<Argument>,
     /// The Span of the Constant Definition
     pub span: AstSpan,
 }
diff --git a/huff_utils/src/sol_interface.rs b/huff_utils/src/sol_interface.rs
index 047d417..6a68929 100644
--- a/huff_utils/src/sol_interface.rs
+++ b/huff_utils/src/sol_interface.rs
@@ -9,7 +9,10 @@ use std::{
 ///
 /// @param artifacts The vector of artifacts to generate interfaces from.
 /// @return The vector of generated interfaces.
-pub fn gen_sol_interfaces(artifacts: &Vec<Arc<Artifact>>) -> Vec<(PathBuf, &str, String)> {
+pub fn gen_sol_interfaces(
+    artifacts: &Vec<Arc<Artifact>>,
+    interface: Option<String>,
+) -> Vec<(PathBuf, String, String)> {
     let mut interfaces = Vec::new();
 
     for artifact in artifacts {
@@ -37,6 +40,22 @@ pub fn gen_sol_interfaces(artifacts: &Vec<Arc<Artifact>>) -> Vec<(PathBuf, &str,
                         .join(", "),
                 ));
             });
+            a.errors.iter().for_each(|(_, e)| {
+                defs.push(format!(
+                    "{}error {}({});",
+                    "\t",
+                    e.name,
+                    e.inputs
+                        .iter()
+                        .map(|i| format!(
+                            "{}{}",
+                            i.kind,
+                            if i.kind.is_memory_type() { " memory" } else { "" }
+                        ))
+                        .collect::<Vec<_>>()
+                        .join(", "),
+                ));
+            });
             a.functions.iter().for_each(|(_, f)| {
                 defs.push(format!(
                     "{}function {}({}) external{}{};",
@@ -71,12 +90,17 @@ pub fn gen_sol_interfaces(artifacts: &Vec<Arc<Artifact>>) -> Vec<(PathBuf, &str,
                 ));
             });
 
-            let interface_name =
-                artifact.file.path.split('/').last().unwrap().split('.').next().unwrap();
+            let interface_name = interface.clone().unwrap_or_else(|| {
+                format!(
+                    "I{}",
+                    artifact.file.path.split('/').last().unwrap().split('.').next().unwrap()
+                )
+            });
+            let formatted_str = format!("interface {} {{\n{}\n}}", interface_name, defs.join("\n"));
             interfaces.push((
                 Path::new(&artifact.file.path).parent().unwrap().to_path_buf(),
                 interface_name,
-                format!("interface I{} {{\n{}\n}}", interface_name, defs.join("\n"),),
+                formatted_str,
             ));
         }
     }
@@ -88,9 +112,11 @@ pub fn gen_sol_interfaces(artifacts: &Vec<Arc<Artifact>>) -> Vec<(PathBuf, &str,
 ///
 /// @param interfaces The vector of generated interfaces.
 /// @return Unit type if success, error if failure.
-pub fn export_interfaces(interfaces: &Vec<(PathBuf, &str, String)>) -> Result<(), std::io::Error> {
+pub fn export_interfaces(
+    interfaces: &Vec<(PathBuf, String, String)>,
+) -> Result<(), std::io::Error> {
     for (path, name, interface) in interfaces {
-        let path_str = format!("{}/I{}.sol", path.to_str().unwrap_or(""), name);
+        let path_str = format!("{}/{}.sol", path.to_str().unwrap_or(""), name);
         let file_path = Path::new(&path_str);
         fs::write(file_path, interface)?;
     }


Test patch:
diff --git a/huff_codegen/tests/abigen.rs b/huff_codegen/tests/abigen.rs
index 1761e30..94b91bc 100644
--- a/huff_codegen/tests/abigen.rs
+++ b/huff_codegen/tests/abigen.rs
@@ -34,6 +34,7 @@ fn constructs_valid_abi() {
             constructor: Some(Constructor { inputs: vec![] }),
             functions: BTreeMap::new(),
             events: BTreeMap::new(),
+            errors: BTreeMap::new(),
             receive: false,
             fallback: false
         }
diff --git a/huff_parser/tests/error.rs b/huff_parser/tests/error.rs
index 8ffcc6a..7395864 100644
--- a/huff_parser/tests/error.rs
+++ b/huff_parser/tests/error.rs
@@ -18,6 +18,12 @@ fn test_parses_custom_error() {
         ErrorDefinition {
             name: String::from("TestError"),
             selector: [124, 104, 44, 83],
+            parameters: vec![Argument {
+                arg_type: Some(String::from("uint256")),
+                name: None,
+                indexed: false,
+                span: AstSpan(vec![Span { start: 24, end: 31, file: None }])
+            }],
             span: AstSpan(vec![
                 Span { start: 0, end: 7, file: None },
                 Span { start: 8, end: 13, file: None },