Repository: huff-language/huff-rs
Pull request #164 (2022-07-07 22:07:56)

feat(huff_codegen): Functions
## Overview

Adds the ability to manually outline macros by defining an `fn` instead of a `macro`.

Syntax:
```
#define fn FUNC() = takes (0) returns (0) {
    // ...
}
```

### Motivation

Currently, when macros are invoked, their bytecode is pasted at the location of the invocation. This can quickly increase the size of a contract if a macro is used commonly throughout. By allowing manual outlining of macros, developers can reduce the deployment size of their contract for a small runtime gas cost trade-off.

Extra runtime gas cost when calling a function: `22 + n_inputs * 3 + n_outputs * 3`

### Checklist
- [x] Finish implementation
  - [x] Prevent label collision if multiple outlined calls exist.
    - [x] Return `JUMPDEST` PCs are now stored on the stack rather than in memory, allowing for nested function invocations as well as removing the need for a memory allocator.  
- [x] Testing
  - [x]  Add tests to the lexer, parser, and codegen.
  - [x] Test against contracts compiled with no macro outlining in Foundry and ensure results do not differ. 
- [x] Clean up :broom: 


Patch:
diff --git a/huff_codegen/README.md b/huff_codegen/README.md
index cc9ad30..a46ec3c 100644
--- a/huff_codegen/README.md
+++ b/huff_codegen/README.md
@@ -81,6 +81,7 @@ let contract = Contract {
       takes: 0,
       returns: 0,
       span: AstSpan(vec![]),
+      outlined: false,
     }
   ],
   invocations: vec![],
@@ -132,6 +133,7 @@ let contract = Contract {
       takes: 0,
       returns: 0,
       span: AstSpan(vec![]),
+      outlined: false,
     }
   ],
   invocations: vec![],
diff --git a/huff_codegen/src/irgen/statements.rs b/huff_codegen/src/irgen/statements.rs
index 87a1e06..6e85671 100644
--- a/huff_codegen/src/irgen/statements.rs
+++ b/huff_codegen/src/irgen/statements.rs
@@ -42,12 +42,57 @@ pub fn statement_gen(
 
             tracing::info!(target: "codegen", "FOUND INNER MACRO: {}", ir_macro.name);
 
-            // Recurse into macro invocation
-            scope.push(ir_macro.clone());
-            mis.push((*offset, mi.clone()));
+            // If invoked macro is a function (outlined), insert a jump to the function's code and a
+            // jumpdest to return to. If it is inlined, insert the macro's code at the
+            // current offset.
+            if ir_macro.outlined {
+                // Get necessary swap ops to reorder stack
+                // PC of the return jumpdest should be below the function's stack inputs
+                let stack_swaps = (0..ir_macro.takes)
+                    .rev()
+                    .map(|i| format!("{:02x}", 0x90 + i))
+                    .collect::<Vec<_>>();
 
-            let mut res: BytecodeRes =
-                match Codegen::macro_to_bytecode(ir_macro.clone(), contract, scope, *offset, mis) {
+                // Insert a jump to the outlined macro's code
+                jump_table.insert(
+                    *offset + stack_swaps.len() + 3, // PUSH2 + 2 bytes + stack_swaps.len()
+                    vec![Jump {
+                        label: format!("goto_{}", &ir_macro.name),
+                        bytecode_index: 0,
+                        span: s.span.clone(),
+                    }],
+                );
+
+                // Store return JUMPDEST PC on the stack and re-order the stack so that
+                // the return JUMPDEST PC is below the function's stack inputs
+                bytes.push((
+                    *offset,
+                    Bytes(format!(
+                        "{}{:04x}{}",
+                        Opcode::Push2,
+                        *offset + stack_swaps.len() + 7,
+                        stack_swaps.join("")
+                    )),
+                ));
+                // Insert jump to outlined macro + jumpdest to return to
+                bytes.push((
+                    *offset + stack_swaps.len() + 3, // PUSH2 + 2 bytes + stack_swaps.len()
+                    Bytes(format!("{}xxxx{}{}", Opcode::Push2, Opcode::Jump, Opcode::Jumpdest)),
+                ));
+                // PUSH2 + 2 bytes + stack_swaps.len() + PUSH2 + 2 bytes + JUMP + JUMPDEST
+                *offset += stack_swaps.len() + 8;
+            } else {
+                // Recurse into macro invocation
+                scope.push(ir_macro.clone());
+                mis.push((*offset, mi.clone()));
+
+                let mut res: BytecodeRes = match Codegen::macro_to_bytecode(
+                    ir_macro.clone(),
+                    contract,
+                    scope,
+                    *offset,
+                    mis,
+                ) {
                     Ok(r) => r,
                     Err(e) => {
                         tracing::error!(
@@ -59,27 +104,28 @@ pub fn statement_gen(
                     }
                 };
 
-            // Set jump table values
-            tracing::debug!(target: "codegen", "Unmatched jumps: {:?}", res.unmatched_jumps.iter().map(|uj| uj.label.clone()).collect::<Vec<String>>());
-            for j in res.unmatched_jumps.iter_mut() {
-                let new_index = j.bytecode_index;
-                j.bytecode_index = 0;
-                let mut new_jumps = if let Some(jumps) = jump_table.get(&new_index) {
-                    jumps.clone()
-                } else {
-                    vec![]
-                };
-                new_jumps.push(j.clone());
-                jump_table.insert(new_index, new_jumps);
-            }
-            table_instances.extend(res.table_instances);
-            label_indices.extend(res.label_indices);
-            utilized_tables.extend(res.utilized_tables);
+                // Set jump table values
+                tracing::debug!(target: "codegen", "Unmatched jumps: {:?}", res.unmatched_jumps.iter().map(|uj| uj.label.clone()).collect::<Vec<String>>());
+                for j in res.unmatched_jumps.iter_mut() {
+                    let new_index = j.bytecode_index;
+                    j.bytecode_index = 0;
+                    let mut new_jumps = if let Some(jumps) = jump_table.get(&new_index) {
+                        jumps.clone()
+                    } else {
+                        vec![]
+                    };
+                    new_jumps.push(j.clone());
+                    jump_table.insert(new_index, new_jumps);
+                }
+                table_instances.extend(res.table_instances);
+                label_indices.extend(res.label_indices);
+                utilized_tables.extend(res.utilized_tables);
 
-            // Increase offset by byte length of recursed macro
-            *offset += res.bytes.iter().map(|(_, b)| b.0.len()).sum::<usize>() / 2;
-            // Add the macro's bytecode to the final result
-            bytes = [bytes, res.bytes].concat()
+                // Increase offset by byte length of recursed macro
+                *offset += res.bytes.iter().map(|(_, b)| b.0.len()).sum::<usize>() / 2;
+                // Add the macro's bytecode to the final result
+                bytes = [bytes, res.bytes].concat()
+            }
         }
         StatementType::Label(label) => {
             // Add JUMPDEST opcode to final result and add to label_indices
@@ -90,8 +136,7 @@ pub fn statement_gen(
         }
         StatementType::LabelCall(label) => {
             // Generate code for a `LabelCall`
-            // PUSH2 + 2 byte destination (placeholder for now, filled at the bottom
-            // of this function)
+            // PUSH2 + 2 byte destination (placeholder for now, filled in `Codegen::fill_unmatched`
             tracing::info!(target: "codegen", "RECURSE BYTECODE GOT LABEL CALL: {}", label);
             jump_table.insert(
                 *offset,
diff --git a/huff_codegen/src/lib.rs b/huff_codegen/src/lib.rs
index 425f567..73ffbf9 100644
--- a/huff_codegen/src/lib.rs
+++ b/huff_codegen/src/lib.rs
@@ -10,6 +10,7 @@ use huff_utils::{
     ast::*,
     bytecode::*,
     error::CodegenError,
+    evm::Opcode,
     prelude::{
         bytes32_to_string, format_even_bytes, pad_n_bytes, CodegenErrorKind, FileSource, Span,
     },
@@ -316,8 +317,24 @@ impl Codegen {
             tracing::warn!(target: "codegen", "ATTEMPTED MACRO INVOCATION POP FAILED AT SCOPE: {}", scope.len());
         }
 
-        let bytecode: String = bytes.iter().map(|byte| byte.0.to_string()).collect();
-        tracing::info!(target: "codegen", "MACRO \"{}\" GENERATED BYTECODE EXCLUDING JUMPS: {}", macro_def.name, bytecode);
+        // Add functions (outlined macros) to the end of the bytecode if the scope length == 1
+        // (i.e., we're at the top level of recursion)
+        if scope.len() == 1 {
+            bytes = Codegen::append_functions(
+                contract,
+                scope,
+                &mut offset,
+                mis,
+                &mut jump_table,
+                &mut label_indices,
+                &mut table_instances,
+                bytes,
+            )?;
+        } else {
+            // If the scope length is > 1, we're processing a child macro. Since we're done
+            // with it, it can be popped.
+            scope.pop();
+        }
 
         // Fill JUMPDEST placeholders
         let (bytes, unmatched_jumps) = Codegen::fill_unmatched(bytes, &jump_table, &label_indices)?;
@@ -392,6 +409,70 @@ impl Codegen {
         Ok((bytes, unmatched_jumps))
     }
 
+    /// Helper associated function to append functions to the end of the bytecode.
+    ///
+    /// ## Overview
+    ///
+    /// Iterates over the contract's functions, generates their bytecode, fills unmatched jumps &
+    /// label indices, and appends the functions' bytecode to the end of the contract's bytecode.
+    ///
+    /// On success, passes ownership of `bytes` back to the caller.
+    /// On failure, returns a CodegenError.
+    #[allow(clippy::too_many_arguments)]
+    pub fn append_functions(
+        contract: &Contract,
+        scope: &mut Vec<MacroDefinition>,
+        offset: &mut usize,
+        mis: &mut Vec<(usize, MacroInvocation)>,
+        jump_table: &mut JumpTable,
+        label_indices: &mut LabelIndices,
+        table_instances: &mut Jumps,
+        mut bytes: Vec<(usize, Bytes)>,
+    ) -> Result<Vec<(usize, Bytes)>, CodegenError> {
+        for macro_def in contract.macros.iter().filter(|m| m.outlined) {
+            // Push the function to the scope
+            scope.push(macro_def.clone());
+
+            // Add 1 to starting offset to account for the JUMPDEST opcode
+            let mut res =
+                Codegen::macro_to_bytecode(macro_def.clone(), contract, scope, *offset + 1, mis)?;
+
+            for j in res.unmatched_jumps.iter_mut() {
+                let new_index = j.bytecode_index;
+                j.bytecode_index = 0;
+                let mut new_jumps = if let Some(jumps) = jump_table.get(&new_index) {
+                    jumps.clone()
+                } else {
+                    vec![]
+                };
+                new_jumps.push(j.clone());
+                jump_table.insert(new_index, new_jumps);
+            }
+            table_instances.extend(res.table_instances);
+            label_indices.extend(res.label_indices);
+
+            let macro_code_len = res.bytes.iter().map(|(_, b)| b.0.len()).sum::<usize>() / 2;
+
+            // Get necessary swap ops to reorder stack
+            // PC of the return jumpdest should be above the function's outputs on the stack
+            let stack_swaps =
+                (0..macro_def.returns).map(|i| format!("{:02x}", 0x90 + i)).collect::<Vec<_>>();
+
+            // Insert JUMPDEST, stack swaps, and final JUMP back to the location of invocation.
+            bytes.push((*offset, Bytes(Opcode::Jumpdest.to_string())));
+            res.bytes.push((
+                *offset + macro_code_len + 1,
+                Bytes(format!("{}{}", stack_swaps.join(""), Opcode::Jump)),
+            ));
+            bytes = [bytes, res.bytes].concat();
+            // Add the jumpdest to the beginning of the outlined macro.
+            label_indices.insert(format!("goto_{}", macro_def.name.clone()), *offset);
+            *offset += macro_code_len + stack_swaps.len() + 2; // JUMPDEST + MACRO_CODE_LEN +
+                                                               // stack_swaps.len() + JUMP
+        }
+        Ok(bytes)
+    }
+
     /// Generate a codegen artifact
     ///
     /// # Arguments
diff --git a/huff_lexer/src/lib.rs b/huff_lexer/src/lib.rs
index d4cbbd8..fe39412 100644
--- a/huff_lexer/src/lib.rs
+++ b/huff_lexer/src/lib.rs
@@ -268,7 +268,7 @@ impl<'a> Lexer<'a> {
     /// `TokenKind::Ident`.
     ///
     /// Rules:
-    /// - The `macro`, `function`, `constant`, `event`, `jumptable`, `jumptable__packed`, and
+    /// - The `macro`, `fn`, `function`, `constant`, `event`, `jumptable`, `jumptable__packed`, and
     ///   `table` keywords must be preceded by a `#define` keyword.
     /// - The `takes` keyword must be preceded by an assignment operator: `=`.
     /// - The `nonpayable`, `payable`, `view`, and `pure` keywords must be preceeded by one of these
@@ -278,6 +278,7 @@ impl<'a> Lexer<'a> {
     pub fn check_keyword_rules(&mut self, found_kind: &Option<TokenKind>) -> bool {
         match found_kind {
             Some(TokenKind::Macro) |
+            Some(TokenKind::Fn) |
             Some(TokenKind::Function) |
             Some(TokenKind::Constant) |
             Some(TokenKind::Event) |
@@ -379,6 +380,7 @@ impl<'a> Iterator for Lexer<'a> {
 
                     let keys = [
                         TokenKind::Macro,
+                        TokenKind::Fn,
                         TokenKind::Function,
                         TokenKind::Constant,
                         TokenKind::Takes,
@@ -419,7 +421,9 @@ impl<'a> Iterator for Lexer<'a> {
 
                     if let Some(kind) = &found_kind {
                         match kind {
-                            TokenKind::Macro => self.context = Context::MacroDefinition,
+                            TokenKind::Macro | TokenKind::Fn => {
+                                self.context = Context::MacroDefinition
+                            }
                             TokenKind::Function | TokenKind::Event => self.context = Context::Abi,
                             TokenKind::Constant => self.context = Context::Constant,
                             TokenKind::CodeTable => self.context = Context::CodeTableBody,
diff --git a/huff_parser/README.md b/huff_parser/README.md
index 904c072..1258ced 100644
--- a/huff_parser/README.md
+++ b/huff_parser/README.md
@@ -45,6 +45,7 @@ let expected_contract = Contract {
       takes: 0,
       returns: 0,
       span: AstSpan(vec![Span { start: 0, end: 7, file: None }, Span { start: 8, end: 13, file: None }, Span { start: 14, end: 25, file: None }, Span { start: 25, end: 26, file: None }, Span { start: 26, end: 27, file: None }, Span { start: 28, end: 29, file: None }, Span { start: 30, end: 35, file: None }, Span { start: 35, end: 36, file: None }, Span { start: 36, end: 37, file: None }, Span { start: 37, end: 38, file: None }, Span { start: 39, end: 46, file: None }, Span { start: 46, end: 47, file: None }, Span { start: 47, end: 48, file: None }, Span { start: 48, end: 49, file: None }, Span { start: 50, end: 51, file: None }, Span { start: 51, end: 52, file: None }]),
+      outlined: false,
     }
   ],
   invocations: vec![],
diff --git a/huff_parser/src/lib.rs b/huff_parser/src/lib.rs
index f671113..5101a0a 100644
--- a/huff_parser/src/lib.rs
+++ b/huff_parser/src/lib.rs
@@ -88,7 +88,7 @@ impl Parser {
                     tracing::info!(target: "parser", "SUCCESSFULLY PARSED CONSTANT {}", c.name);
                     contract.constants.push(c);
                 }
-                TokenKind::Macro => {
+                TokenKind::Macro | TokenKind::Fn => {
                     let m = self.parse_macro()?;
                     tracing::info!(target: "parser", "SUCCESSFULLY PARSED MACRO {}", m.name);
                     contract.macros.push(m);
@@ -104,7 +104,7 @@ impl Parser {
                     );
                     return Err(ParserError {
                         kind: ParserErrorKind::InvalidDefinition(self.current_token.kind.clone()),
-                        hint: Some("Definition must be one of: `function`, `event`, `constant`, or `macro`.".to_string()),
+                        hint: Some("Definition must be one of: `function`, `event`, `constant`, `macro`, or `fn`.".to_string()),
                         spans: AstSpan(vec![self.current_token.span.clone()]),
                     })
                 }
@@ -378,7 +378,8 @@ impl Parser {
     ///
     /// It should parse the following : macro MACRO_NAME(args...) = takes (x) returns (n) {...}
     pub fn parse_macro(&mut self) -> Result<MacroDefinition, ParserError> {
-        self.match_kind(TokenKind::Macro)?;
+        let outlined = self.check(TokenKind::Fn);
+        self.match_kind(if outlined { TokenKind::Fn } else { TokenKind::Macro })?;
         let macro_name: String =
             self.match_kind(TokenKind::Ident("MACRO_NAME".to_string()))?.to_string();
         tracing::info!(target: "parser", "PARSING MACRO: \"{}\"", macro_name);
@@ -398,6 +399,7 @@ impl Parser {
             macro_takes,
             macro_returns,
             self.spans.clone(),
+            outlined,
         ))
     }
 
diff --git a/huff_utils/src/ast.rs b/huff_utils/src/ast.rs
index 9c6209d..70ee54b 100644
--- a/huff_utils/src/ast.rs
+++ b/huff_utils/src/ast.rs
@@ -427,6 +427,8 @@ pub struct MacroDefinition {
     pub returns: usize,
     /// The Span of the Macro Definition
     pub span: AstSpan,
+    /// Is the macro a function (outlined)?
+    pub outlined: bool,
 }
 
 impl ToIRBytecode<CodegenError> for MacroDefinition {
@@ -445,8 +447,17 @@ impl MacroDefinition {
         takes: usize,
         returns: usize,
         spans: Vec<Span>,
+        outlined: bool,
     ) -> Self {
-        MacroDefinition { name, parameters, statements, takes, returns, span: AstSpan(spans) }
+        MacroDefinition {
+            name,
+            parameters,
+            statements,
+            takes,
+            returns,
+            span: AstSpan(spans),
+            outlined,
+        }
     }
 
     /// Translate statements into IRBytes
@@ -500,7 +511,7 @@ impl MacroDefinition {
                     });
                 }
                 StatementType::LabelCall(jump_to) => {
-                    /* Jump To doesn't translate directly to bytecode ? */
+                    /* Jump To doesn't translate directly to bytecode */
                     inner_irbytes.push(IRBytes {
                         ty: IRByteType::Statement(Statement {
                             ty: StatementType::LabelCall(jump_to.to_string()),
@@ -510,7 +521,7 @@ impl MacroDefinition {
                     });
                 }
                 StatementType::Label(l) => {
-                    /* Jump Dests don't translate directly to bytecode ? */
+                    /* Jump Dests don't translate directly to bytecode */
                     inner_irbytes.push(IRBytes {
                         ty: IRByteType::Statement(Statement {
                             ty: StatementType::Label(l.clone()),
diff --git a/huff_utils/src/token.rs b/huff_utils/src/token.rs
index 67a8a71..c572a7b 100644
--- a/huff_utils/src/token.rs
+++ b/huff_utils/src/token.rs
@@ -35,6 +35,8 @@ pub enum TokenKind {
     Include,
     /// "macro" keyword
     Macro,
+    /// "fn" keyword
+    Fn,
     /// "function" keyword
     Function,
     /// "event" keyword
@@ -126,6 +128,7 @@ impl fmt::Display for TokenKind {
             TokenKind::Define => "#define",
             TokenKind::Include => "#include",
             TokenKind::Macro => "macro",
+            TokenKind::Fn => "fn",
             TokenKind::Function => "function",
             TokenKind::Event => "event",
             TokenKind::Constant => "constant",


Test patch:
diff --git a/huff_codegen/tests/abigen.rs b/huff_codegen/tests/abigen.rs
index 7473956..b05bc6a 100644
--- a/huff_codegen/tests/abigen.rs
+++ b/huff_codegen/tests/abigen.rs
@@ -12,6 +12,7 @@ fn constructs_valid_abi() {
         takes: 0,
         returns: 0,
         span: AstSpan(vec![]),
+        outlined: false,
     };
     let contract = Contract {
         macros: vec![constructor],
@@ -47,6 +48,7 @@ fn missing_constructor_fails() {
         takes: 0,
         returns: 0,
         span: AstSpan(vec![]),
+        outlined: false,
     };
     let contract = Contract {
         macros: vec![],
diff --git a/huff_core/tests/functions.rs b/huff_core/tests/functions.rs
new file mode 100644
index 0000000..555e4db
--- /dev/null
+++ b/huff_core/tests/functions.rs
@@ -0,0 +1,213 @@
+use huff_codegen::Codegen;
+use huff_lexer::Lexer;
+use huff_parser::Parser;
+use huff_utils::prelude::{FileSource, FullFileSource, Token};
+use std::sync::Arc;
+
+#[test]
+fn test_function() {
+    let source: &str = r#"
+        #define function test1(uint256,uint256,uint256) pure returns(uint256)
+        #define function test2(uint256,uint256,uint256) pure returns(uint256)
+        #define function test3(uint256,uint256,uint256) pure returns(uint256)
+
+        #define fn MUL_DIV_DOWN() = takes (3) returns (1) {
+            // Input stack:      [x, y, denominator]
+            dup3              // [denominator, x, y, denominator]
+            dup3              // [y, denominator, x, y, denominator]
+            dup3              // [x, y, denominator, x, y, denominator]
+
+            mul               // [x * y, denominator, x, y, denominator]
+            0x00 mstore       // [denominator, x, y, denominator]
+
+            iszero iszero     // [denominator != 0, x, y, denominator]
+
+            swap1             // [x, denominator != 0, y, denominator]
+            dup1              // [x, x, denominator != 0, y, denominator]
+            iszero            // [x == 0, x, denominator != 0, y, denominator]
+
+            swap1             // [x, x == 0, denominator != 0, y, denominator]
+            0x00 mload        // [x * y, x, x == 0, denominator != 0, y, denominator]
+            div               // [(x * y) / x, x == 0, denominator != 0, y, denominator]
+
+            dup4              // [y, (x * y) / x, x == 0, denominator != 0, y, denominator]
+            eq                // [y == (x * y) / x, x == 0, denominator != 0, y, denominator]
+            or                // [y == (x * y) / x | x == 0, denominator != 0, y, denominator]
+            and               // [(y == (x * y) / x | x == 0) & denominator != 0, y, denominator]
+
+            iszero fail jumpi // Revert if (y == (x * y) / x | x == 0) & denominator != 0 is not satisfied
+
+            pop               // [denominator]
+            0x00 mload        // [x * y, denominator]
+            div               // [(x * y) / denominator]
+
+            0x01 finish jumpi
+
+            fail:
+                0x00 0x00 revert
+            finish:
+            // Return stack:     [(x * y) / denominator]
+        }
+
+        #define macro TEST_1() = takes(0) returns(0) {
+            0x44 calldataload // [denominator]
+            0x24 calldataload // [y, denominator]
+            0x04 calldataload // [x, y, denominator]
+            MUL_DIV_DOWN()
+            0x00 mstore
+            0x20 0x00 return
+        }
+
+        #define macro TEST_2() = takes(0) returns(0) {
+            0x44 calldataload // [denominator]
+            0x24 calldataload // [y, denominator]
+            0x04 calldataload // [x, y, denominator]
+            MUL_DIV_DOWN()
+            0x00 mstore
+            0x20 0x00 return
+        }
+
+        #define macro TEST_3() = takes(0) returns(0) {
+            0x44 calldataload // [denominator]
+            0x24 calldataload // [y, denominator]
+            0x04 calldataload // [x, y, denominator]
+            MUL_DIV_DOWN()
+            0x00 mstore
+            0x20 0x00 return
+        }
+
+        #define macro MAIN() = takes (0) returns (0) {
+            0x00 calldataload 0xE0 shr
+            dup1 __FUNC_SIG(test1) eq test_one jumpi
+            dup1 __FUNC_SIG(test2) eq test_two jumpi
+            dup1 __FUNC_SIG(test3) eq test_three jumpi
+
+            test_one:
+                TEST_1()
+            test_two:
+                TEST_2()
+            test_three:
+                TEST_3()
+        }
+    "#;
+
+    // Parse tokens
+    let flattened_source = FullFileSource { source, file: None, spans: vec![] };
+    let lexer = Lexer::new(flattened_source);
+    let tokens = lexer.into_iter().map(|x| x.unwrap()).collect::<Vec<Token>>();
+    let mut parser = Parser::new(tokens, None);
+
+    // Parse the AST
+    let mut contract = parser.parse().unwrap();
+
+    // Derive storage pointers
+    contract.derive_storage_pointers();
+
+    // Instantiate Codegen
+    let cg = Codegen::new();
+
+    // The codegen instance should have no artifact
+    assert!(cg.artifact.is_none());
+
+    // Have the Codegen create the runtime bytecode
+    let rbytes = Codegen::generate_main_bytecode(&contract).unwrap();
+    // Churn
+    let mut cg = Codegen::new();
+    let artifact =
+        cg.churn(Arc::clone(&Arc::new(FileSource::default())), vec![], &rbytes, "").unwrap();
+    assert_eq!(artifact.bytecode, String::from("60ad8060093d393df360003560e01c8063075900201461002757806319715c0d1461004457806327902d6914610061575b60443560243560043561003b92919061007e565b60005260206000f35b60443560243560043561005892919061007e565b60005260206000f35b60443560243560043561007592919061007e565b60005260206000f35b828282026000521515908015906000510483141716156100a457506000510460016100aa575b60006000fd5b9056"));
+}
+
+#[test]
+fn test_nested_function() {
+    let source: &str = r#"
+        #define function test1(uint256,uint256,uint256) pure returns(uint256)
+        #define function test2(uint256,uint256,uint256) pure returns(uint256)
+        #define function test3(uint256,uint256,uint256) pure returns(uint256)
+
+        #define fn MUL_DIV_DOWN() = takes (3) returns (1) {
+            // Input stack:      [x, y, denominator]
+            dup3              // [denominator, x, y, denominator]
+            dup3              // [y, denominator, x, y, denominator]
+            dup3              // [x, y, denominator, x, y, denominator]
+
+            mul               // [x * y, denominator, x, y, denominator]
+            0x00 mstore       // [denominator, x, y, denominator]
+
+            iszero iszero     // [denominator != 0, x, y, denominator]
+
+            swap1             // [x, denominator != 0, y, denominator]
+            dup1              // [x, x, denominator != 0, y, denominator]
+            iszero            // [x == 0, x, denominator != 0, y, denominator]
+
+            swap1             // [x, x == 0, denominator != 0, y, denominator]
+            0x00 mload        // [x * y, x, x == 0, denominator != 0, y, denominator]
+            div               // [(x * y) / x, x == 0, denominator != 0, y, denominator]
+
+            dup4              // [y, (x * y) / x, x == 0, denominator != 0, y, denominator]
+            eq                // [y == (x * y) / x, x == 0, denominator != 0, y, denominator]
+            or                // [y == (x * y) / x | x == 0, denominator != 0, y, denominator]
+            and               // [(y == (x * y) / x | x == 0) & denominator != 0, y, denominator]
+
+            iszero fail jumpi // Revert if (y == (x * y) / x | x == 0) & denominator != 0 is not satisfied
+
+            pop               // [denominator]
+            0x00 mload        // [x * y, denominator]
+            div               // [(x * y) / denominator]
+
+            0x01 finish jumpi
+
+            fail:
+                0x00 0x00 revert
+            finish:
+            // Return stack:     [(x * y) / denominator]
+        }
+
+        #define fn MUL_DIV_DOWN_2() = takes (3) returns (1) {
+            MUL_DIV_DOWN()
+        }
+
+        #define macro TEST_1() = takes(0) returns(0) {
+            0x44 calldataload // [denominator]
+            0x24 calldataload // [y, denominator]
+            0x04 calldataload // [x, y, denominator]
+            MUL_DIV_DOWN_2()
+            0x00 mstore
+            0x20 0x00 return
+        }
+
+        #define macro MAIN() = takes (0) returns (0) {
+            0x00 calldataload 0xE0 shr
+            dup1 __FUNC_SIG(test1) eq test_one jumpi
+
+            test_one:
+                TEST_1()
+        }
+    "#;
+
+    // Parse tokens
+    let flattened_source = FullFileSource { source, file: None, spans: vec![] };
+    let lexer = Lexer::new(flattened_source);
+    let tokens = lexer.into_iter().map(|x| x.unwrap()).collect::<Vec<Token>>();
+    let mut parser = Parser::new(tokens, None);
+
+    // Parse the AST
+    let mut contract = parser.parse().unwrap();
+
+    // Derive storage pointers
+    contract.derive_storage_pointers();
+
+    // Instantiate Codegen
+    let cg = Codegen::new();
+
+    // The codegen instance should have no artifact
+    assert!(cg.artifact.is_none());
+
+    // Have the Codegen create the runtime bytecode
+    let rbytes = Codegen::generate_main_bytecode(&contract).unwrap();
+    // Churn
+    let mut cg = Codegen::new();
+    let artifact =
+        cg.churn(Arc::clone(&Arc::new(FileSource::default())), vec![], &rbytes, "").unwrap();
+    assert_eq!(artifact.bytecode, String::from("606b8060093d393df360003560e01c80630759002014610011575b60443560243560043561002592919061005d565b60005260206000f35b82828202600052151590801590600051048314171615610054575060005104600161005a575b60006000fd5b90565b61006892919061002e565b9056"));
+}
diff --git a/huff_core/tests/parser_errors.rs b/huff_core/tests/parser_errors.rs
index dd8a150..18831bd 100644
--- a/huff_core/tests/parser_errors.rs
+++ b/huff_core/tests/parser_errors.rs
@@ -1,7 +1,6 @@
 use huff_lexer::*;
 use huff_parser::*;
 use huff_utils::prelude::*;
-use rand::prelude::*;
 
 #[test]
 fn test_invalid_macro_statement() {
@@ -96,7 +95,7 @@ fn test_invalid_definition() {
                 ParserError {
                     kind: ParserErrorKind::InvalidDefinition(TokenKind::Ident("test".to_string())),
                     hint: Some(
-                        "Definition must be one of: `function`, `event`, `constant`, or `macro`."
+                        "Definition must be one of: `function`, `event`, `constant`, `macro`, or `fn`."
                             .to_string()
                     ),
                     spans: AstSpan(vec![Span {
diff --git a/huff_lexer/tests/keywords.rs b/huff_lexer/tests/keywords.rs
index f235a66..5f9554d 100644
--- a/huff_lexer/tests/keywords.rs
+++ b/huff_lexer/tests/keywords.rs
@@ -34,6 +34,38 @@ fn parses_macro_keyword() {
     assert!(lexer.eof);
 }
 
+#[test]
+fn parses_fn_keyword() {
+    let source = "#define fn";
+    let flattened_source = FullFileSource { source, file: None, spans: vec![] };
+    let mut lexer = Lexer::new(flattened_source);
+
+    // Define Identifier first
+    let tok = lexer.next();
+    let unwrapped = tok.unwrap().unwrap();
+    let define_span = Span::new(0..7, None);
+    assert_eq!(unwrapped, Token::new(TokenKind::Define, define_span.clone()));
+    assert_eq!(lexer.current_span().deref(), &define_span);
+
+    // The next token should be the whitespace
+    let tok = lexer.next();
+    let unwrapped = tok.unwrap().unwrap();
+    let whitespace_span = Span::new(7..8, None);
+    assert_eq!(unwrapped, Token::new(TokenKind::Whitespace, whitespace_span.clone()));
+    assert_eq!(lexer.current_span().deref(), &whitespace_span);
+
+    // Lastly we should parse the fn keyword
+    let tok = lexer.next();
+    let unwrapped = tok.unwrap().unwrap();
+    let macro_span = Span::new(8..10, None);
+    assert_eq!(unwrapped, Token::new(TokenKind::Fn, macro_span.clone()));
+    assert_eq!(lexer.current_span().deref(), &macro_span);
+
+    // We covered the whole source
+    assert_eq!(lexer.current_span().end, source.len());
+    assert!(lexer.eof);
+}
+
 #[test]
 fn parses_function_keyword() {
     let source = "#define function";
@@ -270,6 +302,7 @@ fn parses_function_type_keywords() {
 fn parses_function_definition_with_keyword_name() {
     let key_words = [
         "macro",
+        "fn",
         "function",
         "constant",
         "takes",
@@ -332,6 +365,7 @@ fn parses_function_definition_with_keyword_name() {
 fn parses_label_with_keyword_name() {
     let key_words = [
         "macro",
+        "fn",
         "function",
         "constant",
         "takes",
@@ -389,6 +423,7 @@ fn parses_label_with_keyword_name() {
 fn parses_function_with_keyword_name() {
     let key_words = [
         "macro",
+        "fn",
         "function",
         "constant",
         "takes",
@@ -437,6 +472,7 @@ fn parses_function_with_keyword_name() {
 fn parses_function_with_keyword_name_in_macro() {
     let key_words = [
         "macro",
+        "fn",
         "function",
         "constant",
         "takes",
@@ -536,6 +572,7 @@ fn parses_keyword_arbitrary_whitespace() {
     // Macro, constant, and function keywords first- they are all preceded by "#define"
     let key_words = [
         ("macro", TokenKind::Macro),
+        ("fn", TokenKind::Fn),
         ("constant", TokenKind::Constant),
         ("function", TokenKind::Function),
     ];
diff --git a/huff_parser/tests/labels.rs b/huff_parser/tests/labels.rs
index c162434..c73a86b 100644
--- a/huff_parser/tests/labels.rs
+++ b/huff_parser/tests/labels.rs
@@ -160,6 +160,7 @@ fn multiline_labels() {
             // "}"
             Span { start: 158, end: 159, file: None },
         ]),
+        outlined: false,
     };
     assert_eq!(macro_definition.name, md_expected.name);
     assert_eq!(macro_definition.parameters, md_expected.parameters);
diff --git a/huff_parser/tests/macro.rs b/huff_parser/tests/macro.rs
index dcf45e9..34aacae 100644
--- a/huff_parser/tests/macro.rs
+++ b/huff_parser/tests/macro.rs
@@ -36,6 +36,7 @@ fn empty_macro() {
             Span { start: 50, end: 51, file: None },
             Span { start: 51, end: 52, file: None },
         ]),
+        outlined: false,
     };
     assert_eq!(macro_definition, expected);
     assert_eq!(parser.current_token.kind, TokenKind::Eof);
@@ -102,6 +103,7 @@ fn macro_with_simple_body() {
             Span { start: 75, end: 78, file: None },
             Span { start: 79, end: 80, file: None },
         ]),
+        outlined: false,
     };
     assert_eq!(macro_definition, expected);
     assert_eq!(parser.current_token.kind, TokenKind::Eof);
@@ -280,6 +282,7 @@ fn macro_with_arg_calls() {
             Span { start: 1028, end: 1029, file: None },
             Span { start: 1055, end: 1056, file: None },
         ]),
+        outlined: false,
     };
     assert_eq!(macro_definition, expected);
     assert_eq!(parser.current_token.kind, TokenKind::Eof);
@@ -453,6 +456,7 @@ fn macro_labels() {
             Span { start: 205, end: 211, file: None },
             Span { start: 216, end: 217, file: None },
         ]),
+        outlined: false,
     };
     assert_eq!(macro_definition, expected);
     assert_eq!(parser.current_token.kind, TokenKind::Eof);
@@ -595,6 +599,7 @@ fn macro_invocation_with_arg_call() {
             Span { start: 173, end: 179, file: None },
             Span { start: 184, end: 185, file: None },
         ]),
+        outlined: false,
     };
     assert_eq!(macro_definition, expected);
     assert_eq!(parser.current_token.kind, TokenKind::Eof);
@@ -663,6 +668,115 @@ fn macro_with_builtin_fn_call() {
             Span { start: 81, end: 82, file: None },
             Span { start: 87, end: 88, file: None },
         ]),
+        outlined: false,
+    };
+    assert_eq!(macro_definition, expected);
+    assert_eq!(parser.current_token.kind, TokenKind::Eof);
+}
+
+// Outlined macros (functions) are parsed the same way as inlined macros, so there should be no
+// difference besides the spans as well as the outlined flag.
+#[test]
+fn empty_outlined_macro() {
+    let source = "#define fn HELLO_WORLD() = takes(0) returns(4) {}";
+    let flattened_source = FullFileSource { source, file: None, spans: vec![] };
+    let lexer = Lexer::new(flattened_source);
+    let tokens = lexer.into_iter().map(|x| x.unwrap()).collect::<Vec<Token>>();
+    let mut parser = Parser::new(tokens, None);
+
+    // Grab the first macro
+    let macro_definition = parser.parse().unwrap().macros[0].clone();
+    let expected = MacroDefinition {
+        name: "HELLO_WORLD".to_string(),
+        parameters: vec![],
+        statements: vec![],
+        takes: 0,
+        returns: 4,
+        span: AstSpan(vec![
+            Span { start: 0, end: 7, file: None },
+            Span { start: 8, end: 10, file: None },
+            Span { start: 11, end: 22, file: None },
+            Span { start: 22, end: 23, file: None },
+            Span { start: 23, end: 24, file: None },
+            Span { start: 25, end: 26, file: None },
+            Span { start: 27, end: 32, file: None },
+            Span { start: 32, end: 33, file: None },
+            Span { start: 33, end: 34, file: None },
+            Span { start: 34, end: 35, file: None },
+            Span { start: 36, end: 43, file: None },
+            Span { start: 43, end: 44, file: None },
+            Span { start: 44, end: 45, file: None },
+            Span { start: 45, end: 46, file: None },
+            Span { start: 47, end: 48, file: None },
+            Span { start: 48, end: 49, file: None },
+        ]),
+        outlined: true,
+    };
+    assert_eq!(macro_definition, expected);
+    assert_eq!(parser.current_token.kind, TokenKind::Eof);
+}
+
+#[test]
+fn outlined_macro_with_simple_body() {
+    let source = "#define fn HELLO_WORLD() = takes(3) returns(0) {\n0x00 mstore\n 0x01 0x02 add\n}";
+    let flattened_source = FullFileSource { source, file: None, spans: vec![] };
+    let lexer = Lexer::new(flattened_source);
+    let tokens = lexer.into_iter().map(|x| x.unwrap()).collect::<Vec<Token>>();
+    let mut parser = Parser::new(tokens, None);
+
+    // Grab the first macro
+    let macro_definition = parser.parse().unwrap().macros[0].clone();
+    let expected = MacroDefinition {
+        name: "HELLO_WORLD".to_string(),
+        parameters: vec![],
+        statements: vec![
+            Statement {
+                ty: StatementType::Literal(str_to_bytes32("00")),
+                span: AstSpan(vec![Span { start: 51, end: 53, file: None }]),
+            },
+            Statement {
+                ty: StatementType::Opcode(Opcode::Mstore),
+                span: AstSpan(vec![Span { start: 54, end: 60, file: None }]),
+            },
+            Statement {
+                ty: StatementType::Literal(str_to_bytes32("01")),
+                span: AstSpan(vec![Span { start: 64, end: 66, file: None }]),
+            },
+            Statement {
+                ty: StatementType::Literal(str_to_bytes32("02")),
+                span: AstSpan(vec![Span { start: 69, end: 71, file: None }]),
+            },
+            Statement {
+                ty: StatementType::Opcode(Opcode::Add),
+                span: AstSpan(vec![Span { start: 72, end: 75, file: None }]),
+            },
+        ],
+        takes: 3,
+        returns: 0,
+        span: AstSpan(vec![
+            Span { start: 0, end: 7, file: None },
+            Span { start: 8, end: 10, file: None },
+            Span { start: 11, end: 22, file: None },
+            Span { start: 22, end: 23, file: None },
+            Span { start: 23, end: 24, file: None },
+            Span { start: 25, end: 26, file: None },
+            Span { start: 27, end: 32, file: None },
+            Span { start: 32, end: 33, file: None },
+            Span { start: 33, end: 34, file: None },
+            Span { start: 34, end: 35, file: None },
+            Span { start: 36, end: 43, file: None },
+            Span { start: 43, end: 44, file: None },
+            Span { start: 44, end: 45, file: None },
+            Span { start: 45, end: 46, file: None },
+            Span { start: 47, end: 48, file: None },
+            Span { start: 51, end: 53, file: None },
+            Span { start: 54, end: 60, file: None },
+            Span { start: 64, end: 66, file: None },
+            Span { start: 69, end: 71, file: None },
+            Span { start: 72, end: 75, file: None },
+            Span { start: 76, end: 77, file: None },
+        ]),
+        outlined: true,
     };
     assert_eq!(macro_definition, expected);
     assert_eq!(parser.current_token.kind, TokenKind::Eof);