diff --git a/.aztec-sync-commit b/.aztec-sync-commit
index 6841c89b691..5a1cd9c70bd 100644
--- a/.aztec-sync-commit
+++ b/.aztec-sync-commit
@@ -1 +1 @@
-7ff9b71d8d87fc93ae7dbd8ba63f5176b0cd17be
+58e15edf7fd3d32267b0aed883fc84f6cee327c9
diff --git a/Cargo.lock b/Cargo.lock
index ad3dce229fc..d5ce8a10509 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -66,6 +66,26 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "acvm_cli"
+version = "0.40.0"
+dependencies = [
+ "acir",
+ "acvm",
+ "bn254_blackbox_solver",
+ "clap",
+ "color-eyre",
+ "const_format",
+ "nargo",
+ "paste",
+ "proptest",
+ "rand 0.8.5",
+ "thiserror",
+ "toml 0.7.6",
+ "tracing-appender",
+ "tracing-subscriber",
+]
+
 [[package]]
 name = "acvm_js"
 version = "0.41.0"
diff --git a/Cargo.toml b/Cargo.toml
index 2ddb9c9e28f..b8f9b9ceacc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,6 +25,7 @@ members = [
     "tooling/nargo_toml",
     "tooling/noirc_abi",
     "tooling/noirc_abi_wasm",
+    "tooling/acvm_cli",
     # ACVM
     "acvm-repo/acir_field",
     "acvm-repo/acir",
@@ -35,7 +36,7 @@ members = [
     "acvm-repo/blackbox_solver",
     "acvm-repo/bn254_blackbox_solver",
 ]
-default-members = ["tooling/nargo_cli"]
+default-members = ["tooling/nargo_cli", "tooling/acvm_cli"]
 resolver = "2"
 
 [workspace.package]
@@ -77,6 +78,7 @@ noir_lsp = { path = "tooling/lsp" }
 noir_debugger = { path = "tooling/debugger" }
 noirc_abi = { path = "tooling/noirc_abi" }
 bb_abstraction_leaks = { path = "tooling/bb_abstraction_leaks" }
+acvm_cli = { path = "tooling/acvm_cli" }
 
 # LSP
 async-lsp = { version = "0.1.0", default-features = false }
diff --git a/aztec_macros/src/lib.rs b/aztec_macros/src/lib.rs
index c21e26bdcad..1f3546cbb6a 100644
--- a/aztec_macros/src/lib.rs
+++ b/aztec_macros/src/lib.rs
@@ -1,28 +1,26 @@
-use std::borrow::{Borrow, BorrowMut};
-use std::vec;
+mod transforms;
+mod utils;
+
+use transforms::{
+    compute_note_hash_and_nullifier::inject_compute_note_hash_and_nullifier,
+    events::{generate_selector_impl, transform_events},
+    functions::{transform_function, transform_unconstrained, transform_vm_function},
+    storage::{
+        assign_storage_slots, check_for_storage_definition, check_for_storage_implementation,
+        generate_storage_implementation,
+    },
+};
 
-use convert_case::{Case, Casing};
-use iter_extended::vecmap;
-use noirc_errors::{Location, Spanned};
 use noirc_frontend::hir::def_collector::dc_crate::{UnresolvedFunctions, UnresolvedTraitImpl};
-use noirc_frontend::hir::def_map::{LocalModuleId, ModuleId};
-use noirc_frontend::macros_api::parse_program;
-use noirc_frontend::macros_api::FieldElement;
-use noirc_frontend::macros_api::{
-    BlockExpression, CallExpression, CastExpression, Distinctness, Expression, ExpressionKind,
-    ForLoopStatement, ForRange, FunctionDefinition, FunctionReturnType, HirContext, HirExpression,
-    HirLiteral, HirStatement, Ident, IndexExpression, LetStatement, Literal,
-    MemberAccessExpression, MethodCallExpression, NoirFunction, NoirStruct, Param, Path, PathKind,
-    Pattern, PrefixExpression, SecondaryAttribute, Signedness, Span, Statement, StatementKind,
-    StructType, Type, TypeImpl, UnaryOp, UnresolvedType, UnresolvedTypeData, Visibility,
-};
-use noirc_frontend::macros_api::{CrateId, FileId};
-use noirc_frontend::macros_api::{MacroError, MacroProcessor};
-use noirc_frontend::macros_api::{ModuleDefId, NodeInterner, SortedModule, StructId};
-use noirc_frontend::node_interner::{FuncId, TraitId, TraitImplId, TraitImplKind};
-use noirc_frontend::{
-    BinaryOpKind, ConstrainKind, ConstrainStatement, InfixExpression, ItemVisibility, Lambda,
-};
+
+use noirc_frontend::macros_api::SortedModule;
+use noirc_frontend::macros_api::{CrateId, MacroError};
+use noirc_frontend::macros_api::{FileId, MacroProcessor};
+use noirc_frontend::macros_api::{HirContext, SecondaryAttribute, Span};
+
+use utils::ast_utils::is_custom_attribute;
+use utils::checks::{check_for_aztec_dependency, has_aztec_dependency};
+use utils::{constants::MAX_CONTRACT_PRIVATE_FUNCTIONS, errors::AztecMacroError};
 pub struct AztecMacro;
 
 impl MacroProcessor for AztecMacro {
@@ -35,23 +33,14 @@ impl MacroProcessor for AztecMacro {
         transform(ast, crate_id, context)
     }
 
-    fn process_unresolved_traits_impls(
+    fn process_collected_defs(
         &self,
         crate_id: &CrateId,
         context: &mut HirContext,
-        unresolved_traits_impls: &[UnresolvedTraitImpl],
-        collected_functions: &mut Vec<UnresolvedFunctions>,
+        collected_trait_impls: &[UnresolvedTraitImpl],
+        collected_functions: &mut [UnresolvedFunctions],
     ) -> Result<(), (MacroError, FileId)> {
-        if has_aztec_dependency(crate_id, context) {
-            inject_compute_note_hash_and_nullifier(
-                crate_id,
-                context,
-                unresolved_traits_impls,
-                collected_functions,
-            )
-        } else {
-            Ok(())
-        }
+        transform_collected_defs(crate_id, context, collected_trait_impls, collected_functions)
     }
 
     fn process_typed_ast(
@@ -63,225 +52,6 @@ impl MacroProcessor for AztecMacro {
     }
 }
 
-const FUNCTION_TREE_HEIGHT: u32 = 5;
-const MAX_CONTRACT_PRIVATE_FUNCTIONS: usize = 2_usize.pow(FUNCTION_TREE_HEIGHT);
-
-#[derive(Debug, Clone)]
-pub enum AztecMacroError {
-    AztecDepNotFound,
-    ContractHasTooManyPrivateFunctions { span: Span },
-    ContractConstructorMissing { span: Span },
-    UnsupportedFunctionArgumentType { span: Span, typ: UnresolvedTypeData },
-    UnsupportedStorageType { span: Option<Span>, typ: UnresolvedTypeData },
-    CouldNotAssignStorageSlots { secondary_message: Option<String> },
-    EventError { span: Span, message: String },
-    UnsupportedAttributes { span: Span, secondary_message: Option<String> },
-}
-
-impl From<AztecMacroError> for MacroError {
-    fn from(err: AztecMacroError) -> Self {
-        match err {
-            AztecMacroError::AztecDepNotFound {} => MacroError {
-                primary_message: "Aztec dependency not found. Please add aztec as a dependency in your Cargo.toml. For more information go to https://docs.aztec.network/developers/debugging/aztecnr-errors#aztec-dependency-not-found-please-add-aztec-as-a-dependency-in-your-nargotoml".to_owned(),
-                secondary_message: None,
-                span: None,
-            },
-            AztecMacroError::ContractHasTooManyPrivateFunctions { span } => MacroError {
-                primary_message: format!("Contract can only have a maximum of {} private functions", MAX_CONTRACT_PRIVATE_FUNCTIONS),
-                secondary_message: None,
-                span: Some(span),
-            },
-            AztecMacroError::ContractConstructorMissing { span } => MacroError {
-                primary_message: "Contract must have a constructor function".to_owned(),
-                secondary_message: None,
-                span: Some(span),
-            },
-            AztecMacroError::UnsupportedFunctionArgumentType { span, typ } => MacroError {
-                primary_message: format!("Provided parameter type `{typ:?}` is not supported in Aztec contract interface"),
-                secondary_message: None,
-                span: Some(span),
-            },
-            AztecMacroError::UnsupportedStorageType { span, typ } => MacroError {
-                primary_message: format!("Provided storage type `{typ:?}` is not directly supported in Aztec. Please provide a custom storage implementation"),
-                secondary_message: None,
-                span,
-            },
-            AztecMacroError::CouldNotAssignStorageSlots { secondary_message } => MacroError {
-                primary_message: "Could not assign storage slots, please provide a custom storage implementation".to_string(),
-                secondary_message,
-                span: None,
-            },
-            AztecMacroError::EventError { span, message } => MacroError {
-                primary_message: message,
-                secondary_message: None,
-                span: Some(span),
-            },
-            AztecMacroError::UnsupportedAttributes { span, secondary_message } => MacroError {
-                primary_message: "Unsupported attributes in contract function".to_string(),
-                secondary_message,
-                span: Some(span),
-            },
-        }
-    }
-}
-
-//
-//             Helper macros for creating noir ast nodes
-//
-fn ident(name: &str) -> Ident {
-    Ident::new(name.to_string(), Span::default())
-}
-
-fn ident_path(name: &str) -> Path {
-    Path::from_ident(ident(name))
-}
-
-fn path(ident: Ident) -> Path {
-    Path::from_ident(ident)
-}
-
-fn expression(kind: ExpressionKind) -> Expression {
-    Expression::new(kind, Span::default())
-}
-
-fn variable(name: &str) -> Expression {
-    expression(ExpressionKind::Variable(ident_path(name)))
-}
-
-fn variable_ident(identifier: Ident) -> Expression {
-    expression(ExpressionKind::Variable(path(identifier)))
-}
-
-fn variable_path(path: Path) -> Expression {
-    expression(ExpressionKind::Variable(path))
-}
-
-fn method_call(object: Expression, method_name: &str, arguments: Vec<Expression>) -> Expression {
-    expression(ExpressionKind::MethodCall(Box::new(MethodCallExpression {
-        object,
-        method_name: ident(method_name),
-        arguments,
-    })))
-}
-
-fn call(func: Expression, arguments: Vec<Expression>) -> Expression {
-    expression(ExpressionKind::Call(Box::new(CallExpression { func: Box::new(func), arguments })))
-}
-
-fn pattern(name: &str) -> Pattern {
-    Pattern::Identifier(ident(name))
-}
-
-fn mutable(name: &str) -> Pattern {
-    Pattern::Mutable(Box::new(pattern(name)), Span::default(), true)
-}
-
-fn mutable_assignment(name: &str, assigned_to: Expression) -> Statement {
-    make_statement(StatementKind::Let(LetStatement {
-        pattern: mutable(name),
-        r#type: make_type(UnresolvedTypeData::Unspecified),
-        expression: assigned_to,
-    }))
-}
-
-fn mutable_reference(variable_name: &str) -> Expression {
-    expression(ExpressionKind::Prefix(Box::new(PrefixExpression {
-        operator: UnaryOp::MutableReference,
-        rhs: variable(variable_name),
-    })))
-}
-
-fn assignment(name: &str, assigned_to: Expression) -> Statement {
-    make_statement(StatementKind::Let(LetStatement {
-        pattern: pattern(name),
-        r#type: make_type(UnresolvedTypeData::Unspecified),
-        expression: assigned_to,
-    }))
-}
-
-fn member_access(lhs: &str, rhs: &str) -> Expression {
-    expression(ExpressionKind::MemberAccess(Box::new(MemberAccessExpression {
-        lhs: variable(lhs),
-        rhs: ident(rhs),
-    })))
-}
-
-fn return_type(path: Path) -> FunctionReturnType {
-    let ty = make_type(UnresolvedTypeData::Named(path, vec![], true));
-    FunctionReturnType::Ty(ty)
-}
-
-fn lambda(parameters: Vec<(Pattern, UnresolvedType)>, body: Expression) -> Expression {
-    expression(ExpressionKind::Lambda(Box::new(Lambda {
-        parameters,
-        return_type: UnresolvedType {
-            typ: UnresolvedTypeData::Unspecified,
-            span: Some(Span::default()),
-        },
-        body,
-    })))
-}
-
-fn make_eq(lhs: Expression, rhs: Expression) -> Expression {
-    expression(ExpressionKind::Infix(Box::new(InfixExpression {
-        lhs,
-        rhs,
-        operator: Spanned::from(Span::default(), BinaryOpKind::Equal),
-    })))
-}
-
-macro_rules! chained_path {
-    ( $base:expr ) => {
-        {
-            ident_path($base)
-        }
-    };
-    ( $base:expr $(, $tail:expr)* ) => {
-        {
-            let mut base_path = ident_path($base);
-            $(
-                base_path.segments.push(ident($tail));
-            )*
-            base_path
-        }
-    }
-}
-
-macro_rules! chained_dep {
-    ( $base:expr $(, $tail:expr)* ) => {
-        {
-            let mut base_path = ident_path($base);
-            base_path.kind = PathKind::Dep;
-            $(
-                base_path.segments.push(ident($tail));
-            )*
-            base_path
-        }
-    }
-}
-
-fn cast(lhs: Expression, ty: UnresolvedTypeData) -> Expression {
-    expression(ExpressionKind::Cast(Box::new(CastExpression { lhs, r#type: make_type(ty) })))
-}
-
-fn make_type(typ: UnresolvedTypeData) -> UnresolvedType {
-    UnresolvedType { typ, span: Some(Span::default()) }
-}
-
-fn index_array(array: Ident, index: &str) -> Expression {
-    expression(ExpressionKind::Index(Box::new(IndexExpression {
-        collection: variable_path(path(array)),
-        index: variable(index),
-    })))
-}
-
-fn index_array_variable(array: Expression, index: &str) -> Expression {
-    expression(ExpressionKind::Index(Box::new(IndexExpression {
-        collection: array,
-        index: variable(index),
-    })))
-}
-
 //
 //                    Create AST Nodes for Aztec
 //
@@ -294,7 +64,6 @@ fn transform(
     context: &HirContext,
 ) -> Result<SortedModule, (MacroError, FileId)> {
     // Usage -> mut ast -> aztec_library::transform(&mut ast)
-
     // Covers all functions in the ast
     for submodule in ast.submodules.iter_mut().filter(|submodule| submodule.is_contract) {
         if transform_module(&mut submodule.contents, crate_id, context)
@@ -306,97 +75,6 @@ fn transform(
     Ok(ast)
 }
 
-//
-//                    Transform Hir Nodes for Aztec
-//
-
-/// Completes the Hir with data gathered from type resolution
-fn transform_hir(
-    crate_id: &CrateId,
-    context: &mut HirContext,
-) -> Result<(), (AztecMacroError, FileId)> {
-    transform_events(crate_id, context)?;
-    assign_storage_slots(crate_id, context)
-}
-
-/// Creates an error alerting the user that they have not downloaded the Aztec-noir library
-fn check_for_aztec_dependency(
-    crate_id: &CrateId,
-    context: &HirContext,
-) -> Result<(), (MacroError, FileId)> {
-    if has_aztec_dependency(crate_id, context) {
-        Ok(())
-    } else {
-        Err((AztecMacroError::AztecDepNotFound.into(), context.crate_graph[crate_id].root_file_id))
-    }
-}
-
-fn has_aztec_dependency(crate_id: &CrateId, context: &HirContext) -> bool {
-    context.crate_graph[crate_id].dependencies.iter().any(|dep| dep.as_name() == "aztec")
-}
-
-// Check to see if the user has defined a storage struct
-fn check_for_storage_definition(module: &SortedModule) -> bool {
-    module.types.iter().any(|r#struct| r#struct.name.0.contents == "Storage")
-}
-
-// Check to see if the user has defined a storage struct
-fn check_for_storage_implementation(module: &SortedModule) -> bool {
-    module.impls.iter().any(|r#impl| match &r#impl.object_type.typ {
-        UnresolvedTypeData::Named(path, _, _) => {
-            path.segments.last().is_some_and(|segment| segment.0.contents == "Storage")
-        }
-        _ => false,
-    })
-}
-
-// Check if "compute_note_hash_and_nullifier(AztecAddress,Field,Field,Field,[Field; N]) -> [Field; 4]" is defined
-fn check_for_compute_note_hash_and_nullifier_definition(
-    functions_data: &[(LocalModuleId, FuncId, NoirFunction)],
-    module_id: LocalModuleId,
-) -> bool {
-    functions_data.iter().filter(|func_data| func_data.0 == module_id).any(|func_data| {
-        func_data.2.def.name.0.contents == "compute_note_hash_and_nullifier"
-                && func_data.2.def.parameters.len() == 5
-                && match &func_data.2.def.parameters[0].typ.typ {
-                    UnresolvedTypeData::Named(path, _, _) => path.segments.last().unwrap().0.contents == "AztecAddress",
-                    _ => false,
-                }
-                && func_data.2.def.parameters[1].typ.typ == UnresolvedTypeData::FieldElement
-                && func_data.2.def.parameters[2].typ.typ == UnresolvedTypeData::FieldElement
-                && func_data.2.def.parameters[3].typ.typ == UnresolvedTypeData::FieldElement
-                // checks if the 5th parameter is an array and the Box<UnresolvedType> in
-                // Array(Option<UnresolvedTypeExpression>, Box<UnresolvedType>) contains only fields
-                && match &func_data.2.def.parameters[4].typ.typ {
-                    UnresolvedTypeData::Array(_, inner_type) => {
-                        matches!(inner_type.typ, UnresolvedTypeData::FieldElement)
-                    },
-                    _ => false,
-                }
-                // We check the return type the same way as we did the 5th parameter
-                && match &func_data.2.def.return_type {
-                    FunctionReturnType::Default(_) => false,
-                    FunctionReturnType::Ty(unresolved_type) => {
-                        match &unresolved_type.typ {
-                            UnresolvedTypeData::Array(_, inner_type) => {
-                                matches!(inner_type.typ, UnresolvedTypeData::FieldElement)
-                            },
-                            _ => false,
-                        }
-                    }
-                }
-    })
-}
-
-/// Checks if an attribute is a custom attribute with a specific name
-fn is_custom_attribute(attr: &SecondaryAttribute, attribute_name: &str) -> bool {
-    if let SecondaryAttribute::Custom(custom_attr) = attr {
-        custom_attr.as_str() == attribute_name
-    } else {
-        false
-    }
-}
-
 /// Determines if ast nodes are annotated with aztec attributes.
 /// For annotated functions it calls the `transform` function which will perform the required transformations.
 /// Returns true if an annotated node is found, false otherwise
@@ -473,10 +151,7 @@ fn transform_module(
             transform_vm_function(func, storage_defined)
                 .map_err(|err| (err, crate_graph.root_file_id))?;
             has_transformed_module = true;
-        }
-
-        // Add the storage struct to the beginning of the function if it is unconstrained in an aztec contract
-        if storage_defined && func.def.is_unconstrained {
+        } else if storage_defined && func.def.is_unconstrained {
             transform_unconstrained(func);
             has_transformed_module = true;
         }
@@ -518,1402 +193,33 @@ fn transform_module(
     Ok(has_transformed_module)
 }
 
-/// Auxiliary function to generate the storage constructor for a given field, using
-/// the Storage definition as a reference. Supports nesting.
-fn generate_storage_field_constructor(
-    (type_ident, unresolved_type): &(Ident, UnresolvedType),
-    slot: Expression,
-) -> Result<Expression, AztecMacroError> {
-    let typ = &unresolved_type.typ;
-    match typ {
-        UnresolvedTypeData::Named(path, generics, _) => {
-            let mut new_path = path.clone().to_owned();
-            new_path.segments.push(ident("new"));
-            match path.segments.last().unwrap().0.contents.as_str() {
-                "Map" => Ok(call(
-                    variable_path(new_path),
-                    vec![
-                        variable("context"),
-                        slot,
-                        lambda(
-                            vec![
-                                (
-                                    pattern("context"),
-                                    make_type(UnresolvedTypeData::Named(
-                                        chained_dep!("aztec", "context", "Context"),
-                                        vec![],
-                                        true,
-                                    )),
-                                ),
-                                (
-                                    Pattern::Identifier(ident("slot")),
-                                    make_type(UnresolvedTypeData::FieldElement),
-                                ),
-                            ],
-                            generate_storage_field_constructor(
-                                &(type_ident.clone(), generics.iter().last().unwrap().clone()),
-                                variable("slot"),
-                            )?,
-                        ),
-                    ],
-                )),
-                _ => Ok(call(variable_path(new_path), vec![variable("context"), slot])),
-            }
-        }
-        _ => Err(AztecMacroError::UnsupportedStorageType {
-            typ: typ.clone(),
-            span: Some(type_ident.span()),
-        }),
-    }
-}
-
-// Generates the Storage implementation block from the Storage struct definition if it does not exist
-/// From:
-///
-/// struct Storage {
-///     a_map: Map<Field, SomeStoragePrimitive<ASerializableType>>,
-///     a_nested_map: Map<Field, Map<Field, SomeStoragePrimitive<ASerializableType>>>,
-///     a_field: SomeStoragePrimitive<ASerializableType>,
-/// }
-///
-/// To:
-///
-/// impl Storage {
-///    fn init(context: Context) -> Self {
-///        Storage {
-///             a_map: Map::new(context, 0, |context, slot| {
-///                 SomeStoragePrimitive::new(context, slot)
-///             }),
-///             a_nested_map: Map::new(context, 0, |context, slot| {
-///                 Map::new(context, slot, |context, slot| {
-///                     SomeStoragePrimitive::new(context, slot)
-///                })
-///            }),
-///            a_field: SomeStoragePrimitive::new(context, 0),
-///         }
-///    }
-/// }
-///
-/// Storage slots are generated as 0 and will be populated using the information from the HIR
-/// at a later stage.
-fn generate_storage_implementation(module: &mut SortedModule) -> Result<(), AztecMacroError> {
-    let definition =
-        module.types.iter().find(|r#struct| r#struct.name.0.contents == "Storage").unwrap();
-
-    let slot_zero = expression(ExpressionKind::Literal(Literal::Integer(
-        FieldElement::from(i128::from(0)),
-        false,
-    )));
-
-    let field_constructors = definition
-        .fields
-        .iter()
-        .flat_map(|field| {
-            generate_storage_field_constructor(field, slot_zero.clone())
-                .map(|expression| (field.0.clone(), expression))
-        })
-        .collect();
-
-    let storage_constructor_statement = make_statement(StatementKind::Expression(expression(
-        ExpressionKind::constructor((chained_path!("Storage"), field_constructors)),
-    )));
-
-    let init = NoirFunction::normal(FunctionDefinition::normal(
-        &ident("init"),
-        &vec![],
-        &[(
-            ident("context"),
-            make_type(UnresolvedTypeData::Named(
-                chained_dep!("aztec", "context", "Context"),
-                vec![],
-                true,
-            )),
-        )],
-        &BlockExpression(vec![storage_constructor_statement]),
-        &[],
-        &return_type(chained_path!("Self")),
-    ));
-
-    let storage_impl = TypeImpl {
-        object_type: UnresolvedType {
-            typ: UnresolvedTypeData::Named(chained_path!("Storage"), vec![], true),
-            span: Some(Span::default()),
-        },
-        type_span: Span::default(),
-        generics: vec![],
-        methods: vec![(init, Span::default())],
-    };
-    module.impls.push(storage_impl);
-
-    Ok(())
-}
-
-/// If it does, it will insert the following things:
-/// - A new Input that is provided for a kernel app circuit, named: {Public/Private}ContextInputs
-/// - Hashes all of the function input variables
-///     - This instantiates a helper function
-fn transform_function(
-    ty: &str,
-    func: &mut NoirFunction,
-    storage_defined: bool,
-    is_initializer: bool,
-    insert_init_check: bool,
-    is_internal: bool,
-) -> Result<(), AztecMacroError> {
-    let context_name = format!("{}Context", ty);
-    let inputs_name = format!("{}ContextInputs", ty);
-    let return_type_name = format!("{}CircuitPublicInputs", ty);
-
-    // Add check that msg sender equals this address and flag function as internal
-    if is_internal {
-        let is_internal_check = create_internal_check(func.name());
-        func.def.body.0.insert(0, is_internal_check);
-        func.def.is_internal = true;
-    }
-
-    // Add initialization check
-    if insert_init_check {
-        let init_check = create_init_check();
-        func.def.body.0.insert(0, init_check);
-    }
-
-    // Add access to the storage struct
-    if storage_defined {
-        let storage_def = abstract_storage(&ty.to_lowercase(), false);
-        func.def.body.0.insert(0, storage_def);
-    }
-
-    // Insert the context creation as the first action
-    let create_context = create_context(&context_name, &func.def.parameters)?;
-    func.def.body.0.splice(0..0, (create_context).iter().cloned());
-
-    // Add the inputs to the params
-    let input = create_inputs(&inputs_name);
-    func.def.parameters.insert(0, input);
-
-    // Abstract return types such that they get added to the kernel's return_values
-    if let Some(return_values) = abstract_return_values(func) {
-        // In case we are pushing return values to the context, we remove the statement that originated it
-        // This avoids running duplicate code, since blocks like if/else can be value returning statements
-        func.def.body.0.pop();
-        // Add the new return statement
-        func.def.body.0.push(return_values);
-    }
-
-    // Before returning mark the contract as initialized
-    if is_initializer {
-        let mark_initialized = create_mark_as_initialized(ty);
-        func.def.body.0.push(mark_initialized);
-    }
-
-    // Push the finish method call to the end of the function
-    let finish_def = create_context_finish();
-    func.def.body.0.push(finish_def);
-
-    let return_type = create_return_type(&return_type_name);
-    func.def.return_type = return_type;
-    func.def.return_visibility = Visibility::Public;
-
-    // Distinct return types are only required for private functions
-    // Public functions should have open auto-inferred
-    match ty {
-        "Private" => func.def.return_distinctness = Distinctness::Distinct,
-        "Public" => func.def.is_open = true,
-        _ => (),
-    }
-
-    Ok(())
-}
-
-/// Transform a function to work with AVM bytecode
-fn transform_vm_function(
-    func: &mut NoirFunction,
-    _storage_defined: bool,
-) -> Result<(), AztecMacroError> {
-    // Push Avm context creation to the beginning of the function
-    let create_context = create_avm_context()?;
-    func.def.body.0.insert(0, create_context);
-
-    // We want the function to be seen as a public function
-    func.def.is_open = true;
-
-    // NOTE: the line below is a temporary hack to trigger external transpilation tools
-    // It will be removed once the transpiler is integrated into the Noir compiler
-    func.def.name.0.contents = format!("avm_{}", func.def.name.0.contents);
-    Ok(())
-}
-
-/// Transform Unconstrained
-///
-/// Inserts the following code at the beginning of an unconstrained function
-/// ```noir
-/// let storage = Storage::init(Context::none());
-/// ```
-///
-/// This will allow developers to access their contract' storage struct in unconstrained functions
-fn transform_unconstrained(func: &mut NoirFunction) {
-    func.def.body.0.insert(0, abstract_storage("Unconstrained", true));
-}
-
-fn collect_crate_structs(crate_id: &CrateId, context: &HirContext) -> Vec<StructId> {
-    context
-        .def_map(crate_id)
-        .expect("ICE: Missing crate in def_map")
-        .modules()
-        .iter()
-        .flat_map(|(_, module)| {
-            module.type_definitions().filter_map(|typ| {
-                if let ModuleDefId::TypeId(struct_id) = typ {
-                    Some(struct_id)
-                } else {
-                    None
-                }
-            })
-        })
-        .collect()
-}
-
-fn collect_traits(context: &HirContext) -> Vec<TraitId> {
-    let crates = context.crates();
-    crates
-        .flat_map(|crate_id| context.def_map(&crate_id).map(|def_map| def_map.modules()))
-        .flatten()
-        .flat_map(|module| {
-            module.type_definitions().filter_map(|typ| {
-                if let ModuleDefId::TraitId(struct_id) = typ {
-                    Some(struct_id)
-                } else {
-                    None
-                }
-            })
-        })
-        .collect()
-}
-
-/// Substitutes the signature literal that was introduced in the selector method previously with the actual signature.
-fn transform_event(
-    struct_id: StructId,
-    interner: &mut NodeInterner,
-) -> Result<(), (AztecMacroError, FileId)> {
-    let struct_type = interner.get_struct(struct_id);
-    let selector_id = interner
-        .lookup_method(&Type::Struct(struct_type.clone(), vec![]), struct_id, "selector", false)
-        .ok_or_else(|| {
-            let error = AztecMacroError::EventError {
-                span: struct_type.borrow().location.span,
-                message: "Selector method not found".to_owned(),
-            };
-            (error, struct_type.borrow().location.file)
-        })?;
-    let selector_function = interner.function(&selector_id);
-
-    let compute_selector_statement = interner.statement(
-        selector_function.block(interner).statements().first().ok_or_else(|| {
-            let error = AztecMacroError::EventError {
-                span: struct_type.borrow().location.span,
-                message: "Compute selector statement not found".to_owned(),
-            };
-            (error, struct_type.borrow().location.file)
-        })?,
-    );
-
-    let compute_selector_expression = match compute_selector_statement {
-        HirStatement::Expression(expression_id) => match interner.expression(&expression_id) {
-            HirExpression::Call(hir_call_expression) => Some(hir_call_expression),
-            _ => None,
-        },
-        _ => None,
-    }
-    .ok_or_else(|| {
-        let error = AztecMacroError::EventError {
-            span: struct_type.borrow().location.span,
-            message: "Compute selector statement is not a call expression".to_owned(),
-        };
-        (error, struct_type.borrow().location.file)
-    })?;
-
-    let first_arg_id = compute_selector_expression.arguments.first().ok_or_else(|| {
-        let error = AztecMacroError::EventError {
-            span: struct_type.borrow().location.span,
-            message: "Compute selector statement is not a call expression".to_owned(),
-        };
-        (error, struct_type.borrow().location.file)
-    })?;
-
-    match interner.expression(first_arg_id) {
-        HirExpression::Literal(HirLiteral::Str(signature))
-            if signature == SIGNATURE_PLACEHOLDER =>
-        {
-            let selector_literal_id = *first_arg_id;
-
-            let structure = interner.get_struct(struct_id);
-            let signature = event_signature(&structure.borrow());
-            interner.update_expression(selector_literal_id, |expr| {
-                *expr = HirExpression::Literal(HirLiteral::Str(signature.clone()));
-            });
-
-            // Also update the type! It might have a different length now than the placeholder.
-            interner.push_expr_type(
-                selector_literal_id,
-                Type::String(Box::new(Type::Constant(signature.len() as u64))),
-            );
-            Ok(())
-        }
-        _ => Err((
-            AztecMacroError::EventError {
-                span: struct_type.borrow().location.span,
-                message: "Signature placeholder literal does not match".to_owned(),
-            },
-            struct_type.borrow().location.file,
-        )),
-    }
-}
-
-fn transform_events(
+fn transform_collected_defs(
     crate_id: &CrateId,
     context: &mut HirContext,
-) -> Result<(), (AztecMacroError, FileId)> {
-    for struct_id in collect_crate_structs(crate_id, context) {
-        let attributes = context.def_interner.struct_attributes(&struct_id);
-        if attributes.iter().any(|attr| matches!(attr, SecondaryAttribute::Event)) {
-            transform_event(struct_id, &mut context.def_interner)?;
-        }
-    }
-    Ok(())
-}
-
-/// Obtains the serialized length of a type that implements the Serialize trait.
-fn get_serialized_length(
-    traits: &[TraitId],
-    typ: &Type,
-    interner: &NodeInterner,
-) -> Result<u64, AztecMacroError> {
-    let (struct_name, maybe_stored_in_state) = match typ {
-        Type::Struct(struct_type, generics) => {
-            Ok((struct_type.borrow().name.0.contents.clone(), generics.first()))
-        }
-        _ => Err(AztecMacroError::CouldNotAssignStorageSlots {
-            secondary_message: Some("State storage variable must be a struct".to_string()),
-        }),
-    }?;
-    let stored_in_state =
-        maybe_stored_in_state.ok_or(AztecMacroError::CouldNotAssignStorageSlots {
-            secondary_message: Some("State storage variable must be generic".to_string()),
-        })?;
-
-    let is_note = traits.iter().any(|&trait_id| {
-        let r#trait = interner.get_trait(trait_id);
-        r#trait.name.0.contents == "NoteInterface"
-            && !interner.lookup_all_trait_implementations(stored_in_state, trait_id).is_empty()
-    });
-
-    // Maps and (private) Notes always occupy a single slot. Someone could store a Note in PublicMutable for whatever reason though.
-    if struct_name == "Map" || (is_note && struct_name != "PublicMutable") {
-        return Ok(1);
-    }
-
-    let serialized_trait_impl_kind = traits
-        .iter()
-        .find_map(|&trait_id| {
-            let r#trait = interner.get_trait(trait_id);
-            if r#trait.borrow().name.0.contents == "Serialize"
-                && r#trait.borrow().generics.len() == 1
-            {
-                interner
-                    .lookup_all_trait_implementations(stored_in_state, trait_id)
-                    .into_iter()
-                    .next()
-            } else {
-                None
-            }
-        })
-        .ok_or(AztecMacroError::CouldNotAssignStorageSlots {
-            secondary_message: Some("Stored data must implement Serialize trait".to_string()),
-        })?;
-
-    let serialized_trait_impl_id = match serialized_trait_impl_kind {
-        TraitImplKind::Normal(trait_impl_id) => Ok(trait_impl_id),
-        _ => Err(AztecMacroError::CouldNotAssignStorageSlots { secondary_message: None }),
-    }?;
-
-    let serialized_trait_impl_shared = interner.get_trait_implementation(*serialized_trait_impl_id);
-    let serialized_trait_impl = serialized_trait_impl_shared.borrow();
-
-    match serialized_trait_impl.trait_generics.first().unwrap() {
-        Type::Constant(value) => Ok(*value),
-        _ => Err(AztecMacroError::CouldNotAssignStorageSlots { secondary_message: None }),
-    }
-}
-
-/// Assigns storage slots to the storage struct fields based on the serialized length of the types. This automatic assignment
-/// will only trigger if the assigned storage slot is invalid (0 as generated by generate_storage_implementation)
-fn assign_storage_slots(
-    crate_id: &CrateId,
-    context: &mut HirContext,
-) -> Result<(), (AztecMacroError, FileId)> {
-    let traits: Vec<_> = collect_traits(context);
-    for struct_id in collect_crate_structs(crate_id, context) {
-        let interner: &mut NodeInterner = context.def_interner.borrow_mut();
-        let r#struct = interner.get_struct(struct_id);
-        let file_id = r#struct.borrow().location.file;
-        if r#struct.borrow().name.0.contents == "Storage" && r#struct.borrow().id.krate().is_root()
-        {
-            let init_id = interner
-                .lookup_method(
-                    &Type::Struct(interner.get_struct(struct_id), vec![]),
-                    struct_id,
-                    "init",
-                    false,
-                )
-                .ok_or((
-                    AztecMacroError::CouldNotAssignStorageSlots {
-                        secondary_message: Some(
-                            "Storage struct must have an init function".to_string(),
-                        ),
-                    },
-                    file_id,
-                ))?;
-            let init_function = interner.function(&init_id).block(interner);
-            let init_function_statement_id = init_function.statements().first().ok_or((
-                AztecMacroError::CouldNotAssignStorageSlots {
-                    secondary_message: Some("Init storage statement not found".to_string()),
-                },
-                file_id,
-            ))?;
-            let storage_constructor_statement = interner.statement(init_function_statement_id);
-
-            let storage_constructor_expression = match storage_constructor_statement {
-                HirStatement::Expression(expression_id) => {
-                    match interner.expression(&expression_id) {
-                        HirExpression::Constructor(hir_constructor_expression) => {
-                            Ok(hir_constructor_expression)
-                        }
-                        _ => Err((AztecMacroError::CouldNotAssignStorageSlots {
-                            secondary_message: Some(
-                                "Storage constructor statement must be a constructor expression"
-                                    .to_string(),
-                            ),
-                        }, file_id))
-                    }
-                }
-                _ => Err((
-                    AztecMacroError::CouldNotAssignStorageSlots {
-                        secondary_message: Some(
-                            "Storage constructor statement must be an expression".to_string(),
-                        ),
-                    },
-                    file_id,
-                )),
-            }?;
-
-            let mut storage_slot: u64 = 1;
-            for (index, (_, expr_id)) in storage_constructor_expression.fields.iter().enumerate() {
-                let fields = r#struct.borrow().get_fields(&[]);
-                let (_, field_type) = fields.get(index).unwrap();
-                let new_call_expression = match interner.expression(expr_id) {
-                    HirExpression::Call(hir_call_expression) => Ok(hir_call_expression),
-                    _ => Err((
-                        AztecMacroError::CouldNotAssignStorageSlots {
-                            secondary_message: Some(
-                                "Storage field initialization expression is not a call expression"
-                                    .to_string(),
-                            ),
-                        },
-                        file_id,
-                    )),
-                }?;
-
-                let slot_arg_expression = interner.expression(&new_call_expression.arguments[1]);
-
-                let current_storage_slot = match slot_arg_expression {
-                    HirExpression::Literal(HirLiteral::Integer(slot, _)) => Ok(slot.to_u128()),
-                    _ => Err((
-                        AztecMacroError::CouldNotAssignStorageSlots {
-                            secondary_message: Some(
-                                "Storage slot argument expression must be a literal integer"
-                                    .to_string(),
-                            ),
-                        },
-                        file_id,
-                    )),
-                }?;
-
-                if current_storage_slot != 0 {
-                    continue;
-                }
-
-                let type_serialized_len = get_serialized_length(&traits, field_type, interner)
-                    .map_err(|err| (err, file_id))?;
-                interner.update_expression(new_call_expression.arguments[1], |expr| {
-                    *expr = HirExpression::Literal(HirLiteral::Integer(
-                        FieldElement::from(u128::from(storage_slot)),
-                        false,
-                    ));
-                });
-
-                storage_slot += type_serialized_len;
-            }
-        }
-    }
-    Ok(())
-}
-
-const SIGNATURE_PLACEHOLDER: &str = "SIGNATURE_PLACEHOLDER";
-
-/// Generates the impl for an event selector
-///
-/// Inserts the following code:
-/// ```noir
-/// impl SomeStruct {
-///    fn selector() -> FunctionSelector {
-///       aztec::protocol_types::abis::function_selector::FunctionSelector::from_signature("SIGNATURE_PLACEHOLDER")
-///    }
-/// }
-/// ```
-///
-/// This allows developers to emit events without having to write the signature of the event every time they emit it.
-/// The signature cannot be known at this point since types are not resolved yet, so we use a signature placeholder.
-/// It'll get resolved after by transforming the HIR.
-fn generate_selector_impl(structure: &NoirStruct) -> TypeImpl {
-    let struct_type =
-        make_type(UnresolvedTypeData::Named(path(structure.name.clone()), vec![], true));
-
-    let selector_path =
-        chained_dep!("aztec", "protocol_types", "abis", "function_selector", "FunctionSelector");
-    let mut from_signature_path = selector_path.clone();
-    from_signature_path.segments.push(ident("from_signature"));
-
-    let selector_fun_body = BlockExpression(vec![make_statement(StatementKind::Expression(call(
-        variable_path(from_signature_path),
-        vec![expression(ExpressionKind::Literal(Literal::Str(SIGNATURE_PLACEHOLDER.to_string())))],
-    )))]);
-
-    // Define `FunctionSelector` return type
-    let return_type =
-        FunctionReturnType::Ty(make_type(UnresolvedTypeData::Named(selector_path, vec![], true)));
-
-    let mut selector_fn_def = FunctionDefinition::normal(
-        &ident("selector"),
-        &vec![],
-        &[],
-        &selector_fun_body,
-        &[],
-        &return_type,
-    );
-
-    selector_fn_def.visibility = ItemVisibility::Public;
-
-    // Seems to be necessary on contract modules
-    selector_fn_def.return_visibility = Visibility::Public;
-
-    TypeImpl {
-        object_type: struct_type,
-        type_span: structure.span,
-        generics: vec![],
-        methods: vec![(NoirFunction::normal(selector_fn_def), Span::default())],
-    }
-}
-
-/// Helper function that returns what the private context would look like in the ast
-/// This should make it available to be consumed within aztec private annotated functions.
-///
-/// The replaced code:
-/// ```noir
-/// /// Before
-/// fn foo(inputs: PrivateContextInputs) {
-///    // ...
-/// }
-///
-/// /// After
-/// #[aztec(private)]
-/// fn foo() {
-///   // ...
-/// }
-fn create_inputs(ty: &str) -> Param {
-    let context_ident = ident("inputs");
-    let context_pattern = Pattern::Identifier(context_ident);
-
-    let path_snippet = ty.to_case(Case::Snake); // e.g. private_context_inputs
-    let type_path = chained_dep!("aztec", "context", "inputs", &path_snippet, ty);
-
-    let context_type = make_type(UnresolvedTypeData::Named(type_path, vec![], true));
-    let visibility = Visibility::Private;
-
-    Param { pattern: context_pattern, typ: context_type, visibility, span: Span::default() }
-}
-
-/// Creates an initialization check to ensure that the contract has been initialized, meant to
-/// be injected as the first statement of any function after the context has been created.
-///
-/// ```noir
-/// assert_is_initialized(&mut context);
-/// ```
-fn create_init_check() -> Statement {
-    make_statement(StatementKind::Expression(call(
-        variable_path(chained_dep!("aztec", "initializer", "assert_is_initialized")),
-        vec![mutable_reference("context")],
-    )))
-}
-
-/// Creates a call to mark_as_initialized which emits the initialization nullifier, meant to
-/// be injected as the last statement before returning in a constructor.
-///
-/// ```noir
-/// mark_as_initialized(&mut context);
-/// ```
-fn create_mark_as_initialized(ty: &str) -> Statement {
-    let name = if ty == "Public" { "mark_as_initialized_public" } else { "mark_as_initialized" };
-    make_statement(StatementKind::Expression(call(
-        variable_path(chained_dep!("aztec", "initializer", name)),
-        vec![mutable_reference("context")],
-    )))
-}
-
-/// Creates a check for internal functions ensuring that the caller is self.
-///
-/// ```noir
-/// assert(context.msg_sender() == context.this_address(), "Function can only be called internally");
-/// ```
-fn create_internal_check(fname: &str) -> Statement {
-    make_statement(StatementKind::Constrain(ConstrainStatement(
-        make_eq(
-            method_call(variable("context"), "msg_sender", vec![]),
-            method_call(variable("context"), "this_address", vec![]),
-        ),
-        Some(expression(ExpressionKind::Literal(Literal::Str(format!(
-            "Function {} can only be called internally",
-            fname
-        ))))),
-        ConstrainKind::Assert,
-    )))
-}
-
-/// Creates the private context object to be accessed within the function, the parameters need to be extracted to be
-/// appended into the args hash object.
-///
-/// The replaced code:
-/// ```noir
-/// #[aztec(private)]
-/// fn foo(structInput: SomeStruct, arrayInput: [u8; 10], fieldInput: Field) -> Field {
-///     // Create the hasher object
-///     let mut hasher = Hasher::new();
-///
-///     // struct inputs call serialize on them to add an array of fields
-///     hasher.add_multiple(structInput.serialize());
-///
-///     // Array inputs are iterated over and each element is added to the hasher (as a field)
-///     for i in 0..arrayInput.len() {
-///         hasher.add(arrayInput[i] as Field);
-///     }
-///     // Field inputs are added to the hasher
-///     hasher.add({ident});
-///
-///     // Create the context
-///     // The inputs (injected by this `create_inputs`) and completed hash object are passed to the context
-///     let mut context = PrivateContext::new(inputs, hasher.hash());
-/// }
-/// ```
-fn create_context(ty: &str, params: &[Param]) -> Result<Vec<Statement>, AztecMacroError> {
-    let mut injected_expressions: Vec<Statement> = vec![];
-
-    // `let mut hasher = Hasher::new();`
-    let let_hasher = mutable_assignment(
-        "hasher", // Assigned to
-        call(
-            variable_path(chained_dep!("aztec", "hasher", "Hasher", "new")), // Path
-            vec![],                                                          // args
-        ),
-    );
-
-    // Completes: `let mut hasher = Hasher::new();`
-    injected_expressions.push(let_hasher);
-
-    // Iterate over each of the function parameters, adding to them to the hasher
-    for Param { pattern, typ, span, .. } in params {
-        match pattern {
-            Pattern::Identifier(identifier) => {
-                // Match the type to determine the padding to do
-                let unresolved_type = &typ.typ;
-                let expression = match unresolved_type {
-                    // `hasher.add_multiple({ident}.serialize())`
-                    UnresolvedTypeData::Named(..) => add_struct_to_hasher(identifier),
-                    UnresolvedTypeData::Array(_, arr_type) => {
-                        add_array_to_hasher(identifier, arr_type)
-                    }
-                    // `hasher.add({ident})`
-                    UnresolvedTypeData::FieldElement => add_field_to_hasher(identifier),
-                    // Add the integer to the hasher, casted to a field
-                    // `hasher.add({ident} as Field)`
-                    UnresolvedTypeData::Integer(..) | UnresolvedTypeData::Bool => {
-                        add_cast_to_hasher(identifier)
-                    }
-                    UnresolvedTypeData::String(..) => {
-                        let (var_bytes, id) = str_to_bytes(identifier);
-                        injected_expressions.push(var_bytes);
-                        add_array_to_hasher(
-                            &id,
-                            &UnresolvedType {
-                                typ: UnresolvedTypeData::Integer(
-                                    Signedness::Unsigned,
-                                    noirc_frontend::IntegerBitSize::ThirtyTwo,
-                                ),
-                                span: None,
-                            },
-                        )
-                    }
-                    _ => {
-                        return Err(AztecMacroError::UnsupportedFunctionArgumentType {
-                            typ: unresolved_type.clone(),
-                            span: *span,
-                        })
-                    }
-                };
-                injected_expressions.push(expression);
-            }
-            _ => todo!(), // Maybe unreachable?
-        }
-    }
-
-    // Create the inputs to the context
-    let inputs_expression = variable("inputs");
-    // `hasher.hash()`
-    let hash_call = method_call(
-        variable("hasher"), // variable
-        "hash",             // method name
-        vec![],             // args
-    );
-
-    let path_snippet = ty.to_case(Case::Snake); // e.g. private_context
-
-    // let mut context = {ty}::new(inputs, hash);
-    let let_context = mutable_assignment(
-        "context", // Assigned to
-        call(
-            variable_path(chained_dep!("aztec", "context", &path_snippet, ty, "new")), // Path
-            vec![inputs_expression, hash_call],                                        // args
-        ),
-    );
-    injected_expressions.push(let_context);
-
-    // Return all expressions that will be injected by the hasher
-    Ok(injected_expressions)
-}
-
-/// Creates an mutable avm context
-///
-/// ```noir
-/// /// Before
-/// #[aztec(public-vm)]
-/// fn foo() -> Field {
-///   let mut context = aztec::context::AVMContext::new();
-///   let timestamp = context.timestamp();
-///   // ...
-/// }
-///
-/// /// After
-/// #[aztec(private)]
-/// fn foo() -> Field {
-///     let mut timestamp = context.timestamp();
-///     // ...
-/// }
-fn create_avm_context() -> Result<Statement, AztecMacroError> {
-    let let_context = mutable_assignment(
-        "context", // Assigned to
-        call(
-            variable_path(chained_dep!("aztec", "context", "AVMContext", "new")), // Path
-            vec![],                                                               // args
-        ),
-    );
-
-    Ok(let_context)
-}
-
-/// Abstract Return Type
-///
-/// This function intercepts the function's current return type and replaces it with pushes
-/// To the kernel
-///
-/// The replaced code:
-/// ```noir
-/// /// Before
-/// #[aztec(private)]
-/// fn foo() -> protocol_types::abis::private_circuit_public_inputs::PrivateCircuitPublicInputs {
-///   // ...
-///   let my_return_value: Field = 10;
-///   context.return_values.push(my_return_value);
-/// }
-///
-/// /// After
-/// #[aztec(private)]
-/// fn foo() -> Field {
-///     // ...
-///    let my_return_value: Field = 10;
-///    my_return_value
-/// }
-/// ```
-/// Similarly; Structs will be pushed to the context, after serialize() is called on them.
-/// Arrays will be iterated over and each element will be pushed to the context.
-/// Any primitive type that can be cast will be casted to a field and pushed to the context.
-fn abstract_return_values(func: &NoirFunction) -> Option<Statement> {
-    let current_return_type = func.return_type().typ;
-    let last_statement = func.def.body.0.last()?;
-
-    // TODO: (length, type) => We can limit the size of the array returned to be limited by kernel size
-    // Doesn't need done until we have settled on a kernel size
-    // TODO: support tuples here and in inputs -> convert into an issue
-    // Check if the return type is an expression, if it is, we can handle it
-    match last_statement {
-        Statement { kind: StatementKind::Expression(expression), .. } => {
-            match current_return_type {
-                // Call serialize on structs, push the whole array, calling push_array
-                UnresolvedTypeData::Named(..) => Some(make_struct_return_type(expression.clone())),
-                UnresolvedTypeData::Array(..) => Some(make_array_return_type(expression.clone())),
-                // Cast these types to a field before pushing
-                UnresolvedTypeData::Bool | UnresolvedTypeData::Integer(..) => {
-                    Some(make_castable_return_type(expression.clone()))
-                }
-                UnresolvedTypeData::FieldElement => Some(make_return_push(expression.clone())),
-                _ => None,
-            }
-        }
-        _ => None,
-    }
-}
-
-/// Abstract storage
-///
-/// For private functions:
-/// ```noir
-/// #[aztec(private)]
-/// fn lol() {
-///     let storage = Storage::init(Context::private(context));
-/// }
-/// ```
-///
-/// For public functions:
-/// ```noir
-/// #[aztec(public)]
-/// fn lol() {
-///    let storage = Storage::init(Context::public(context));
-/// }
-/// ```
-///
-/// For unconstrained functions:
-/// ```noir
-/// unconstrained fn lol() {
-///   let storage = Storage::init(Context::none());
-/// }
-fn abstract_storage(typ: &str, unconstrained: bool) -> Statement {
-    let init_context_call = if unconstrained {
-        call(
-            variable_path(chained_dep!("aztec", "context", "Context", "none")), // Path
-            vec![],                                                             // args
+    collected_trait_impls: &[UnresolvedTraitImpl],
+    collected_functions: &mut [UnresolvedFunctions],
+) -> Result<(), (MacroError, FileId)> {
+    if has_aztec_dependency(crate_id, context) {
+        inject_compute_note_hash_and_nullifier(
+            crate_id,
+            context,
+            collected_trait_impls,
+            collected_functions,
         )
     } else {
-        call(
-            variable_path(chained_dep!("aztec", "context", "Context", typ)), // Path
-            vec![mutable_reference("context")],                              // args
-        )
-    };
-
-    assignment(
-        "storage", // Assigned to
-        call(
-            variable_path(chained_path!("Storage", "init")), // Path
-            vec![init_context_call],                         // args
-        ),
-    )
-}
-
-/// Context Return Values
-///
-/// Creates an instance to the context return values
-/// ```noir
-/// `context.return_values`
-/// ```
-fn context_return_values() -> Expression {
-    member_access("context", "return_values")
-}
-
-fn make_statement(kind: StatementKind) -> Statement {
-    Statement { span: Span::default(), kind }
-}
-
-/// Make return Push
-///
-/// Translates to:
-/// `context.return_values.push({push_value})`
-fn make_return_push(push_value: Expression) -> Statement {
-    make_statement(StatementKind::Semi(method_call(
-        context_return_values(),
-        "push",
-        vec![push_value],
-    )))
-}
-
-/// Make Return push array
-///
-/// Translates to:
-/// `context.return_values.extend_from_array({push_value})`
-fn make_return_extend_from_array(push_value: Expression) -> Statement {
-    make_statement(StatementKind::Semi(method_call(
-        context_return_values(),
-        "extend_from_array",
-        vec![push_value],
-    )))
-}
-
-/// Make struct return type
-///
-/// Translates to:
-/// ```noir
-/// `context.return_values.extend_from_array({push_value}.serialize())`
-fn make_struct_return_type(expression: Expression) -> Statement {
-    let serialized_call = method_call(
-        expression,  // variable
-        "serialize", // method name
-        vec![],      // args
-    );
-    make_return_extend_from_array(serialized_call)
-}
-
-/// Make array return type
-///
-/// Translates to:
-/// ```noir
-/// for i in 0..{ident}.len() {
-///    context.return_values.push({ident}[i] as Field)
-/// }
-/// ```
-fn make_array_return_type(expression: Expression) -> Statement {
-    let inner_cast_expression =
-        cast(index_array_variable(expression.clone(), "i"), UnresolvedTypeData::FieldElement);
-    let assignment = make_statement(StatementKind::Semi(method_call(
-        context_return_values(), // variable
-        "push",                  // method name
-        vec![inner_cast_expression],
-    )));
-
-    create_loop_over(expression, vec![assignment])
-}
-
-/// Castable return type
-///
-/// Translates to:
-/// ```noir
-/// context.return_values.push({ident} as Field)
-/// ```
-fn make_castable_return_type(expression: Expression) -> Statement {
-    // Cast these types to a field before pushing
-    let cast_expression = cast(expression, UnresolvedTypeData::FieldElement);
-    make_return_push(cast_expression)
-}
-
-/// Create Return Type
-///
-/// Public functions return protocol_types::abis::public_circuit_public_inputs::PublicCircuitPublicInputs while
-/// private functions return protocol_types::abis::private_circuit_public_inputs::::PrivateCircuitPublicInputs
-///
-/// This call constructs an ast token referencing the above types
-/// The name is set in the function above `transform`, hence the
-/// whole token name is passed in
-///
-/// The replaced code:
-/// ```noir
-///
-/// /// Before
-/// fn foo() -> protocol_types::abis::private_circuit_public_inputs::PrivateCircuitPublicInputs {
-///    // ...
-/// }
-///
-/// /// After
-/// #[aztec(private)]
-/// fn foo() {
-///  // ...
-/// }
-fn create_return_type(ty: &str) -> FunctionReturnType {
-    let path_snippet = ty.to_case(Case::Snake); // e.g. private_circuit_public_inputs or public_circuit_public_inputs
-    let return_path = chained_dep!("aztec", "protocol_types", "abis", &path_snippet, ty);
-    return_type(return_path)
-}
-
-/// Create Context Finish
-///
-/// Each aztec function calls `context.finish()` at the end of a function
-/// to return values required by the kernel.
-///
-/// The replaced code:
-/// ```noir
-/// /// Before
-/// fn foo() -> protocol_types::abis::private_circuit_public_inputs::PrivateCircuitPublicInputs {
-///   // ...
-///  context.finish()
-/// }
-///
-/// /// After
-/// #[aztec(private)]
-/// fn foo() {
-///  // ...
-/// }
-fn create_context_finish() -> Statement {
-    let method_call = method_call(
-        variable("context"), // variable
-        "finish",            // method name
-        vec![],              // args
-    );
-    make_statement(StatementKind::Expression(method_call))
+        Ok(())
+    }
 }
 
 //
-//                 Methods to create hasher inputs
+//                    Transform Hir Nodes for Aztec
 //
 
-fn add_struct_to_hasher(identifier: &Ident) -> Statement {
-    // If this is a struct, we call serialize and add the array to the hasher
-    let serialized_call = method_call(
-        variable_path(path(identifier.clone())), // variable
-        "serialize",                             // method name
-        vec![],                                  // args
-    );
-
-    make_statement(StatementKind::Semi(method_call(
-        variable("hasher"),    // variable
-        "add_multiple",        // method name
-        vec![serialized_call], // args
-    )))
-}
-
-fn str_to_bytes(identifier: &Ident) -> (Statement, Ident) {
-    // let identifier_as_bytes = identifier.as_bytes();
-    let var = variable_ident(identifier.clone());
-    let contents = if let ExpressionKind::Variable(p) = &var.kind {
-        p.segments.first().cloned().unwrap_or_else(|| panic!("No segments")).0.contents
-    } else {
-        panic!("Unexpected identifier type")
-    };
-    let bytes_name = format!("{}_bytes", contents);
-    let var_bytes = assignment(&bytes_name, method_call(var, "as_bytes", vec![]));
-    let id = Ident::new(bytes_name, Span::default());
-
-    (var_bytes, id)
-}
-
-fn create_loop_over(var: Expression, loop_body: Vec<Statement>) -> Statement {
-    // If this is an array of primitive types (integers / fields) we can add them each to the hasher
-    // casted to a field
-    let span = var.span;
-
-    // `array.len()`
-    let end_range_expression = method_call(
-        var,    // variable
-        "len",  // method name
-        vec![], // args
-    );
-
-    // What will be looped over
-    // - `hasher.add({ident}[i] as Field)`
-    let for_loop_block = expression(ExpressionKind::Block(BlockExpression(loop_body)));
-
-    // `for i in 0..{ident}.len()`
-    make_statement(StatementKind::For(ForLoopStatement {
-        range: ForRange::Range(
-            expression(ExpressionKind::Literal(Literal::Integer(
-                FieldElement::from(i128::from(0)),
-                false,
-            ))),
-            end_range_expression,
-        ),
-        identifier: ident("i"),
-        block: for_loop_block,
-        span,
-    }))
-}
-
-fn add_array_to_hasher(identifier: &Ident, arr_type: &UnresolvedType) -> Statement {
-    // If this is an array of primitive types (integers / fields) we can add them each to the hasher
-    // casted to a field
-
-    // Wrap in the semi thing - does that mean ended with semi colon?
-    // `hasher.add({ident}[i] as Field)`
-
-    let arr_index = index_array(identifier.clone(), "i");
-    let (add_expression, hasher_method_name) = match arr_type.typ {
-        UnresolvedTypeData::Named(..) => {
-            let hasher_method_name = "add_multiple".to_owned();
-            let call = method_call(
-                // All serialize on each element
-                arr_index,   // variable
-                "serialize", // method name
-                vec![],      // args
-            );
-            (call, hasher_method_name)
-        }
-        _ => {
-            let hasher_method_name = "add".to_owned();
-            let call = cast(
-                arr_index,                        // lhs - `ident[i]`
-                UnresolvedTypeData::FieldElement, // cast to - `as Field`
-            );
-            (call, hasher_method_name)
-        }
-    };
-
-    let block_statement = make_statement(StatementKind::Semi(method_call(
-        variable("hasher"),  // variable
-        &hasher_method_name, // method name
-        vec![add_expression],
-    )));
-
-    create_loop_over(variable_ident(identifier.clone()), vec![block_statement])
-}
-
-fn add_field_to_hasher(identifier: &Ident) -> Statement {
-    // `hasher.add({ident})`
-    let ident = variable_path(path(identifier.clone()));
-    make_statement(StatementKind::Semi(method_call(
-        variable("hasher"), // variable
-        "add",              // method name
-        vec![ident],        // args
-    )))
-}
-
-fn add_cast_to_hasher(identifier: &Ident) -> Statement {
-    // `hasher.add({ident} as Field)`
-    // `{ident} as Field`
-    let cast_operation = cast(
-        variable_path(path(identifier.clone())), // lhs
-        UnresolvedTypeData::FieldElement,        // rhs
-    );
-
-    // `hasher.add({ident} as Field)`
-    make_statement(StatementKind::Semi(method_call(
-        variable("hasher"),   // variable
-        "add",                // method name
-        vec![cast_operation], // args
-    )))
-}
-
-/// Computes the aztec signature for a resolved type.
-fn signature_of_type(typ: &Type) -> String {
-    match typ {
-        Type::Integer(Signedness::Signed, bit_size) => format!("i{}", bit_size),
-        Type::Integer(Signedness::Unsigned, bit_size) => format!("u{}", bit_size),
-        Type::FieldElement => "Field".to_owned(),
-        Type::Bool => "bool".to_owned(),
-        Type::Array(len, typ) => {
-            if let Type::Constant(len) = **len {
-                format!("[{};{len}]", signature_of_type(typ))
-            } else {
-                unimplemented!("Cannot generate signature for array with length type {:?}", typ)
-            }
-        }
-        Type::Struct(def, args) => {
-            let fields = def.borrow().get_fields(args);
-            let fields = vecmap(fields, |(_, typ)| signature_of_type(&typ));
-            format!("({})", fields.join(","))
-        }
-        Type::Tuple(types) => {
-            let fields = vecmap(types, signature_of_type);
-            format!("({})", fields.join(","))
-        }
-        _ => unimplemented!("Cannot generate signature for type {:?}", typ),
-    }
-}
-
-/// Computes the signature for a resolved event type.
-/// It has the form 'EventName(Field,(Field),[u8;2])'
-fn event_signature(event: &StructType) -> String {
-    let fields = vecmap(event.get_fields(&[]), |(_, typ)| signature_of_type(&typ));
-    format!("{}({})", event.name.0.contents, fields.join(","))
-}
-
-fn inject_compute_note_hash_and_nullifier(
+/// Completes the Hir with data gathered from type resolution
+fn transform_hir(
     crate_id: &CrateId,
     context: &mut HirContext,
-    unresolved_traits_impls: &[UnresolvedTraitImpl],
-    collected_functions: &mut [UnresolvedFunctions],
-) -> Result<(), (MacroError, FileId)> {
-    // We first fetch modules in this crate which correspond to contracts, along with their file id.
-    let contract_module_file_ids: Vec<(LocalModuleId, FileId)> = context
-        .def_map(crate_id)
-        .expect("ICE: Missing crate in def_map")
-        .modules()
-        .iter()
-        .filter(|(_, module)| module.is_contract)
-        .map(|(idx, module)| (LocalModuleId(idx), module.location.file))
-        .collect();
-
-    // If the current crate does not contain a contract module we simply skip it.
-    if contract_module_file_ids.is_empty() {
-        return Ok(());
-    } else if contract_module_file_ids.len() != 1 {
-        panic!("Found multiple contracts in the same crate");
-    }
-
-    let (module_id, file_id) = contract_module_file_ids[0];
-
-    // If compute_note_hash_and_nullifier is already defined by the user, we skip auto-generation in order to provide an
-    // escape hatch for this mechanism.
-    // TODO(#4647): improve this diagnosis and error messaging.
-    if collected_functions.iter().any(|coll_funcs_data| {
-        check_for_compute_note_hash_and_nullifier_definition(&coll_funcs_data.functions, module_id)
-    }) {
-        return Ok(());
-    }
-
-    // In order to implement compute_note_hash_and_nullifier, we need to know all of the different note types the
-    // contract might use. These are the types that implement the NoteInterface trait, which provides the
-    // get_note_type_id function.
-    let note_types = fetch_struct_trait_impls(context, unresolved_traits_impls, "NoteInterface");
-
-    // We can now generate a version of compute_note_hash_and_nullifier tailored for the contract in this crate.
-    let func = generate_compute_note_hash_and_nullifier(&note_types);
-
-    // And inject the newly created function into the contract.
-
-    // TODO(#4373): We don't have a reasonable location for the source code of this autogenerated function, so we simply
-    // pass an empty span. This function should not produce errors anyway so this should not matter.
-    let location = Location::new(Span::empty(0), file_id);
-
-    // These are the same things the ModCollector does when collecting functions: we push the function to the
-    // NodeInterner, declare it in the module (which checks for duplicate definitions), and finally add it to the list
-    // on collected but unresolved functions.
-
-    let func_id = context.def_interner.push_empty_fn();
-    context.def_interner.push_function(
-        func_id,
-        &func.def,
-        ModuleId { krate: *crate_id, local_id: module_id },
-        location,
-    );
-
-    context.def_map_mut(crate_id).unwrap()
-        .modules_mut()[module_id.0]
-        .declare_function(
-            func.name_ident().clone(), func_id
-        ).expect(
-            "Failed to declare the autogenerated compute_note_hash_and_nullifier function, likely due to a duplicate definition. See https://github.com/AztecProtocol/aztec-packages/issues/4647."
-        );
-
-    collected_functions
-        .iter_mut()
-        .find(|fns| fns.file_id == file_id)
-        .expect("ICE: no functions found in contract file")
-        .push_fn(module_id, func_id, func.clone());
-
-    Ok(())
-}
-
-// Fetches the name of all structs that implement trait_name, both in the current crate and all of its dependencies.
-fn fetch_struct_trait_impls(
-    context: &mut HirContext,
-    unresolved_traits_impls: &[UnresolvedTraitImpl],
-    trait_name: &str,
-) -> Vec<String> {
-    let mut struct_typenames: Vec<String> = Vec::new();
-
-    // These structs can be declared in either external crates or the current one. External crates that contain
-    // dependencies have already been processed and resolved, but are available here via the NodeInterner. Note that
-    // crates on which the current crate does not depend on may not have been processed, and will be ignored.
-    for trait_impl_id in 0..context.def_interner.next_trait_impl_id().0 {
-        let trait_impl = &context.def_interner.get_trait_implementation(TraitImplId(trait_impl_id));
-
-        if trait_impl.borrow().ident.0.contents == *trait_name {
-            if let Type::Struct(s, _) = &trait_impl.borrow().typ {
-                struct_typenames.push(s.borrow().name.0.contents.clone());
-            } else {
-                panic!("Found impl for {} on non-Struct", trait_name);
-            }
-        }
-    }
-
-    // This crate's traits and impls have not yet been resolved, so we look for impls in unresolved_trait_impls.
-    struct_typenames.extend(
-        unresolved_traits_impls
-            .iter()
-            .filter(|trait_impl| {
-                trait_impl
-                    .trait_path
-                    .segments
-                    .last()
-                    .expect("ICE: empty trait_impl path")
-                    .0
-                    .contents
-                    == *trait_name
-            })
-            .filter_map(|trait_impl| match &trait_impl.object_type.typ {
-                UnresolvedTypeData::Named(path, _, _) => {
-                    Some(path.segments.last().unwrap().0.contents.clone())
-                }
-                _ => None,
-            }),
-    );
-
-    struct_typenames
-}
-
-fn generate_compute_note_hash_and_nullifier(note_types: &Vec<String>) -> NoirFunction {
-    let function_source = generate_compute_note_hash_and_nullifier_source(note_types);
-
-    let (function_ast, errors) = parse_program(&function_source);
-    if !errors.is_empty() {
-        dbg!(errors.clone());
-    }
-    assert_eq!(errors.len(), 0, "Failed to parse Noir macro code. This is either a bug in the compiler or the Noir macro code");
-
-    let mut function_ast = function_ast.into_sorted();
-    function_ast.functions.remove(0)
-}
-
-fn generate_compute_note_hash_and_nullifier_source(note_types: &Vec<String>) -> String {
-    // TODO(#4649): The serialized_note parameter is a fixed-size array, but we don't know what length it should have.
-    // For now we hardcode it to 20, which is the same as MAX_NOTE_FIELDS_LENGTH.
-
-    if note_types.is_empty() {
-        // TODO(#4520): Even if the contract does not include any notes, other parts of the stack expect for this
-        // function to exist, so we include a dummy version. We likely should error out here instead.
-        "
-        unconstrained fn compute_note_hash_and_nullifier(
-            contract_address: AztecAddress,
-            nonce: Field,
-            storage_slot: Field,
-            note_type_id: Field,
-            serialized_note: [Field; 20]
-        ) -> pub [Field; 4] {
-            [0, 0, 0, 0]
-        }"
-        .to_string()
-    } else {
-        // For contracts that include notes we do a simple if-else chain comparing note_type_id with the different
-        // get_note_type_id of each of the note types.
-
-        let if_statements: Vec<String> = note_types.iter().map(|note_type| format!(
-            "if (note_type_id == {0}::get_note_type_id()) {{
-                dep::aztec::note::utils::compute_note_hash_and_nullifier({0}::deserialize_content, note_header, serialized_note)
-            }}"
-        , note_type)).collect();
-
-        // TODO(#4520): error out on the else instead of returning a zero array
-        let full_if_statement = if_statements.join(" else ")
-            + "
-            else {
-                [0, 0, 0, 0]
-            }";
-
-        format!(
-            "
-            unconstrained fn compute_note_hash_and_nullifier(
-                contract_address: AztecAddress,
-                nonce: Field,
-                storage_slot: Field,
-                note_type_id: Field,
-                serialized_note: [Field; 20]
-            ) -> pub [Field; 4] {{
-                let note_header = dep::aztec::prelude::NoteHeader::new(contract_address, nonce, storage_slot);
-
-                {}
-            }}",
-            full_if_statement
-        )
-    }
+) -> Result<(), (AztecMacroError, FileId)> {
+    transform_events(crate_id, context)?;
+    assign_storage_slots(crate_id, context)
 }
diff --git a/aztec_macros/src/transforms/compute_note_hash_and_nullifier.rs b/aztec_macros/src/transforms/compute_note_hash_and_nullifier.rs
new file mode 100644
index 00000000000..4f8f3f19ab8
--- /dev/null
+++ b/aztec_macros/src/transforms/compute_note_hash_and_nullifier.rs
@@ -0,0 +1,195 @@
+use noirc_errors::{Location, Span};
+use noirc_frontend::{
+    graph::CrateId,
+    hir::{
+        def_collector::dc_crate::{UnresolvedFunctions, UnresolvedTraitImpl},
+        def_map::{LocalModuleId, ModuleId},
+    },
+    macros_api::{FileId, HirContext, MacroError},
+    node_interner::FuncId,
+    parse_program, FunctionReturnType, NoirFunction, UnresolvedTypeData,
+};
+
+use crate::utils::hir_utils::fetch_struct_trait_impls;
+
+// Check if "compute_note_hash_and_nullifier(AztecAddress,Field,Field,Field,[Field; N]) -> [Field; 4]" is defined
+fn check_for_compute_note_hash_and_nullifier_definition(
+    functions_data: &[(LocalModuleId, FuncId, NoirFunction)],
+    module_id: LocalModuleId,
+) -> bool {
+    functions_data.iter().filter(|func_data| func_data.0 == module_id).any(|func_data| {
+        func_data.2.def.name.0.contents == "compute_note_hash_and_nullifier"
+                && func_data.2.def.parameters.len() == 5
+                && match &func_data.2.def.parameters[0].typ.typ {
+                    UnresolvedTypeData::Named(path, _, _) => path.segments.last().unwrap().0.contents == "AztecAddress",
+                    _ => false,
+                }
+                && func_data.2.def.parameters[1].typ.typ == UnresolvedTypeData::FieldElement
+                && func_data.2.def.parameters[2].typ.typ == UnresolvedTypeData::FieldElement
+                && func_data.2.def.parameters[3].typ.typ == UnresolvedTypeData::FieldElement
+                // checks if the 5th parameter is an array and the Box<UnresolvedType> in
+                // Array(Option<UnresolvedTypeExpression>, Box<UnresolvedType>) contains only fields
+                && match &func_data.2.def.parameters[4].typ.typ {
+                    UnresolvedTypeData::Array(_, inner_type) => {
+                        matches!(inner_type.typ, UnresolvedTypeData::FieldElement)
+                    },
+                    _ => false,
+                }
+                // We check the return type the same way as we did the 5th parameter
+                && match &func_data.2.def.return_type {
+                    FunctionReturnType::Default(_) => false,
+                    FunctionReturnType::Ty(unresolved_type) => {
+                        match &unresolved_type.typ {
+                            UnresolvedTypeData::Array(_, inner_type) => {
+                                matches!(inner_type.typ, UnresolvedTypeData::FieldElement)
+                            },
+                            _ => false,
+                        }
+                    }
+                }
+    })
+}
+
+pub fn inject_compute_note_hash_and_nullifier(
+    crate_id: &CrateId,
+    context: &mut HirContext,
+    unresolved_traits_impls: &[UnresolvedTraitImpl],
+    collected_functions: &mut [UnresolvedFunctions],
+) -> Result<(), (MacroError, FileId)> {
+    // We first fetch modules in this crate which correspond to contracts, along with their file id.
+    let contract_module_file_ids: Vec<(LocalModuleId, FileId)> = context
+        .def_map(crate_id)
+        .expect("ICE: Missing crate in def_map")
+        .modules()
+        .iter()
+        .filter(|(_, module)| module.is_contract)
+        .map(|(idx, module)| (LocalModuleId(idx), module.location.file))
+        .collect();
+
+    // If the current crate does not contain a contract module we simply skip it.
+    if contract_module_file_ids.is_empty() {
+        return Ok(());
+    } else if contract_module_file_ids.len() != 1 {
+        panic!("Found multiple contracts in the same crate");
+    }
+
+    let (module_id, file_id) = contract_module_file_ids[0];
+
+    // If compute_note_hash_and_nullifier is already defined by the user, we skip auto-generation in order to provide an
+    // escape hatch for this mechanism.
+    // TODO(#4647): improve this diagnosis and error messaging.
+    if collected_functions.iter().any(|coll_funcs_data| {
+        check_for_compute_note_hash_and_nullifier_definition(&coll_funcs_data.functions, module_id)
+    }) {
+        return Ok(());
+    }
+
+    // In order to implement compute_note_hash_and_nullifier, we need to know all of the different note types the
+    // contract might use. These are the types that implement the NoteInterface trait, which provides the
+    // get_note_type_id function.
+    let note_types = fetch_struct_trait_impls(context, unresolved_traits_impls, "NoteInterface");
+
+    // We can now generate a version of compute_note_hash_and_nullifier tailored for the contract in this crate.
+    let func = generate_compute_note_hash_and_nullifier(&note_types);
+
+    // And inject the newly created function into the contract.
+
+    // TODO(#4373): We don't have a reasonable location for the source code of this autogenerated function, so we simply
+    // pass an empty span. This function should not produce errors anyway so this should not matter.
+    let location = Location::new(Span::empty(0), file_id);
+
+    // These are the same things the ModCollector does when collecting functions: we push the function to the
+    // NodeInterner, declare it in the module (which checks for duplicate definitions), and finally add it to the list
+    // on collected but unresolved functions.
+
+    let func_id = context.def_interner.push_empty_fn();
+    context.def_interner.push_function(
+        func_id,
+        &func.def,
+        ModuleId { krate: *crate_id, local_id: module_id },
+        location,
+    );
+
+    context.def_map_mut(crate_id).unwrap()
+        .modules_mut()[module_id.0]
+        .declare_function(
+            func.name_ident().clone(), func_id
+        ).expect(
+            "Failed to declare the autogenerated compute_note_hash_and_nullifier function, likely due to a duplicate definition. See https://github.com/AztecProtocol/aztec-packages/issues/4647."
+        );
+
+    collected_functions
+        .iter_mut()
+        .find(|fns| fns.file_id == file_id)
+        .expect("ICE: no functions found in contract file")
+        .push_fn(module_id, func_id, func.clone());
+
+    Ok(())
+}
+
+fn generate_compute_note_hash_and_nullifier(note_types: &Vec<String>) -> NoirFunction {
+    let function_source = generate_compute_note_hash_and_nullifier_source(note_types);
+
+    let (function_ast, errors) = parse_program(&function_source);
+    if !errors.is_empty() {
+        dbg!(errors.clone());
+    }
+    assert_eq!(errors.len(), 0, "Failed to parse Noir macro code. This is either a bug in the compiler or the Noir macro code");
+
+    let mut function_ast = function_ast.into_sorted();
+    function_ast.functions.remove(0)
+}
+
+fn generate_compute_note_hash_and_nullifier_source(note_types: &Vec<String>) -> String {
+    // TODO(#4649): The serialized_note parameter is a fixed-size array, but we don't know what length it should have.
+    // For now we hardcode it to 20, which is the same as MAX_NOTE_FIELDS_LENGTH.
+
+    if note_types.is_empty() {
+        // Even if the contract does not include any notes, other parts of the stack expect for this function to exist,
+        // so we include a dummy version.
+        "
+        unconstrained fn compute_note_hash_and_nullifier(
+            contract_address: AztecAddress,
+            nonce: Field,
+            storage_slot: Field,
+            note_type_id: Field,
+            serialized_note: [Field; 20]
+        ) -> pub [Field; 4] {
+            assert(false, \"This contract does not use private notes\");
+            [0, 0, 0, 0]
+        }"
+        .to_string()
+    } else {
+        // For contracts that include notes we do a simple if-else chain comparing note_type_id with the different
+        // get_note_type_id of each of the note types.
+
+        let if_statements: Vec<String> = note_types.iter().map(|note_type| format!(
+            "if (note_type_id == {0}::get_note_type_id()) {{
+                dep::aztec::note::utils::compute_note_hash_and_nullifier({0}::deserialize_content, note_header, serialized_note)
+            }}"
+        , note_type)).collect();
+
+        let full_if_statement = if_statements.join(" else ")
+            + "
+            else {
+                assert(false, \"Unknown note type ID\");
+                [0, 0, 0, 0]
+            }";
+
+        format!(
+            "
+            unconstrained fn compute_note_hash_and_nullifier(
+                contract_address: AztecAddress,
+                nonce: Field,
+                storage_slot: Field,
+                note_type_id: Field,
+                serialized_note: [Field; 20]
+            ) -> pub [Field; 4] {{
+                let note_header = dep::aztec::prelude::NoteHeader::new(contract_address, nonce, storage_slot);
+
+                {}
+            }}",
+            full_if_statement
+        )
+    }
+}
diff --git a/aztec_macros/src/transforms/events.rs b/aztec_macros/src/transforms/events.rs
new file mode 100644
index 00000000000..b02709efacb
--- /dev/null
+++ b/aztec_macros/src/transforms/events.rs
@@ -0,0 +1,178 @@
+use iter_extended::vecmap;
+use noirc_errors::Span;
+use noirc_frontend::{
+    graph::CrateId,
+    macros_api::{
+        BlockExpression, FileId, HirContext, HirExpression, HirLiteral, HirStatement, NodeInterner,
+        NoirStruct, PathKind, StatementKind, StructId, StructType, Type, TypeImpl,
+        UnresolvedTypeData,
+    },
+    token::SecondaryAttribute,
+    ExpressionKind, FunctionDefinition, FunctionReturnType, ItemVisibility, Literal, NoirFunction,
+    Visibility,
+};
+
+use crate::{
+    chained_dep,
+    utils::{
+        ast_utils::{
+            call, expression, ident, ident_path, make_statement, make_type, path, variable_path,
+        },
+        constants::SIGNATURE_PLACEHOLDER,
+        errors::AztecMacroError,
+        hir_utils::{collect_crate_structs, signature_of_type},
+    },
+};
+
+/// Generates the impl for an event selector
+///
+/// Inserts the following code:
+/// ```noir
+/// impl SomeStruct {
+///    fn selector() -> FunctionSelector {
+///       aztec::protocol_types::abis::function_selector::FunctionSelector::from_signature("SIGNATURE_PLACEHOLDER")
+///    }
+/// }
+/// ```
+///
+/// This allows developers to emit events without having to write the signature of the event every time they emit it.
+/// The signature cannot be known at this point since types are not resolved yet, so we use a signature placeholder.
+/// It'll get resolved after by transforming the HIR.
+pub fn generate_selector_impl(structure: &NoirStruct) -> TypeImpl {
+    let struct_type =
+        make_type(UnresolvedTypeData::Named(path(structure.name.clone()), vec![], true));
+
+    let selector_path =
+        chained_dep!("aztec", "protocol_types", "abis", "function_selector", "FunctionSelector");
+    let mut from_signature_path = selector_path.clone();
+    from_signature_path.segments.push(ident("from_signature"));
+
+    let selector_fun_body = BlockExpression(vec![make_statement(StatementKind::Expression(call(
+        variable_path(from_signature_path),
+        vec![expression(ExpressionKind::Literal(Literal::Str(SIGNATURE_PLACEHOLDER.to_string())))],
+    )))]);
+
+    // Define `FunctionSelector` return type
+    let return_type =
+        FunctionReturnType::Ty(make_type(UnresolvedTypeData::Named(selector_path, vec![], true)));
+
+    let mut selector_fn_def = FunctionDefinition::normal(
+        &ident("selector"),
+        &vec![],
+        &[],
+        &selector_fun_body,
+        &[],
+        &return_type,
+    );
+
+    selector_fn_def.visibility = ItemVisibility::Public;
+
+    // Seems to be necessary on contract modules
+    selector_fn_def.return_visibility = Visibility::Public;
+
+    TypeImpl {
+        object_type: struct_type,
+        type_span: structure.span,
+        generics: vec![],
+        methods: vec![(NoirFunction::normal(selector_fn_def), Span::default())],
+    }
+}
+
+/// Computes the signature for a resolved event type.
+/// It has the form 'EventName(Field,(Field),[u8;2])'
+fn event_signature(event: &StructType) -> String {
+    let fields = vecmap(event.get_fields(&[]), |(_, typ)| signature_of_type(&typ));
+    format!("{}({})", event.name.0.contents, fields.join(","))
+}
+
+/// Substitutes the signature literal that was introduced in the selector method previously with the actual signature.
+fn transform_event(
+    struct_id: StructId,
+    interner: &mut NodeInterner,
+) -> Result<(), (AztecMacroError, FileId)> {
+    let struct_type = interner.get_struct(struct_id);
+    let selector_id = interner
+        .lookup_method(&Type::Struct(struct_type.clone(), vec![]), struct_id, "selector", false)
+        .ok_or_else(|| {
+            let error = AztecMacroError::EventError {
+                span: struct_type.borrow().location.span,
+                message: "Selector method not found".to_owned(),
+            };
+            (error, struct_type.borrow().location.file)
+        })?;
+    let selector_function = interner.function(&selector_id);
+
+    let compute_selector_statement = interner.statement(
+        selector_function.block(interner).statements().first().ok_or_else(|| {
+            let error = AztecMacroError::EventError {
+                span: struct_type.borrow().location.span,
+                message: "Compute selector statement not found".to_owned(),
+            };
+            (error, struct_type.borrow().location.file)
+        })?,
+    );
+
+    let compute_selector_expression = match compute_selector_statement {
+        HirStatement::Expression(expression_id) => match interner.expression(&expression_id) {
+            HirExpression::Call(hir_call_expression) => Some(hir_call_expression),
+            _ => None,
+        },
+        _ => None,
+    }
+    .ok_or_else(|| {
+        let error = AztecMacroError::EventError {
+            span: struct_type.borrow().location.span,
+            message: "Compute selector statement is not a call expression".to_owned(),
+        };
+        (error, struct_type.borrow().location.file)
+    })?;
+
+    let first_arg_id = compute_selector_expression.arguments.first().ok_or_else(|| {
+        let error = AztecMacroError::EventError {
+            span: struct_type.borrow().location.span,
+            message: "Compute selector statement is not a call expression".to_owned(),
+        };
+        (error, struct_type.borrow().location.file)
+    })?;
+
+    match interner.expression(first_arg_id) {
+        HirExpression::Literal(HirLiteral::Str(signature))
+            if signature == SIGNATURE_PLACEHOLDER =>
+        {
+            let selector_literal_id = *first_arg_id;
+
+            let structure = interner.get_struct(struct_id);
+            let signature = event_signature(&structure.borrow());
+            interner.update_expression(selector_literal_id, |expr| {
+                *expr = HirExpression::Literal(HirLiteral::Str(signature.clone()));
+            });
+
+            // Also update the type! It might have a different length now than the placeholder.
+            interner.push_expr_type(
+                selector_literal_id,
+                Type::String(Box::new(Type::Constant(signature.len() as u64))),
+            );
+            Ok(())
+        }
+        _ => Err((
+            AztecMacroError::EventError {
+                span: struct_type.borrow().location.span,
+                message: "Signature placeholder literal does not match".to_owned(),
+            },
+            struct_type.borrow().location.file,
+        )),
+    }
+}
+
+pub fn transform_events(
+    crate_id: &CrateId,
+    context: &mut HirContext,
+) -> Result<(), (AztecMacroError, FileId)> {
+    for struct_id in collect_crate_structs(crate_id, context) {
+        let attributes = context.def_interner.struct_attributes(&struct_id);
+        if attributes.iter().any(|attr| matches!(attr, SecondaryAttribute::Event)) {
+            transform_event(struct_id, &mut context.def_interner)?;
+        }
+    }
+    Ok(())
+}
diff --git a/aztec_macros/src/transforms/functions.rs b/aztec_macros/src/transforms/functions.rs
new file mode 100644
index 00000000000..8b3c7d2f53b
--- /dev/null
+++ b/aztec_macros/src/transforms/functions.rs
@@ -0,0 +1,707 @@
+use convert_case::{Case, Casing};
+use noirc_errors::Span;
+use noirc_frontend::{
+    macros_api::FieldElement, BlockExpression, ConstrainKind, ConstrainStatement, Distinctness,
+    Expression, ExpressionKind, ForLoopStatement, ForRange, FunctionReturnType, Ident, Literal,
+    NoirFunction, Param, PathKind, Pattern, Signedness, Statement, StatementKind, UnresolvedType,
+    UnresolvedTypeData, Visibility,
+};
+
+use crate::{
+    chained_dep, chained_path,
+    utils::{
+        ast_utils::{
+            assignment, call, cast, expression, ident, ident_path, index_array,
+            index_array_variable, make_eq, make_statement, make_type, member_access, method_call,
+            mutable_assignment, mutable_reference, path, return_type, variable, variable_ident,
+            variable_path,
+        },
+        errors::AztecMacroError,
+    },
+};
+
+// If it does, it will insert the following things:
+/// - A new Input that is provided for a kernel app circuit, named: {Public/Private}ContextInputs
+/// - Hashes all of the function input variables
+///     - This instantiates a helper function
+pub fn transform_function(
+    ty: &str,
+    func: &mut NoirFunction,
+    storage_defined: bool,
+    is_initializer: bool,
+    insert_init_check: bool,
+    is_internal: bool,
+) -> Result<(), AztecMacroError> {
+    let context_name = format!("{}Context", ty);
+    let inputs_name = format!("{}ContextInputs", ty);
+    let return_type_name = format!("{}CircuitPublicInputs", ty);
+
+    // Add check that msg sender equals this address and flag function as internal
+    if is_internal {
+        let is_internal_check = create_internal_check(func.name());
+        func.def.body.0.insert(0, is_internal_check);
+    }
+
+    // Add initialization check
+    if insert_init_check {
+        let init_check = create_init_check();
+        func.def.body.0.insert(0, init_check);
+    }
+
+    // Add access to the storage struct
+    if storage_defined {
+        let storage_def = abstract_storage(&ty.to_lowercase(), false);
+        func.def.body.0.insert(0, storage_def);
+    }
+
+    // Insert the context creation as the first action
+    let create_context = create_context(&context_name, &func.def.parameters)?;
+    func.def.body.0.splice(0..0, (create_context).iter().cloned());
+
+    // Add the inputs to the params
+    let input = create_inputs(&inputs_name);
+    func.def.parameters.insert(0, input);
+
+    // Abstract return types such that they get added to the kernel's return_values
+    if let Some(return_values) = abstract_return_values(func) {
+        // In case we are pushing return values to the context, we remove the statement that originated it
+        // This avoids running duplicate code, since blocks like if/else can be value returning statements
+        func.def.body.0.pop();
+        // Add the new return statement
+        func.def.body.0.push(return_values);
+    }
+
+    // Before returning mark the contract as initialized
+    if is_initializer {
+        let mark_initialized = create_mark_as_initialized();
+        func.def.body.0.push(mark_initialized);
+    }
+
+    // Push the finish method call to the end of the function
+    let finish_def = create_context_finish();
+    func.def.body.0.push(finish_def);
+
+    let return_type = create_return_type(&return_type_name);
+    func.def.return_type = return_type;
+    func.def.return_visibility = Visibility::Public;
+
+    // Distinct return types are only required for private functions
+    // Public functions should have unconstrained auto-inferred
+    match ty {
+        "Private" => func.def.return_distinctness = Distinctness::Distinct,
+        "Public" => func.def.is_unconstrained = true,
+        _ => (),
+    }
+
+    Ok(())
+}
+
+/// Transform a function to work with AVM bytecode
+pub fn transform_vm_function(
+    func: &mut NoirFunction,
+    storage_defined: bool,
+) -> Result<(), AztecMacroError> {
+    // Create access to storage
+    if storage_defined {
+        let storage = abstract_storage("public_vm", true);
+        func.def.body.0.insert(0, storage);
+    }
+
+    // Push Avm context creation to the beginning of the function
+    let create_context = create_avm_context()?;
+    func.def.body.0.insert(0, create_context);
+
+    // We want the function to be seen as a public function
+    func.def.is_unconstrained = true;
+
+    // NOTE: the line below is a temporary hack to trigger external transpilation tools
+    // It will be removed once the transpiler is integrated into the Noir compiler
+    func.def.name.0.contents = format!("avm_{}", func.def.name.0.contents);
+    Ok(())
+}
+
+/// Transform Unconstrained
+///
+/// Inserts the following code at the beginning of an unconstrained function
+/// ```noir
+/// let storage = Storage::init(Context::none());
+/// ```
+///
+/// This will allow developers to access their contract' storage struct in unconstrained functions
+pub fn transform_unconstrained(func: &mut NoirFunction) {
+    func.def.body.0.insert(0, abstract_storage("Unconstrained", true));
+}
+
+/// Helper function that returns what the private context would look like in the ast
+/// This should make it available to be consumed within aztec private annotated functions.
+///
+/// The replaced code:
+/// ```noir
+/// /// Before
+/// fn foo(inputs: PrivateContextInputs) {
+///    // ...
+/// }
+///
+/// /// After
+/// #[aztec(private)]
+/// fn foo() {
+///   // ...
+/// }
+fn create_inputs(ty: &str) -> Param {
+    let context_ident = ident("inputs");
+    let context_pattern = Pattern::Identifier(context_ident);
+
+    let path_snippet = ty.to_case(Case::Snake); // e.g. private_context_inputs
+    let type_path = chained_dep!("aztec", "context", "inputs", &path_snippet, ty);
+
+    let context_type = make_type(UnresolvedTypeData::Named(type_path, vec![], true));
+    let visibility = Visibility::Private;
+
+    Param { pattern: context_pattern, typ: context_type, visibility, span: Span::default() }
+}
+
+/// Creates an initialization check to ensure that the contract has been initialized, meant to
+/// be injected as the first statement of any function after the context has been created.
+///
+/// ```noir
+/// assert_is_initialized(&mut context);
+/// ```
+fn create_init_check() -> Statement {
+    make_statement(StatementKind::Expression(call(
+        variable_path(chained_dep!("aztec", "initializer", "assert_is_initialized")),
+        vec![mutable_reference("context")],
+    )))
+}
+
+/// Creates a call to mark_as_initialized which emits the initialization nullifier, meant to
+/// be injected as the last statement before returning in a constructor.
+///
+/// ```noir
+/// mark_as_initialized(&mut context);
+/// ```
+fn create_mark_as_initialized() -> Statement {
+    make_statement(StatementKind::Expression(call(
+        variable_path(chained_dep!("aztec", "initializer", "mark_as_initialized")),
+        vec![mutable_reference("context")],
+    )))
+}
+
+/// Creates a check for internal functions ensuring that the caller is self.
+///
+/// ```noir
+/// assert(context.msg_sender() == context.this_address(), "Function can only be called internally");
+/// ```
+fn create_internal_check(fname: &str) -> Statement {
+    make_statement(StatementKind::Constrain(ConstrainStatement(
+        make_eq(
+            method_call(variable("context"), "msg_sender", vec![]),
+            method_call(variable("context"), "this_address", vec![]),
+        ),
+        Some(expression(ExpressionKind::Literal(Literal::Str(format!(
+            "Function {} can only be called internally",
+            fname
+        ))))),
+        ConstrainKind::Assert,
+    )))
+}
+
+/// Creates the private context object to be accessed within the function, the parameters need to be extracted to be
+/// appended into the args hash object.
+///
+/// The replaced code:
+/// ```noir
+/// #[aztec(private)]
+/// fn foo(structInput: SomeStruct, arrayInput: [u8; 10], fieldInput: Field) -> Field {
+///     // Create the hasher object
+///     let mut hasher = Hasher::new();
+///
+///     // struct inputs call serialize on them to add an array of fields
+///     hasher.add_multiple(structInput.serialize());
+///
+///     // Array inputs are iterated over and each element is added to the hasher (as a field)
+///     for i in 0..arrayInput.len() {
+///         hasher.add(arrayInput[i] as Field);
+///     }
+///     // Field inputs are added to the hasher
+///     hasher.add({ident});
+///
+///     // Create the context
+///     // The inputs (injected by this `create_inputs`) and completed hash object are passed to the context
+///     let mut context = PrivateContext::new(inputs, hasher.hash());
+/// }
+/// ```
+fn create_context(ty: &str, params: &[Param]) -> Result<Vec<Statement>, AztecMacroError> {
+    let mut injected_expressions: Vec<Statement> = vec![];
+
+    // `let mut hasher = Hasher::new();`
+    let let_hasher = mutable_assignment(
+        "hasher", // Assigned to
+        call(
+            variable_path(chained_dep!("aztec", "hasher", "Hasher", "new")), // Path
+            vec![],                                                          // args
+        ),
+    );
+
+    // Completes: `let mut hasher = Hasher::new();`
+    injected_expressions.push(let_hasher);
+
+    // Iterate over each of the function parameters, adding to them to the hasher
+    for Param { pattern, typ, span, .. } in params {
+        match pattern {
+            Pattern::Identifier(identifier) => {
+                // Match the type to determine the padding to do
+                let unresolved_type = &typ.typ;
+                let expression = match unresolved_type {
+                    // `hasher.add_multiple({ident}.serialize())`
+                    UnresolvedTypeData::Named(..) => add_struct_to_hasher(identifier),
+                    UnresolvedTypeData::Array(_, arr_type) => {
+                        add_array_to_hasher(identifier, arr_type)
+                    }
+                    // `hasher.add({ident})`
+                    UnresolvedTypeData::FieldElement => add_field_to_hasher(identifier),
+                    // Add the integer to the hasher, casted to a field
+                    // `hasher.add({ident} as Field)`
+                    UnresolvedTypeData::Integer(..) | UnresolvedTypeData::Bool => {
+                        add_cast_to_hasher(identifier)
+                    }
+                    UnresolvedTypeData::String(..) => {
+                        let (var_bytes, id) = str_to_bytes(identifier);
+                        injected_expressions.push(var_bytes);
+                        add_array_to_hasher(
+                            &id,
+                            &UnresolvedType {
+                                typ: UnresolvedTypeData::Integer(
+                                    Signedness::Unsigned,
+                                    noirc_frontend::IntegerBitSize::ThirtyTwo,
+                                ),
+                                span: None,
+                            },
+                        )
+                    }
+                    _ => {
+                        return Err(AztecMacroError::UnsupportedFunctionArgumentType {
+                            typ: unresolved_type.clone(),
+                            span: *span,
+                        })
+                    }
+                };
+                injected_expressions.push(expression);
+            }
+            _ => todo!(), // Maybe unreachable?
+        }
+    }
+
+    // Create the inputs to the context
+    let inputs_expression = variable("inputs");
+    // `hasher.hash()`
+    let hash_call = method_call(
+        variable("hasher"), // variable
+        "hash",             // method name
+        vec![],             // args
+    );
+
+    let path_snippet = ty.to_case(Case::Snake); // e.g. private_context
+
+    // let mut context = {ty}::new(inputs, hash);
+    let let_context = mutable_assignment(
+        "context", // Assigned to
+        call(
+            variable_path(chained_dep!("aztec", "context", &path_snippet, ty, "new")), // Path
+            vec![inputs_expression, hash_call],                                        // args
+        ),
+    );
+    injected_expressions.push(let_context);
+
+    // Return all expressions that will be injected by the hasher
+    Ok(injected_expressions)
+}
+
+/// Creates an mutable avm context
+///
+/// ```noir
+/// /// Before
+/// #[aztec(public-vm)]
+/// fn foo() -> Field {
+///   let mut context = aztec::context::AVMContext::new();
+///   let timestamp = context.timestamp();
+///   // ...
+/// }
+///
+/// /// After
+/// #[aztec(private)]
+/// fn foo() -> Field {
+///     let mut timestamp = context.timestamp();
+///     // ...
+/// }
+fn create_avm_context() -> Result<Statement, AztecMacroError> {
+    let let_context = mutable_assignment(
+        "context", // Assigned to
+        call(
+            variable_path(chained_dep!("aztec", "context", "AVMContext", "new")), // Path
+            vec![],                                                               // args
+        ),
+    );
+
+    Ok(let_context)
+}
+
+/// Abstract Return Type
+///
+/// This function intercepts the function's current return type and replaces it with pushes
+/// To the kernel
+///
+/// The replaced code:
+/// ```noir
+/// /// Before
+/// #[aztec(private)]
+/// fn foo() -> protocol_types::abis::private_circuit_public_inputs::PrivateCircuitPublicInputs {
+///   // ...
+///   let my_return_value: Field = 10;
+///   context.return_values.push(my_return_value);
+/// }
+///
+/// /// After
+/// #[aztec(private)]
+/// fn foo() -> Field {
+///     // ...
+///    let my_return_value: Field = 10;
+///    my_return_value
+/// }
+/// ```
+/// Similarly; Structs will be pushed to the context, after serialize() is called on them.
+/// Arrays will be iterated over and each element will be pushed to the context.
+/// Any primitive type that can be cast will be casted to a field and pushed to the context.
+fn abstract_return_values(func: &NoirFunction) -> Option<Statement> {
+    let current_return_type = func.return_type().typ;
+    let last_statement = func.def.body.0.last()?;
+
+    // TODO: (length, type) => We can limit the size of the array returned to be limited by kernel size
+    // Doesn't need done until we have settled on a kernel size
+    // TODO: support tuples here and in inputs -> convert into an issue
+    // Check if the return type is an expression, if it is, we can handle it
+    match last_statement {
+        Statement { kind: StatementKind::Expression(expression), .. } => {
+            match current_return_type {
+                // Call serialize on structs, push the whole array, calling push_array
+                UnresolvedTypeData::Named(..) => Some(make_struct_return_type(expression.clone())),
+                UnresolvedTypeData::Array(..) => Some(make_array_return_type(expression.clone())),
+                // Cast these types to a field before pushing
+                UnresolvedTypeData::Bool | UnresolvedTypeData::Integer(..) => {
+                    Some(make_castable_return_type(expression.clone()))
+                }
+                UnresolvedTypeData::FieldElement => Some(make_return_push(expression.clone())),
+                _ => None,
+            }
+        }
+        _ => None,
+    }
+}
+
+/// Abstract storage
+///
+/// For private functions:
+/// ```noir
+/// #[aztec(private)]
+/// fn lol() {
+///     let storage = Storage::init(Context::private(context));
+/// }
+/// ```
+///
+/// For public functions:
+/// ```noir
+/// #[aztec(public)]
+/// fn lol() {
+///    let storage = Storage::init(Context::public(context));
+/// }
+/// ```
+///
+/// For unconstrained functions:
+/// ```noir
+/// unconstrained fn lol() {
+///   let storage = Storage::init(Context::none());
+/// }
+fn abstract_storage(typ: &str, unconstrained: bool) -> Statement {
+    let init_context_call = if unconstrained {
+        call(
+            variable_path(chained_dep!("aztec", "context", "Context", "none")), // Path
+            vec![],                                                             // args
+        )
+    } else {
+        call(
+            variable_path(chained_dep!("aztec", "context", "Context", typ)), // Path
+            vec![mutable_reference("context")],                              // args
+        )
+    };
+
+    assignment(
+        "storage", // Assigned to
+        call(
+            variable_path(chained_path!("Storage", "init")), // Path
+            vec![init_context_call],                         // args
+        ),
+    )
+}
+
+/// Context Return Values
+///
+/// Creates an instance to the context return values
+/// ```noir
+/// `context.return_values`
+/// ```
+fn context_return_values() -> Expression {
+    member_access("context", "return_values")
+}
+
+/// Make return Push
+///
+/// Translates to:
+/// `context.return_values.push({push_value})`
+fn make_return_push(push_value: Expression) -> Statement {
+    make_statement(StatementKind::Semi(method_call(
+        context_return_values(),
+        "push",
+        vec![push_value],
+    )))
+}
+
+/// Make Return push array
+///
+/// Translates to:
+/// `context.return_values.extend_from_array({push_value})`
+fn make_return_extend_from_array(push_value: Expression) -> Statement {
+    make_statement(StatementKind::Semi(method_call(
+        context_return_values(),
+        "extend_from_array",
+        vec![push_value],
+    )))
+}
+
+/// Make struct return type
+///
+/// Translates to:
+/// ```noir
+/// `context.return_values.extend_from_array({push_value}.serialize())`
+fn make_struct_return_type(expression: Expression) -> Statement {
+    let serialized_call = method_call(
+        expression,  // variable
+        "serialize", // method name
+        vec![],      // args
+    );
+    make_return_extend_from_array(serialized_call)
+}
+
+/// Make array return type
+///
+/// Translates to:
+/// ```noir
+/// for i in 0..{ident}.len() {
+///    context.return_values.push({ident}[i] as Field)
+/// }
+/// ```
+fn make_array_return_type(expression: Expression) -> Statement {
+    let inner_cast_expression =
+        cast(index_array_variable(expression.clone(), "i"), UnresolvedTypeData::FieldElement);
+    let assignment = make_statement(StatementKind::Semi(method_call(
+        context_return_values(), // variable
+        "push",                  // method name
+        vec![inner_cast_expression],
+    )));
+
+    create_loop_over(expression, vec![assignment])
+}
+
+/// Castable return type
+///
+/// Translates to:
+/// ```noir
+/// context.return_values.push({ident} as Field)
+/// ```
+fn make_castable_return_type(expression: Expression) -> Statement {
+    // Cast these types to a field before pushing
+    let cast_expression = cast(expression, UnresolvedTypeData::FieldElement);
+    make_return_push(cast_expression)
+}
+
+/// Create Return Type
+///
+/// Public functions return protocol_types::abis::public_circuit_public_inputs::PublicCircuitPublicInputs while
+/// private functions return protocol_types::abis::private_circuit_public_inputs::::PrivateCircuitPublicInputs
+///
+/// This call constructs an ast token referencing the above types
+/// The name is set in the function above `transform`, hence the
+/// whole token name is passed in
+///
+/// The replaced code:
+/// ```noir
+///
+/// /// Before
+/// fn foo() -> protocol_types::abis::private_circuit_public_inputs::PrivateCircuitPublicInputs {
+///    // ...
+/// }
+///
+/// /// After
+/// #[aztec(private)]
+/// fn foo() {
+///  // ...
+/// }
+fn create_return_type(ty: &str) -> FunctionReturnType {
+    let path_snippet = ty.to_case(Case::Snake); // e.g. private_circuit_public_inputs or public_circuit_public_inputs
+    let return_path = chained_dep!("aztec", "protocol_types", "abis", &path_snippet, ty);
+    return_type(return_path)
+}
+
+/// Create Context Finish
+///
+/// Each aztec function calls `context.finish()` at the end of a function
+/// to return values required by the kernel.
+///
+/// The replaced code:
+/// ```noir
+/// /// Before
+/// fn foo() -> protocol_types::abis::private_circuit_public_inputs::PrivateCircuitPublicInputs {
+///   // ...
+///  context.finish()
+/// }
+///
+/// /// After
+/// #[aztec(private)]
+/// fn foo() {
+///  // ...
+/// }
+fn create_context_finish() -> Statement {
+    let method_call = method_call(
+        variable("context"), // variable
+        "finish",            // method name
+        vec![],              // args
+    );
+    make_statement(StatementKind::Expression(method_call))
+}
+
+//
+//                 Methods to create hasher inputs
+//
+
+fn add_struct_to_hasher(identifier: &Ident) -> Statement {
+    // If this is a struct, we call serialize and add the array to the hasher
+    let serialized_call = method_call(
+        variable_path(path(identifier.clone())), // variable
+        "serialize",                             // method name
+        vec![],                                  // args
+    );
+
+    make_statement(StatementKind::Semi(method_call(
+        variable("hasher"),    // variable
+        "add_multiple",        // method name
+        vec![serialized_call], // args
+    )))
+}
+
+fn str_to_bytes(identifier: &Ident) -> (Statement, Ident) {
+    // let identifier_as_bytes = identifier.as_bytes();
+    let var = variable_ident(identifier.clone());
+    let contents = if let ExpressionKind::Variable(p) = &var.kind {
+        p.segments.first().cloned().unwrap_or_else(|| panic!("No segments")).0.contents
+    } else {
+        panic!("Unexpected identifier type")
+    };
+    let bytes_name = format!("{}_bytes", contents);
+    let var_bytes = assignment(&bytes_name, method_call(var, "as_bytes", vec![]));
+    let id = Ident::new(bytes_name, Span::default());
+
+    (var_bytes, id)
+}
+
+fn create_loop_over(var: Expression, loop_body: Vec<Statement>) -> Statement {
+    // If this is an array of primitive types (integers / fields) we can add them each to the hasher
+    // casted to a field
+    let span = var.span;
+
+    // `array.len()`
+    let end_range_expression = method_call(
+        var,    // variable
+        "len",  // method name
+        vec![], // args
+    );
+
+    // What will be looped over
+    // - `hasher.add({ident}[i] as Field)`
+    let for_loop_block = expression(ExpressionKind::Block(BlockExpression(loop_body)));
+
+    // `for i in 0..{ident}.len()`
+    make_statement(StatementKind::For(ForLoopStatement {
+        range: ForRange::Range(
+            expression(ExpressionKind::Literal(Literal::Integer(
+                FieldElement::from(i128::from(0)),
+                false,
+            ))),
+            end_range_expression,
+        ),
+        identifier: ident("i"),
+        block: for_loop_block,
+        span,
+    }))
+}
+
+fn add_array_to_hasher(identifier: &Ident, arr_type: &UnresolvedType) -> Statement {
+    // If this is an array of primitive types (integers / fields) we can add them each to the hasher
+    // casted to a field
+
+    // Wrap in the semi thing - does that mean ended with semi colon?
+    // `hasher.add({ident}[i] as Field)`
+
+    let arr_index = index_array(identifier.clone(), "i");
+    let (add_expression, hasher_method_name) = match arr_type.typ {
+        UnresolvedTypeData::Named(..) => {
+            let hasher_method_name = "add_multiple".to_owned();
+            let call = method_call(
+                // All serialize on each element
+                arr_index,   // variable
+                "serialize", // method name
+                vec![],      // args
+            );
+            (call, hasher_method_name)
+        }
+        _ => {
+            let hasher_method_name = "add".to_owned();
+            let call = cast(
+                arr_index,                        // lhs - `ident[i]`
+                UnresolvedTypeData::FieldElement, // cast to - `as Field`
+            );
+            (call, hasher_method_name)
+        }
+    };
+
+    let block_statement = make_statement(StatementKind::Semi(method_call(
+        variable("hasher"),  // variable
+        &hasher_method_name, // method name
+        vec![add_expression],
+    )));
+
+    create_loop_over(variable_ident(identifier.clone()), vec![block_statement])
+}
+
+fn add_field_to_hasher(identifier: &Ident) -> Statement {
+    // `hasher.add({ident})`
+    let ident = variable_path(path(identifier.clone()));
+    make_statement(StatementKind::Semi(method_call(
+        variable("hasher"), // variable
+        "add",              // method name
+        vec![ident],        // args
+    )))
+}
+
+fn add_cast_to_hasher(identifier: &Ident) -> Statement {
+    // `hasher.add({ident} as Field)`
+    // `{ident} as Field`
+    let cast_operation = cast(
+        variable_path(path(identifier.clone())), // lhs
+        UnresolvedTypeData::FieldElement,        // rhs
+    );
+
+    // `hasher.add({ident} as Field)`
+    make_statement(StatementKind::Semi(method_call(
+        variable("hasher"),   // variable
+        "add",                // method name
+        vec![cast_operation], // args
+    )))
+}
diff --git a/aztec_macros/src/transforms/mod.rs b/aztec_macros/src/transforms/mod.rs
new file mode 100644
index 00000000000..144ffc3efc3
--- /dev/null
+++ b/aztec_macros/src/transforms/mod.rs
@@ -0,0 +1,4 @@
+pub mod compute_note_hash_and_nullifier;
+pub mod events;
+pub mod functions;
+pub mod storage;
diff --git a/aztec_macros/src/transforms/storage.rs b/aztec_macros/src/transforms/storage.rs
new file mode 100644
index 00000000000..40a094f78e3
--- /dev/null
+++ b/aztec_macros/src/transforms/storage.rs
@@ -0,0 +1,346 @@
+use std::borrow::{Borrow, BorrowMut};
+
+use noirc_errors::Span;
+use noirc_frontend::{
+    graph::CrateId,
+    macros_api::{
+        FieldElement, FileId, HirContext, HirExpression, HirLiteral, HirStatement, NodeInterner,
+    },
+    node_interner::{TraitId, TraitImplKind},
+    parser::SortedModule,
+    BlockExpression, Expression, ExpressionKind, FunctionDefinition, Ident, Literal, NoirFunction,
+    PathKind, Pattern, StatementKind, Type, TypeImpl, UnresolvedType, UnresolvedTypeData,
+};
+
+use crate::{
+    chained_dep, chained_path,
+    utils::{
+        ast_utils::{
+            call, expression, ident, ident_path, lambda, make_statement, make_type, pattern,
+            return_type, variable, variable_path,
+        },
+        errors::AztecMacroError,
+        hir_utils::{collect_crate_structs, collect_traits},
+    },
+};
+
+// Check to see if the user has defined a storage struct
+pub fn check_for_storage_definition(module: &SortedModule) -> bool {
+    module.types.iter().any(|r#struct| r#struct.name.0.contents == "Storage")
+}
+
+// Check to see if the user has defined a storage struct
+pub fn check_for_storage_implementation(module: &SortedModule) -> bool {
+    module.impls.iter().any(|r#impl| match &r#impl.object_type.typ {
+        UnresolvedTypeData::Named(path, _, _) => {
+            path.segments.last().is_some_and(|segment| segment.0.contents == "Storage")
+        }
+        _ => false,
+    })
+}
+
+/// Auxiliary function to generate the storage constructor for a given field, using
+/// the Storage definition as a reference. Supports nesting.
+pub fn generate_storage_field_constructor(
+    (type_ident, unresolved_type): &(Ident, UnresolvedType),
+    slot: Expression,
+) -> Result<Expression, AztecMacroError> {
+    let typ = &unresolved_type.typ;
+    match typ {
+        UnresolvedTypeData::Named(path, generics, _) => {
+            let mut new_path = path.clone().to_owned();
+            new_path.segments.push(ident("new"));
+            match path.segments.last().unwrap().0.contents.as_str() {
+                "Map" => Ok(call(
+                    variable_path(new_path),
+                    vec![
+                        variable("context"),
+                        slot,
+                        lambda(
+                            vec![
+                                (
+                                    pattern("context"),
+                                    make_type(UnresolvedTypeData::Named(
+                                        chained_dep!("aztec", "context", "Context"),
+                                        vec![],
+                                        true,
+                                    )),
+                                ),
+                                (
+                                    Pattern::Identifier(ident("slot")),
+                                    make_type(UnresolvedTypeData::FieldElement),
+                                ),
+                            ],
+                            generate_storage_field_constructor(
+                                &(type_ident.clone(), generics.iter().last().unwrap().clone()),
+                                variable("slot"),
+                            )?,
+                        ),
+                    ],
+                )),
+                _ => Ok(call(variable_path(new_path), vec![variable("context"), slot])),
+            }
+        }
+        _ => Err(AztecMacroError::UnsupportedStorageType {
+            typ: typ.clone(),
+            span: Some(type_ident.span()),
+        }),
+    }
+}
+
+// Generates the Storage implementation block from the Storage struct definition if it does not exist
+/// From:
+///
+/// struct Storage {
+///     a_map: Map<Field, SomeStoragePrimitive<ASerializableType>>,
+///     a_nested_map: Map<Field, Map<Field, SomeStoragePrimitive<ASerializableType>>>,
+///     a_field: SomeStoragePrimitive<ASerializableType>,
+/// }
+///
+/// To:
+///
+/// impl Storage {
+///    fn init(context: Context) -> Self {
+///        Storage {
+///             a_map: Map::new(context, 0, |context, slot| {
+///                 SomeStoragePrimitive::new(context, slot)
+///             }),
+///             a_nested_map: Map::new(context, 0, |context, slot| {
+///                 Map::new(context, slot, |context, slot| {
+///                     SomeStoragePrimitive::new(context, slot)
+///                })
+///            }),
+///            a_field: SomeStoragePrimitive::new(context, 0),
+///         }
+///    }
+/// }
+///
+/// Storage slots are generated as 0 and will be populated using the information from the HIR
+/// at a later stage.
+pub fn generate_storage_implementation(module: &mut SortedModule) -> Result<(), AztecMacroError> {
+    let definition =
+        module.types.iter().find(|r#struct| r#struct.name.0.contents == "Storage").unwrap();
+
+    let slot_zero = expression(ExpressionKind::Literal(Literal::Integer(
+        FieldElement::from(i128::from(0)),
+        false,
+    )));
+
+    let field_constructors = definition
+        .fields
+        .iter()
+        .flat_map(|field| {
+            generate_storage_field_constructor(field, slot_zero.clone())
+                .map(|expression| (field.0.clone(), expression))
+        })
+        .collect();
+
+    let storage_constructor_statement = make_statement(StatementKind::Expression(expression(
+        ExpressionKind::constructor((chained_path!("Storage"), field_constructors)),
+    )));
+
+    let init = NoirFunction::normal(FunctionDefinition::normal(
+        &ident("init"),
+        &vec![],
+        &[(
+            ident("context"),
+            make_type(UnresolvedTypeData::Named(
+                chained_dep!("aztec", "context", "Context"),
+                vec![],
+                true,
+            )),
+        )],
+        &BlockExpression(vec![storage_constructor_statement]),
+        &[],
+        &return_type(chained_path!("Self")),
+    ));
+
+    let storage_impl = TypeImpl {
+        object_type: UnresolvedType {
+            typ: UnresolvedTypeData::Named(chained_path!("Storage"), vec![], true),
+            span: Some(Span::default()),
+        },
+        type_span: Span::default(),
+        generics: vec![],
+        methods: vec![(init, Span::default())],
+    };
+    module.impls.push(storage_impl);
+
+    Ok(())
+}
+
+/// Obtains the serialized length of a type that implements the Serialize trait.
+fn get_serialized_length(
+    traits: &[TraitId],
+    typ: &Type,
+    interner: &NodeInterner,
+) -> Result<u64, AztecMacroError> {
+    let (struct_name, maybe_stored_in_state) = match typ {
+        Type::Struct(struct_type, generics) => {
+            Ok((struct_type.borrow().name.0.contents.clone(), generics.first()))
+        }
+        _ => Err(AztecMacroError::CouldNotAssignStorageSlots {
+            secondary_message: Some("State storage variable must be a struct".to_string()),
+        }),
+    }?;
+    let stored_in_state =
+        maybe_stored_in_state.ok_or(AztecMacroError::CouldNotAssignStorageSlots {
+            secondary_message: Some("State storage variable must be generic".to_string()),
+        })?;
+
+    let is_note = traits.iter().any(|&trait_id| {
+        let r#trait = interner.get_trait(trait_id);
+        r#trait.name.0.contents == "NoteInterface"
+            && !interner.lookup_all_trait_implementations(stored_in_state, trait_id).is_empty()
+    });
+
+    // Maps and (private) Notes always occupy a single slot. Someone could store a Note in PublicMutable for whatever reason though.
+    if struct_name == "Map" || (is_note && struct_name != "PublicMutable") {
+        return Ok(1);
+    }
+
+    let serialized_trait_impl_kind = traits
+        .iter()
+        .find_map(|&trait_id| {
+            let r#trait = interner.get_trait(trait_id);
+            if r#trait.borrow().name.0.contents == "Serialize"
+                && r#trait.borrow().generics.len() == 1
+            {
+                interner
+                    .lookup_all_trait_implementations(stored_in_state, trait_id)
+                    .into_iter()
+                    .next()
+            } else {
+                None
+            }
+        })
+        .ok_or(AztecMacroError::CouldNotAssignStorageSlots {
+            secondary_message: Some("Stored data must implement Serialize trait".to_string()),
+        })?;
+
+    let serialized_trait_impl_id = match serialized_trait_impl_kind {
+        TraitImplKind::Normal(trait_impl_id) => Ok(trait_impl_id),
+        _ => Err(AztecMacroError::CouldNotAssignStorageSlots { secondary_message: None }),
+    }?;
+
+    let serialized_trait_impl_shared = interner.get_trait_implementation(*serialized_trait_impl_id);
+    let serialized_trait_impl = serialized_trait_impl_shared.borrow();
+
+    match serialized_trait_impl.trait_generics.first().unwrap() {
+        Type::Constant(value) => Ok(*value),
+        _ => Err(AztecMacroError::CouldNotAssignStorageSlots { secondary_message: None }),
+    }
+}
+
+/// Assigns storage slots to the storage struct fields based on the serialized length of the types. This automatic assignment
+/// will only trigger if the assigned storage slot is invalid (0 as generated by generate_storage_implementation)
+pub fn assign_storage_slots(
+    crate_id: &CrateId,
+    context: &mut HirContext,
+) -> Result<(), (AztecMacroError, FileId)> {
+    let traits: Vec<_> = collect_traits(context);
+    for struct_id in collect_crate_structs(crate_id, context) {
+        let interner: &mut NodeInterner = context.def_interner.borrow_mut();
+        let r#struct = interner.get_struct(struct_id);
+        let file_id = r#struct.borrow().location.file;
+        if r#struct.borrow().name.0.contents == "Storage" && r#struct.borrow().id.krate().is_root()
+        {
+            let init_id = interner
+                .lookup_method(
+                    &Type::Struct(interner.get_struct(struct_id), vec![]),
+                    struct_id,
+                    "init",
+                    false,
+                )
+                .ok_or((
+                    AztecMacroError::CouldNotAssignStorageSlots {
+                        secondary_message: Some(
+                            "Storage struct must have an init function".to_string(),
+                        ),
+                    },
+                    file_id,
+                ))?;
+            let init_function = interner.function(&init_id).block(interner);
+            let init_function_statement_id = init_function.statements().first().ok_or((
+                AztecMacroError::CouldNotAssignStorageSlots {
+                    secondary_message: Some("Init storage statement not found".to_string()),
+                },
+                file_id,
+            ))?;
+            let storage_constructor_statement = interner.statement(init_function_statement_id);
+
+            let storage_constructor_expression = match storage_constructor_statement {
+                HirStatement::Expression(expression_id) => {
+                    match interner.expression(&expression_id) {
+                        HirExpression::Constructor(hir_constructor_expression) => {
+                            Ok(hir_constructor_expression)
+                        }
+                        _ => Err((AztecMacroError::CouldNotAssignStorageSlots {
+                            secondary_message: Some(
+                                "Storage constructor statement must be a constructor expression"
+                                    .to_string(),
+                            ),
+                        }, file_id))
+                    }
+                }
+                _ => Err((
+                    AztecMacroError::CouldNotAssignStorageSlots {
+                        secondary_message: Some(
+                            "Storage constructor statement must be an expression".to_string(),
+                        ),
+                    },
+                    file_id,
+                )),
+            }?;
+
+            let mut storage_slot: u64 = 1;
+            for (index, (_, expr_id)) in storage_constructor_expression.fields.iter().enumerate() {
+                let fields = r#struct.borrow().get_fields(&[]);
+                let (_, field_type) = fields.get(index).unwrap();
+                let new_call_expression = match interner.expression(expr_id) {
+                    HirExpression::Call(hir_call_expression) => Ok(hir_call_expression),
+                    _ => Err((
+                        AztecMacroError::CouldNotAssignStorageSlots {
+                            secondary_message: Some(
+                                "Storage field initialization expression is not a call expression"
+                                    .to_string(),
+                            ),
+                        },
+                        file_id,
+                    )),
+                }?;
+
+                let slot_arg_expression = interner.expression(&new_call_expression.arguments[1]);
+
+                let current_storage_slot = match slot_arg_expression {
+                    HirExpression::Literal(HirLiteral::Integer(slot, _)) => Ok(slot.to_u128()),
+                    _ => Err((
+                        AztecMacroError::CouldNotAssignStorageSlots {
+                            secondary_message: Some(
+                                "Storage slot argument expression must be a literal integer"
+                                    .to_string(),
+                            ),
+                        },
+                        file_id,
+                    )),
+                }?;
+
+                if current_storage_slot != 0 {
+                    continue;
+                }
+
+                let type_serialized_len = get_serialized_length(&traits, field_type, interner)
+                    .map_err(|err| (err, file_id))?;
+                interner.update_expression(new_call_expression.arguments[1], |expr| {
+                    *expr = HirExpression::Literal(HirLiteral::Integer(
+                        FieldElement::from(u128::from(storage_slot)),
+                        false,
+                    ));
+                });
+
+                storage_slot += type_serialized_len;
+            }
+        }
+    }
+    Ok(())
+}
diff --git a/aztec_macros/src/utils/ast_utils.rs b/aztec_macros/src/utils/ast_utils.rs
new file mode 100644
index 00000000000..71c6a93f388
--- /dev/null
+++ b/aztec_macros/src/utils/ast_utils.rs
@@ -0,0 +1,183 @@
+use noirc_errors::{Span, Spanned};
+use noirc_frontend::{
+    token::SecondaryAttribute, BinaryOpKind, CallExpression, CastExpression, Expression,
+    ExpressionKind, FunctionReturnType, Ident, IndexExpression, InfixExpression, Lambda,
+    LetStatement, MemberAccessExpression, MethodCallExpression, Path, Pattern, PrefixExpression,
+    Statement, StatementKind, UnaryOp, UnresolvedType, UnresolvedTypeData,
+};
+
+//
+//             Helper macros for creating noir ast nodes
+//
+pub fn ident(name: &str) -> Ident {
+    Ident::new(name.to_string(), Span::default())
+}
+
+pub fn ident_path(name: &str) -> Path {
+    Path::from_ident(ident(name))
+}
+
+pub fn path(ident: Ident) -> Path {
+    Path::from_ident(ident)
+}
+
+pub fn expression(kind: ExpressionKind) -> Expression {
+    Expression::new(kind, Span::default())
+}
+
+pub fn variable(name: &str) -> Expression {
+    expression(ExpressionKind::Variable(ident_path(name)))
+}
+
+pub fn variable_ident(identifier: Ident) -> Expression {
+    expression(ExpressionKind::Variable(path(identifier)))
+}
+
+pub fn variable_path(path: Path) -> Expression {
+    expression(ExpressionKind::Variable(path))
+}
+
+pub fn method_call(
+    object: Expression,
+    method_name: &str,
+    arguments: Vec<Expression>,
+) -> Expression {
+    expression(ExpressionKind::MethodCall(Box::new(MethodCallExpression {
+        object,
+        method_name: ident(method_name),
+        arguments,
+    })))
+}
+
+pub fn call(func: Expression, arguments: Vec<Expression>) -> Expression {
+    expression(ExpressionKind::Call(Box::new(CallExpression { func: Box::new(func), arguments })))
+}
+
+pub fn pattern(name: &str) -> Pattern {
+    Pattern::Identifier(ident(name))
+}
+
+pub fn mutable(name: &str) -> Pattern {
+    Pattern::Mutable(Box::new(pattern(name)), Span::default(), true)
+}
+
+pub fn mutable_assignment(name: &str, assigned_to: Expression) -> Statement {
+    make_statement(StatementKind::Let(LetStatement {
+        pattern: mutable(name),
+        r#type: make_type(UnresolvedTypeData::Unspecified),
+        expression: assigned_to,
+    }))
+}
+
+pub fn mutable_reference(variable_name: &str) -> Expression {
+    expression(ExpressionKind::Prefix(Box::new(PrefixExpression {
+        operator: UnaryOp::MutableReference,
+        rhs: variable(variable_name),
+    })))
+}
+
+pub fn assignment(name: &str, assigned_to: Expression) -> Statement {
+    make_statement(StatementKind::Let(LetStatement {
+        pattern: pattern(name),
+        r#type: make_type(UnresolvedTypeData::Unspecified),
+        expression: assigned_to,
+    }))
+}
+
+pub fn member_access(lhs: &str, rhs: &str) -> Expression {
+    expression(ExpressionKind::MemberAccess(Box::new(MemberAccessExpression {
+        lhs: variable(lhs),
+        rhs: ident(rhs),
+    })))
+}
+
+pub fn return_type(path: Path) -> FunctionReturnType {
+    let ty = make_type(UnresolvedTypeData::Named(path, vec![], true));
+    FunctionReturnType::Ty(ty)
+}
+
+pub fn lambda(parameters: Vec<(Pattern, UnresolvedType)>, body: Expression) -> Expression {
+    expression(ExpressionKind::Lambda(Box::new(Lambda {
+        parameters,
+        return_type: UnresolvedType {
+            typ: UnresolvedTypeData::Unspecified,
+            span: Some(Span::default()),
+        },
+        body,
+    })))
+}
+
+pub fn make_eq(lhs: Expression, rhs: Expression) -> Expression {
+    expression(ExpressionKind::Infix(Box::new(InfixExpression {
+        lhs,
+        rhs,
+        operator: Spanned::from(Span::default(), BinaryOpKind::Equal),
+    })))
+}
+
+pub fn make_statement(kind: StatementKind) -> Statement {
+    Statement { span: Span::default(), kind }
+}
+
+#[macro_export]
+macro_rules! chained_path {
+    ( $base:expr ) => {
+        {
+            ident_path($base)
+        }
+    };
+    ( $base:expr $(, $tail:expr)* ) => {
+        {
+            let mut base_path = ident_path($base);
+            $(
+                base_path.segments.push(ident($tail));
+            )*
+            base_path
+        }
+    }
+}
+
+#[macro_export]
+macro_rules! chained_dep {
+    ( $base:expr $(, $tail:expr)* ) => {
+        {
+            let mut base_path = ident_path($base);
+            base_path.kind = PathKind::Dep;
+            $(
+                base_path.segments.push(ident($tail));
+            )*
+            base_path
+        }
+    }
+}
+
+pub fn cast(lhs: Expression, ty: UnresolvedTypeData) -> Expression {
+    expression(ExpressionKind::Cast(Box::new(CastExpression { lhs, r#type: make_type(ty) })))
+}
+
+pub fn make_type(typ: UnresolvedTypeData) -> UnresolvedType {
+    UnresolvedType { typ, span: Some(Span::default()) }
+}
+
+pub fn index_array(array: Ident, index: &str) -> Expression {
+    expression(ExpressionKind::Index(Box::new(IndexExpression {
+        collection: variable_path(path(array)),
+        index: variable(index),
+    })))
+}
+
+pub fn index_array_variable(array: Expression, index: &str) -> Expression {
+    expression(ExpressionKind::Index(Box::new(IndexExpression {
+        collection: array,
+        index: variable(index),
+    })))
+}
+
+/// Checks if an attribute is a custom attribute with a specific name
+pub fn is_custom_attribute(attr: &SecondaryAttribute, attribute_name: &str) -> bool {
+    if let SecondaryAttribute::Custom(custom_attr) = attr {
+        custom_attr.as_str() == attribute_name
+    } else {
+        false
+    }
+}
diff --git a/aztec_macros/src/utils/checks.rs b/aztec_macros/src/utils/checks.rs
new file mode 100644
index 00000000000..5232f67ae87
--- /dev/null
+++ b/aztec_macros/src/utils/checks.rs
@@ -0,0 +1,22 @@
+use noirc_frontend::{
+    graph::CrateId,
+    macros_api::{FileId, HirContext, MacroError},
+};
+
+use super::errors::AztecMacroError;
+
+/// Creates an error alerting the user that they have not downloaded the Aztec-noir library
+pub fn check_for_aztec_dependency(
+    crate_id: &CrateId,
+    context: &HirContext,
+) -> Result<(), (MacroError, FileId)> {
+    if has_aztec_dependency(crate_id, context) {
+        Ok(())
+    } else {
+        Err((AztecMacroError::AztecDepNotFound.into(), context.crate_graph[crate_id].root_file_id))
+    }
+}
+
+pub fn has_aztec_dependency(crate_id: &CrateId, context: &HirContext) -> bool {
+    context.crate_graph[crate_id].dependencies.iter().any(|dep| dep.as_name() == "aztec")
+}
diff --git a/aztec_macros/src/utils/constants.rs b/aztec_macros/src/utils/constants.rs
new file mode 100644
index 00000000000..464cd10e2c7
--- /dev/null
+++ b/aztec_macros/src/utils/constants.rs
@@ -0,0 +1,3 @@
+pub const FUNCTION_TREE_HEIGHT: u32 = 5;
+pub const MAX_CONTRACT_PRIVATE_FUNCTIONS: usize = 2_usize.pow(FUNCTION_TREE_HEIGHT);
+pub const SIGNATURE_PLACEHOLDER: &str = "SIGNATURE_PLACEHOLDER";
diff --git a/aztec_macros/src/utils/errors.rs b/aztec_macros/src/utils/errors.rs
new file mode 100644
index 00000000000..63892b58af9
--- /dev/null
+++ b/aztec_macros/src/utils/errors.rs
@@ -0,0 +1,69 @@
+use noirc_errors::Span;
+use noirc_frontend::{macros_api::MacroError, UnresolvedTypeData};
+
+use super::constants::MAX_CONTRACT_PRIVATE_FUNCTIONS;
+
+#[derive(Debug, Clone)]
+pub enum AztecMacroError {
+    AztecDepNotFound,
+    ContractHasTooManyPrivateFunctions { span: Span },
+    ContractConstructorMissing { span: Span },
+    UnsupportedFunctionArgumentType { span: Span, typ: UnresolvedTypeData },
+    UnsupportedStorageType { span: Option<Span>, typ: UnresolvedTypeData },
+    CouldNotAssignStorageSlots { secondary_message: Option<String> },
+    CouldNotImplementNoteSerialization { span: Option<Span>, typ: UnresolvedTypeData },
+    EventError { span: Span, message: String },
+    UnsupportedAttributes { span: Span, secondary_message: Option<String> },
+}
+
+impl From<AztecMacroError> for MacroError {
+    fn from(err: AztecMacroError) -> Self {
+        match err {
+            AztecMacroError::AztecDepNotFound {} => MacroError {
+                primary_message: "Aztec dependency not found. Please add aztec as a dependency in your Cargo.toml. For more information go to https://docs.aztec.network/developers/debugging/aztecnr-errors#aztec-dependency-not-found-please-add-aztec-as-a-dependency-in-your-nargotoml".to_owned(),
+                secondary_message: None,
+                span: None,
+            },
+            AztecMacroError::ContractHasTooManyPrivateFunctions { span } => MacroError {
+                primary_message: format!("Contract can only have a maximum of {} private functions", MAX_CONTRACT_PRIVATE_FUNCTIONS),
+                secondary_message: None,
+                span: Some(span),
+            },
+            AztecMacroError::ContractConstructorMissing { span } => MacroError {
+                primary_message: "Contract must have a constructor function".to_owned(),
+                secondary_message: None,
+                span: Some(span),
+            },
+            AztecMacroError::UnsupportedFunctionArgumentType { span, typ } => MacroError {
+                primary_message: format!("Provided parameter type `{typ:?}` is not supported in Aztec contract interface"),
+                secondary_message: None,
+                span: Some(span),
+            },
+            AztecMacroError::UnsupportedStorageType { span, typ } => MacroError {
+                primary_message: format!("Provided storage type `{typ:?}` is not directly supported in Aztec. Please provide a custom storage implementation"),
+                secondary_message: None,
+                span,
+            },
+            AztecMacroError::CouldNotAssignStorageSlots { secondary_message } => MacroError {
+                primary_message: "Could not assign storage slots, please provide a custom storage implementation".to_string(),
+                secondary_message,
+                span: None,
+            },
+            AztecMacroError::CouldNotImplementNoteSerialization { span, typ } => MacroError {
+                primary_message: format!("Could not implement serialization methods for note `{typ:?}`, please provide a serialize_content and deserialize_content methods"),
+                secondary_message: None,
+                span,
+            },
+            AztecMacroError::EventError { span, message } => MacroError {
+                primary_message: message,
+                secondary_message: None,
+                span: Some(span),
+            },
+AztecMacroError::UnsupportedAttributes { span, secondary_message } => MacroError {
+                primary_message: "Unsupported attributes in contract function".to_string(),
+                secondary_message,
+                span: Some(span),
+            },
+        }
+    }
+}
diff --git a/aztec_macros/src/utils/hir_utils.rs b/aztec_macros/src/utils/hir_utils.rs
new file mode 100644
index 00000000000..f31a0584261
--- /dev/null
+++ b/aztec_macros/src/utils/hir_utils.rs
@@ -0,0 +1,118 @@
+use iter_extended::vecmap;
+use noirc_frontend::{
+    graph::CrateId,
+    hir::def_collector::dc_crate::UnresolvedTraitImpl,
+    macros_api::{HirContext, ModuleDefId, StructId},
+    node_interner::{TraitId, TraitImplId},
+    Signedness, Type, UnresolvedTypeData,
+};
+
+pub fn collect_crate_structs(crate_id: &CrateId, context: &HirContext) -> Vec<StructId> {
+    context
+        .def_map(crate_id)
+        .expect("ICE: Missing crate in def_map")
+        .modules()
+        .iter()
+        .flat_map(|(_, module)| {
+            module.type_definitions().filter_map(|typ| {
+                if let ModuleDefId::TypeId(struct_id) = typ {
+                    Some(struct_id)
+                } else {
+                    None
+                }
+            })
+        })
+        .collect()
+}
+
+pub fn collect_traits(context: &HirContext) -> Vec<TraitId> {
+    let crates = context.crates();
+    crates
+        .flat_map(|crate_id| context.def_map(&crate_id).map(|def_map| def_map.modules()))
+        .flatten()
+        .flat_map(|module| {
+            module.type_definitions().filter_map(|typ| {
+                if let ModuleDefId::TraitId(struct_id) = typ {
+                    Some(struct_id)
+                } else {
+                    None
+                }
+            })
+        })
+        .collect()
+}
+
+/// Computes the aztec signature for a resolved type.
+pub fn signature_of_type(typ: &Type) -> String {
+    match typ {
+        Type::Integer(Signedness::Signed, bit_size) => format!("i{}", bit_size),
+        Type::Integer(Signedness::Unsigned, bit_size) => format!("u{}", bit_size),
+        Type::FieldElement => "Field".to_owned(),
+        Type::Bool => "bool".to_owned(),
+        Type::Array(len, typ) => {
+            if let Type::Constant(len) = **len {
+                format!("[{};{len}]", signature_of_type(typ))
+            } else {
+                unimplemented!("Cannot generate signature for array with length type {:?}", typ)
+            }
+        }
+        Type::Struct(def, args) => {
+            let fields = def.borrow().get_fields(args);
+            let fields = vecmap(fields, |(_, typ)| signature_of_type(&typ));
+            format!("({})", fields.join(","))
+        }
+        Type::Tuple(types) => {
+            let fields = vecmap(types, signature_of_type);
+            format!("({})", fields.join(","))
+        }
+        _ => unimplemented!("Cannot generate signature for type {:?}", typ),
+    }
+}
+
+// Fetches the name of all structs that implement trait_name, both in the current crate and all of its dependencies.
+pub fn fetch_struct_trait_impls(
+    context: &mut HirContext,
+    unresolved_traits_impls: &[UnresolvedTraitImpl],
+    trait_name: &str,
+) -> Vec<String> {
+    let mut struct_typenames: Vec<String> = Vec::new();
+
+    // These structs can be declared in either external crates or the current one. External crates that contain
+    // dependencies have already been processed and resolved, but are available here via the NodeInterner. Note that
+    // crates on which the current crate does not depend on may not have been processed, and will be ignored.
+    for trait_impl_id in 0..context.def_interner.next_trait_impl_id().0 {
+        let trait_impl = &context.def_interner.get_trait_implementation(TraitImplId(trait_impl_id));
+
+        if trait_impl.borrow().ident.0.contents == *trait_name {
+            if let Type::Struct(s, _) = &trait_impl.borrow().typ {
+                struct_typenames.push(s.borrow().name.0.contents.clone());
+            } else {
+                panic!("Found impl for {} on non-Struct", trait_name);
+            }
+        }
+    }
+
+    // This crate's traits and impls have not yet been resolved, so we look for impls in unresolved_trait_impls.
+    struct_typenames.extend(
+        unresolved_traits_impls
+            .iter()
+            .filter(|trait_impl| {
+                trait_impl
+                    .trait_path
+                    .segments
+                    .last()
+                    .expect("ICE: empty trait_impl path")
+                    .0
+                    .contents
+                    == *trait_name
+            })
+            .filter_map(|trait_impl| match &trait_impl.object_type.typ {
+                UnresolvedTypeData::Named(path, _, _) => {
+                    Some(path.segments.last().unwrap().0.contents.clone())
+                }
+                _ => None,
+            }),
+    );
+
+    struct_typenames
+}
diff --git a/aztec_macros/src/utils/mod.rs b/aztec_macros/src/utils/mod.rs
new file mode 100644
index 00000000000..c8914f83025
--- /dev/null
+++ b/aztec_macros/src/utils/mod.rs
@@ -0,0 +1,5 @@
+pub mod ast_utils;
+pub mod checks;
+pub mod constants;
+pub mod errors;
+pub mod hir_utils;
diff --git a/compiler/noirc_driver/src/contract.rs b/compiler/noirc_driver/src/contract.rs
index 5f4b66e7dd2..66e8dc0e730 100644
--- a/compiler/noirc_driver/src/contract.rs
+++ b/compiler/noirc_driver/src/contract.rs
@@ -9,23 +9,6 @@ use noirc_evaluator::errors::SsaReport;
 
 use super::debug::DebugFile;
 
-/// Describes the types of smart contract functions that are allowed.
-/// Unlike the similar enum in noirc_frontend, 'open' and 'unconstrained'
-/// are mutually exclusive here. In the case a function is both, 'unconstrained'
-/// takes precedence.
-#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq)]
-pub enum ContractFunctionType {
-    /// This function will be executed in a private
-    /// context.
-    Secret,
-    /// This function will be executed in a public
-    /// context.
-    Open,
-    /// This function cannot constrain any values and can use nondeterministic features
-    /// like arrays of a dynamic size.
-    Unconstrained,
-}
-
 #[derive(Clone, Debug, Serialize, Deserialize)]
 pub struct CompiledContract {
     pub noir_version: String,
@@ -55,9 +38,9 @@ pub struct CompiledContract {
 pub struct ContractFunction {
     pub name: String,
 
-    pub function_type: ContractFunctionType,
+    pub is_unconstrained: bool,
 
-    pub is_internal: bool,
+    pub custom_attributes: Vec<String>,
 
     pub abi: Abi,
 
@@ -69,13 +52,3 @@ pub struct ContractFunction {
 
     pub debug: DebugInfo,
 }
-
-impl ContractFunctionType {
-    pub(super) fn new(kind: noirc_frontend::ContractFunctionType, is_unconstrained: bool) -> Self {
-        match (kind, is_unconstrained) {
-            (_, true) => Self::Unconstrained,
-            (noirc_frontend::ContractFunctionType::Secret, false) => Self::Secret,
-            (noirc_frontend::ContractFunctionType::Open, false) => Self::Open,
-        }
-    }
-}
diff --git a/compiler/noirc_driver/src/lib.rs b/compiler/noirc_driver/src/lib.rs
index fa134f8a0dd..c9494a64b41 100644
--- a/compiler/noirc_driver/src/lib.rs
+++ b/compiler/noirc_driver/src/lib.rs
@@ -18,6 +18,7 @@ use noirc_frontend::hir::Context;
 use noirc_frontend::macros_api::MacroProcessor;
 use noirc_frontend::monomorphization::{monomorphize, monomorphize_debug, MonomorphizationError};
 use noirc_frontend::node_interner::FuncId;
+use noirc_frontend::token::SecondaryAttribute;
 use std::path::Path;
 use thiserror::Error;
 use tracing::info;
@@ -30,7 +31,7 @@ mod stdlib;
 
 use debug::filter_relevant_files;
 
-pub use contract::{CompiledContract, ContractFunction, ContractFunctionType};
+pub use contract::{CompiledContract, ContractFunction};
 pub use debug::DebugFile;
 pub use program::CompiledProgram;
 
@@ -398,19 +399,24 @@ fn compile_contract_inner(
         };
         warnings.extend(function.warnings);
         let modifiers = context.def_interner.function_modifiers(&function_id);
-        let func_type = modifiers
-            .contract_function_type
-            .expect("Expected contract function to have a contract visibility");
 
-        let function_type = ContractFunctionType::new(func_type, modifiers.is_unconstrained);
+        let custom_attributes = modifiers
+            .attributes
+            .secondary
+            .iter()
+            .filter_map(
+                |attr| if let SecondaryAttribute::Custom(tag) = attr { Some(tag) } else { None },
+            )
+            .cloned()
+            .collect();
 
         functions.push(ContractFunction {
             name,
-            function_type,
-            is_internal: modifiers.is_internal.unwrap_or(false),
+            custom_attributes,
             abi: function.abi,
             bytecode: function.circuit,
             debug: function.debug,
+            is_unconstrained: modifiers.is_unconstrained,
         });
     }
 
diff --git a/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_block.rs b/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_block.rs
index 530b6dc69fc..911f4c1924e 100644
--- a/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_block.rs
+++ b/compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_block.rs
@@ -831,10 +831,15 @@ impl<'block> BrilligBlock<'block> {
             _ => unreachable!("ICE: array set on non-array"),
         };
 
+        // Here we want to compare the reference count against 1.
+        let one = self.brillig_context.make_usize_constant(1_usize.into());
         let condition = self.brillig_context.allocate_register();
-
-        self.brillig_context.usize_op(reference_count, condition, BinaryIntOp::Equals, 1_usize);
-
+        self.brillig_context.memory_op(
+            reference_count,
+            one.address,
+            condition,
+            BinaryIntOp::Equals,
+        );
         self.brillig_context.branch_instruction(condition, |ctx, cond| {
             if cond {
                 // Reference count is 1, we can mutate the array directly
diff --git a/compiler/noirc_evaluator/src/brillig/brillig_ir.rs b/compiler/noirc_evaluator/src/brillig/brillig_ir.rs
index 1e53713dfe0..2a96965171b 100644
--- a/compiler/noirc_evaluator/src/brillig/brillig_ir.rs
+++ b/compiler/noirc_evaluator/src/brillig/brillig_ir.rs
@@ -27,7 +27,6 @@ use acvm::{
     FieldElement,
 };
 use debug_show::DebugShow;
-use num_bigint::BigUint;
 
 /// The Brillig VM does not apply a limit to the memory address space,
 /// As a convention, we take use 64 bits. This means that we assume that
@@ -215,7 +214,6 @@ impl BrilligContext {
         // Computes array_ptr + index, ie array[index]
         let index_of_element_in_memory = self.allocate_register();
         self.memory_op(array_ptr, index.address, index_of_element_in_memory, BinaryIntOp::Add);
-
         self.load_instruction(result, index_of_element_in_memory);
         // Free up temporary register
         self.deallocate_register(index_of_element_in_memory);
@@ -232,7 +230,12 @@ impl BrilligContext {
         self.debug_show.array_set(array_ptr, index.address, value);
         // Computes array_ptr + index, ie array[index]
         let index_of_element_in_memory = self.allocate_register();
-        self.memory_op(array_ptr, index.address, index_of_element_in_memory, BinaryIntOp::Add);
+        self.binary_instruction(
+            SingleAddrVariable::new_usize(array_ptr),
+            index,
+            SingleAddrVariable::new_usize(index_of_element_in_memory),
+            BrilligBinaryOp::Integer(BinaryIntOp::Add),
+        );
 
         self.store_instruction(index_of_element_in_memory, value);
         // Free up temporary register
@@ -592,6 +595,7 @@ impl BrilligContext {
     /// Stores the value of `constant` in the `result` register
     pub(crate) fn const_instruction(&mut self, result: SingleAddrVariable, constant: Value) {
         self.debug_show.const_instruction(result.address, constant);
+
         self.push_opcode(BrilligOpcode::Const {
             destination: result.address,
             value: constant,
@@ -760,6 +764,8 @@ impl BrilligContext {
     /// Instead truncation instructions are emitted as to when a
     /// truncation should be done.
     /// For Brillig, all integer operations will overflow as its cheap.
+    /// We currently use cast to truncate: we cast to the required bit size
+    /// and back to the original bit size.
     pub(crate) fn truncate_instruction(
         &mut self,
         destination_of_truncated_value: SingleAddrVariable,
@@ -778,20 +784,12 @@ impl BrilligContext {
             value_to_truncate.bit_size
         );
 
-        let mask = BigUint::from(2_u32).pow(bit_size) - BigUint::from(1_u32);
-        let mask_constant = self.make_constant(
-            FieldElement::from_be_bytes_reduce(&mask.to_bytes_be()).into(),
-            value_to_truncate.bit_size,
-        );
-
-        self.binary_instruction(
-            value_to_truncate,
-            mask_constant,
-            destination_of_truncated_value,
-            BrilligBinaryOp::Integer(BinaryIntOp::And),
-        );
-
-        self.deallocate_single_addr(mask_constant);
+        // We cast back and forth to ensure that the value is truncated.
+        let intermediate_register =
+            SingleAddrVariable { address: self.allocate_register(), bit_size };
+        self.cast_instruction(intermediate_register, value_to_truncate);
+        self.cast_instruction(destination_of_truncated_value, intermediate_register);
+        self.deallocate_register(intermediate_register.address);
     }
 
     /// Emits a stop instruction
diff --git a/compiler/noirc_frontend/src/ast/expression.rs b/compiler/noirc_frontend/src/ast/expression.rs
index def1b082890..a9ecc1a53e5 100644
--- a/compiler/noirc_frontend/src/ast/expression.rs
+++ b/compiler/noirc_frontend/src/ast/expression.rs
@@ -369,11 +369,6 @@ pub struct FunctionDefinition {
     // and `secondary` attributes (ones that do not change the function kind)
     pub attributes: Attributes,
 
-    /// True if this function was defined with the 'open' keyword
-    pub is_open: bool,
-
-    pub is_internal: bool,
-
     /// True if this function was defined with the 'unconstrained' keyword
     pub is_unconstrained: bool,
 
@@ -406,18 +401,6 @@ pub enum FunctionReturnType {
     Ty(UnresolvedType),
 }
 
-/// Describes the types of smart contract functions that are allowed.
-/// - All Noir programs in the non-contract context can be seen as `Secret`.
-#[derive(Debug, Copy, Clone, PartialEq, Eq)]
-pub enum ContractFunctionType {
-    /// This function will be executed in a private
-    /// context.
-    Secret,
-    /// This function will be executed in a public
-    /// context.
-    Open,
-}
-
 #[derive(Debug, PartialEq, Eq, Clone)]
 pub enum ArrayLiteral {
     Standard(Vec<Expression>),
@@ -674,8 +657,6 @@ impl FunctionDefinition {
         FunctionDefinition {
             name: name.clone(),
             attributes: Attributes::empty(),
-            is_open: false,
-            is_internal: false,
             is_unconstrained: false,
             visibility: ItemVisibility::Private,
             generics: generics.clone(),
diff --git a/compiler/noirc_frontend/src/hir/def_collector/dc_crate.rs b/compiler/noirc_frontend/src/hir/def_collector/dc_crate.rs
index a5dfc738824..0c53bff4a54 100644
--- a/compiler/noirc_frontend/src/hir/def_collector/dc_crate.rs
+++ b/compiler/noirc_frontend/src/hir/def_collector/dc_crate.rs
@@ -246,6 +246,7 @@ impl DefCollector {
             crate_root,
             crate_id,
             context,
+            macro_processors,
         ));
 
         let submodules = vecmap(def_collector.def_map.modules().iter(), |(index, _)| index);
@@ -255,7 +256,7 @@ impl DefCollector {
         // TODO(#4653): generalize this function
         for macro_processor in macro_processors {
             macro_processor
-                .process_unresolved_traits_impls(
+                .process_collected_defs(
                     &crate_id,
                     context,
                     &def_collector.collected_traits_impls,
diff --git a/compiler/noirc_frontend/src/hir/def_collector/dc_mod.rs b/compiler/noirc_frontend/src/hir/def_collector/dc_mod.rs
index 6ac263d80be..ae99e61e534 100644
--- a/compiler/noirc_frontend/src/hir/def_collector/dc_mod.rs
+++ b/compiler/noirc_frontend/src/hir/def_collector/dc_mod.rs
@@ -7,6 +7,7 @@ use noirc_errors::Location;
 use crate::{
     graph::CrateId,
     hir::def_collector::dc_crate::{UnresolvedStruct, UnresolvedTrait},
+    macros_api::MacroProcessor,
     node_interner::{FunctionModifiers, TraitId, TypeAliasId},
     parser::{SortedModule, SortedSubModule},
     FunctionDefinition, Ident, LetStatement, ModuleDeclaration, NoirFunction, NoirStruct,
@@ -41,16 +42,28 @@ pub fn collect_defs(
     module_id: LocalModuleId,
     crate_id: CrateId,
     context: &mut Context,
+    macro_processors: &[&dyn MacroProcessor],
 ) -> Vec<(CompilationError, FileId)> {
     let mut collector = ModCollector { def_collector, file_id, module_id };
     let mut errors: Vec<(CompilationError, FileId)> = vec![];
 
     // First resolve the module declarations
     for decl in ast.module_decls {
-        errors.extend(collector.parse_module_declaration(context, &decl, crate_id));
+        errors.extend(collector.parse_module_declaration(
+            context,
+            &decl,
+            crate_id,
+            macro_processors,
+        ));
     }
 
-    errors.extend(collector.collect_submodules(context, crate_id, ast.submodules, file_id));
+    errors.extend(collector.collect_submodules(
+        context,
+        crate_id,
+        ast.submodules,
+        file_id,
+        macro_processors,
+    ));
 
     // Then add the imports to defCollector to resolve once all modules in the hierarchy have been resolved
     for import in ast.imports {
@@ -398,8 +411,6 @@ impl<'a> ModCollector<'a> {
                             // TODO(Maddiaa): Investigate trait implementations with attributes see: https://github.com/noir-lang/noir/issues/2629
                             attributes: crate::token::Attributes::empty(),
                             is_unconstrained: false,
-                            contract_function_type: None,
-                            is_internal: None,
                         };
 
                         let location = Location::new(name.span(), self.file_id);
@@ -494,6 +505,7 @@ impl<'a> ModCollector<'a> {
         crate_id: CrateId,
         submodules: Vec<SortedSubModule>,
         file_id: FileId,
+        macro_processors: &[&dyn MacroProcessor],
     ) -> Vec<(CompilationError, FileId)> {
         let mut errors: Vec<(CompilationError, FileId)> = vec![];
         for submodule in submodules {
@@ -506,6 +518,7 @@ impl<'a> ModCollector<'a> {
                         child,
                         crate_id,
                         context,
+                        macro_processors,
                     ));
                 }
                 Err(error) => {
@@ -524,6 +537,7 @@ impl<'a> ModCollector<'a> {
         context: &mut Context,
         mod_decl: &ModuleDeclaration,
         crate_id: CrateId,
+        macro_processors: &[&dyn MacroProcessor],
     ) -> Vec<(CompilationError, FileId)> {
         let mut errors: Vec<(CompilationError, FileId)> = vec![];
         let child_file_id =
@@ -559,7 +573,19 @@ impl<'a> ModCollector<'a> {
 
         // Parse the AST for the module we just found and then recursively look for it's defs
         let (ast, parsing_errors) = context.parsed_file_results(child_file_id);
-        let ast = ast.into_sorted();
+        let mut ast = ast.into_sorted();
+
+        for macro_processor in macro_processors {
+            match macro_processor.process_untyped_ast(ast.clone(), &crate_id, context) {
+                Ok(processed_ast) => {
+                    ast = processed_ast;
+                }
+                Err((error, file_id)) => {
+                    let def_error = DefCollectorErrorKind::MacroError(error);
+                    errors.push((def_error.into(), file_id));
+                }
+            }
+        }
 
         errors.extend(
             parsing_errors.iter().map(|e| (e.clone().into(), child_file_id)).collect::<Vec<_>>(),
@@ -575,6 +601,7 @@ impl<'a> ModCollector<'a> {
                     child_mod_id,
                     crate_id,
                     context,
+                    macro_processors,
                 ));
             }
             Err(error) => {
diff --git a/compiler/noirc_frontend/src/hir/resolution/errors.rs b/compiler/noirc_frontend/src/hir/resolution/errors.rs
index 1049599f079..30a1ba2ee34 100644
--- a/compiler/noirc_frontend/src/hir/resolution/errors.rs
+++ b/compiler/noirc_frontend/src/hir/resolution/errors.rs
@@ -64,14 +64,10 @@ pub enum ResolverError {
     IncorrectGenericCount { span: Span, item_name: String, actual: usize, expected: usize },
     #[error("{0}")]
     ParserError(Box<ParserError>),
-    #[error("Function is not defined in a contract yet sets its contract visibility")]
-    ContractFunctionTypeInNormalFunction { span: Span },
     #[error("Cannot create a mutable reference to {variable}, it was declared to be immutable")]
     MutableReferenceToImmutableVariable { variable: String, span: Span },
     #[error("Mutable references to array indices are unsupported")]
     MutableReferenceToArrayElement { span: Span },
-    #[error("Function is not defined in a contract yet sets is_internal")]
-    ContractFunctionInternalInNormalFunction { span: Span },
     #[error("Numeric constants should be printed without formatting braces")]
     NumericConstantInFormatString { name: String, span: Span },
     #[error("Closure environment must be a tuple or unit type")]
@@ -276,22 +272,12 @@ impl From<ResolverError> for Diagnostic {
                 )
             }
             ResolverError::ParserError(error) => (*error).into(),
-            ResolverError::ContractFunctionTypeInNormalFunction { span } => Diagnostic::simple_error(
-                "Only functions defined within contracts can set their contract function type".into(),
-                "Non-contract functions cannot be 'open'".into(),
-                span,
-            ),
             ResolverError::MutableReferenceToImmutableVariable { variable, span } => {
                 Diagnostic::simple_error(format!("Cannot mutably reference the immutable variable {variable}"), format!("{variable} is immutable"), span)
             },
             ResolverError::MutableReferenceToArrayElement { span } => {
                 Diagnostic::simple_error("Mutable references to array elements are currently unsupported".into(), "Try storing the element in a fresh variable first".into(), span)
             },
-            ResolverError::ContractFunctionInternalInNormalFunction { span } => Diagnostic::simple_error(
-                "Only functions defined within contracts can set their functions to be internal".into(),
-                "Non-contract functions cannot be 'internal'".into(),
-                span,
-            ),
             ResolverError::NumericConstantInFormatString { name, span } => Diagnostic::simple_error(
                 format!("cannot find `{name}` in this scope "),
                 "Numeric constants should be printed without formatting braces".to_string(),
diff --git a/compiler/noirc_frontend/src/hir/resolution/resolver.rs b/compiler/noirc_frontend/src/hir/resolution/resolver.rs
index 567cc20f789..00b1b443430 100644
--- a/compiler/noirc_frontend/src/hir/resolution/resolver.rs
+++ b/compiler/noirc_frontend/src/hir/resolution/resolver.rs
@@ -37,11 +37,11 @@ use crate::{
     StatementKind,
 };
 use crate::{
-    ArrayLiteral, ContractFunctionType, Distinctness, ForRange, FunctionDefinition,
-    FunctionReturnType, Generics, ItemVisibility, LValue, NoirStruct, NoirTypeAlias, Param, Path,
-    PathKind, Pattern, Shared, StructType, Type, TypeAlias, TypeVariable, TypeVariableKind,
-    UnaryOp, UnresolvedGenerics, UnresolvedTraitConstraint, UnresolvedType, UnresolvedTypeData,
-    UnresolvedTypeExpression, Visibility, ERROR_IDENT,
+    ArrayLiteral, Distinctness, ForRange, FunctionDefinition, FunctionReturnType, Generics,
+    ItemVisibility, LValue, NoirStruct, NoirTypeAlias, Param, Path, PathKind, Pattern, Shared,
+    StructType, Type, TypeAlias, TypeVariable, TypeVariableKind, UnaryOp, UnresolvedGenerics,
+    UnresolvedTraitConstraint, UnresolvedType, UnresolvedTypeData, UnresolvedTypeExpression,
+    Visibility, ERROR_IDENT,
 };
 use fm::FileId;
 use iter_extended::vecmap;
@@ -234,8 +234,6 @@ impl<'a> Resolver<'a> {
         let def = FunctionDefinition {
             name: name.clone(),
             attributes: Attributes::empty(),
-            is_open: false,
-            is_internal: false,
             is_unconstrained: false,
             visibility: ItemVisibility::Public, // Trait functions are always public
             generics: generics.clone(),
@@ -973,9 +971,6 @@ impl<'a> Resolver<'a> {
 
         self.interner.push_definition_type(name_ident.id, typ.clone());
 
-        self.handle_function_type(&func_id);
-        self.handle_is_function_internal(&func_id);
-
         let direct_generics = func.def.generics.iter();
         let direct_generics = direct_generics
             .filter_map(|generic| self.find_generic(&generic.0.contents))
@@ -1023,34 +1018,14 @@ impl<'a> Resolver<'a> {
     /// True if the `distinct` keyword is allowed on a function's return type
     fn distinct_allowed(&self, func: &NoirFunction) -> bool {
         if self.in_contract {
-            // "open" and "unconstrained" functions are compiled to brillig and thus duplication of
+            // "unconstrained" functions are compiled to brillig and thus duplication of
             // witness indices in their abis is not a concern.
-            !func.def.is_unconstrained && !func.def.is_open
+            !func.def.is_unconstrained
         } else {
             func.name() == MAIN_FUNCTION
         }
     }
 
-    fn handle_function_type(&mut self, function: &FuncId) {
-        let function_type = self.interner.function_modifiers(function).contract_function_type;
-
-        if !self.in_contract && function_type == Some(ContractFunctionType::Open) {
-            let span = self.interner.function_ident(function).span();
-            self.errors.push(ResolverError::ContractFunctionTypeInNormalFunction { span });
-            self.interner.function_modifiers_mut(function).contract_function_type = None;
-        }
-    }
-
-    fn handle_is_function_internal(&mut self, function: &FuncId) {
-        if !self.in_contract {
-            if self.interner.function_modifiers(function).is_internal == Some(true) {
-                let span = self.interner.function_ident(function).span();
-                self.push_err(ResolverError::ContractFunctionInternalInNormalFunction { span });
-            }
-            self.interner.function_modifiers_mut(function).is_internal = None;
-        }
-    }
-
     fn declare_numeric_generics(&mut self, params: &[Type], return_type: &Type) {
         if self.generics.is_empty() {
             return;
diff --git a/compiler/noirc_frontend/src/lexer/token.rs b/compiler/noirc_frontend/src/lexer/token.rs
index f096c220200..3dc9d05b15e 100644
--- a/compiler/noirc_frontend/src/lexer/token.rs
+++ b/compiler/noirc_frontend/src/lexer/token.rs
@@ -664,7 +664,6 @@ pub enum Keyword {
     Let,
     Mod,
     Mut,
-    Open,
     Pub,
     Return,
     ReturnData,
@@ -706,7 +705,6 @@ impl fmt::Display for Keyword {
             Keyword::Let => write!(f, "let"),
             Keyword::Mod => write!(f, "mod"),
             Keyword::Mut => write!(f, "mut"),
-            Keyword::Open => write!(f, "open"),
             Keyword::Pub => write!(f, "pub"),
             Keyword::Return => write!(f, "return"),
             Keyword::ReturnData => write!(f, "return_data"),
@@ -751,7 +749,6 @@ impl Keyword {
             "let" => Keyword::Let,
             "mod" => Keyword::Mod,
             "mut" => Keyword::Mut,
-            "open" => Keyword::Open,
             "pub" => Keyword::Pub,
             "return" => Keyword::Return,
             "return_data" => Keyword::ReturnData,
diff --git a/compiler/noirc_frontend/src/lib.rs b/compiler/noirc_frontend/src/lib.rs
index 800d66b0dfb..1871b594ae7 100644
--- a/compiler/noirc_frontend/src/lib.rs
+++ b/compiler/noirc_frontend/src/lib.rs
@@ -76,12 +76,12 @@ pub mod macros_api {
         ) -> Result<SortedModule, (MacroError, FileId)>;
 
         // TODO(#4653): generalize this function
-        fn process_unresolved_traits_impls(
+        fn process_collected_defs(
             &self,
             _crate_id: &CrateId,
             _context: &mut HirContext,
-            _unresolved_traits_impls: &[UnresolvedTraitImpl],
-            _collected_functions: &mut Vec<UnresolvedFunctions>,
+            _collected_trait_impls: &[UnresolvedTraitImpl],
+            _collected_functions: &mut [UnresolvedFunctions],
         ) -> Result<(), (MacroError, FileId)>;
 
         /// Function to manipulate the AST after type checking has been completed.
diff --git a/compiler/noirc_frontend/src/monomorphization/mod.rs b/compiler/noirc_frontend/src/monomorphization/mod.rs
index 9d11ecd54bc..4938d33aff9 100644
--- a/compiler/noirc_frontend/src/monomorphization/mod.rs
+++ b/compiler/noirc_frontend/src/monomorphization/mod.rs
@@ -28,8 +28,8 @@ use crate::{
     },
     node_interner::{self, DefinitionKind, NodeInterner, StmtId, TraitImplKind, TraitMethodId},
     token::FunctionAttribute,
-    ContractFunctionType, FunctionKind, IntegerBitSize, Signedness, Type, TypeBinding,
-    TypeBindings, TypeVariable, TypeVariableKind, UnaryOp, Visibility,
+    FunctionKind, IntegerBitSize, Signedness, Type, TypeBinding, TypeBindings, TypeVariable,
+    TypeVariableKind, UnaryOp, Visibility,
 };
 
 use self::ast::{Definition, FuncId, Function, LocalId, Program};
@@ -312,8 +312,7 @@ impl<'interner> Monomorphizer<'interner> {
             Type::TraitAsType(..) => &body_return_type,
             _ => meta.return_type(),
         });
-        let unconstrained = modifiers.is_unconstrained
-            || matches!(modifiers.contract_function_type, Some(ContractFunctionType::Open));
+        let unconstrained = modifiers.is_unconstrained;
 
         let parameters = self.parameters(&meta.parameters);
         let body = self.expr(body_expr_id)?;
diff --git a/compiler/noirc_frontend/src/node_interner.rs b/compiler/noirc_frontend/src/node_interner.rs
index dc632527898..b83d2008530 100644
--- a/compiler/noirc_frontend/src/node_interner.rs
+++ b/compiler/noirc_frontend/src/node_interner.rs
@@ -28,8 +28,8 @@ use crate::hir_def::{
 };
 use crate::token::{Attributes, SecondaryAttribute};
 use crate::{
-    BinaryOpKind, ContractFunctionType, FunctionDefinition, Generics, ItemVisibility, Shared,
-    TypeAlias, TypeBindings, TypeVariable, TypeVariableId, TypeVariableKind,
+    BinaryOpKind, FunctionDefinition, Generics, ItemVisibility, Shared, TypeAlias, TypeBindings,
+    TypeVariable, TypeVariableId, TypeVariableKind,
 };
 
 /// An arbitrary number to limit the recursion depth when searching for trait impls.
@@ -241,15 +241,6 @@ pub struct FunctionModifiers {
     pub attributes: Attributes,
 
     pub is_unconstrained: bool,
-
-    /// This function's type in its contract.
-    /// If this function is not in a contract, this is always 'Secret'.
-    pub contract_function_type: Option<ContractFunctionType>,
-
-    /// This function's contract visibility.
-    /// If this function is internal can only be called by itself.
-    /// Will be None if not in contract.
-    pub is_internal: Option<bool>,
 }
 
 impl FunctionModifiers {
@@ -262,8 +253,6 @@ impl FunctionModifiers {
             visibility: ItemVisibility::Public,
             attributes: Attributes::empty(),
             is_unconstrained: false,
-            is_internal: None,
-            contract_function_type: None,
         }
     }
 }
@@ -759,17 +748,11 @@ impl NodeInterner {
         module: ModuleId,
         location: Location,
     ) -> DefinitionId {
-        use ContractFunctionType::*;
-
-        // We're filling in contract_function_type and is_internal now, but these will be verified
-        // later during name resolution.
         let modifiers = FunctionModifiers {
             name: function.name.0.contents.clone(),
             visibility: function.visibility,
             attributes: function.attributes.clone(),
             is_unconstrained: function.is_unconstrained,
-            contract_function_type: Some(if function.is_open { Open } else { Secret }),
-            is_internal: Some(function.is_internal),
         };
         self.push_function_definition(id, modifiers, module, location)
     }
diff --git a/compiler/noirc_frontend/src/parser/parser/function.rs b/compiler/noirc_frontend/src/parser/parser/function.rs
index a2a4577a993..06e1a958eb1 100644
--- a/compiler/noirc_frontend/src/parser/parser/function.rs
+++ b/compiler/noirc_frontend/src/parser/parser/function.rs
@@ -36,9 +36,6 @@ pub(super) fn function_definition(allow_self: bool) -> impl NoirParser<NoirFunct
                 name,
                 attributes,
                 is_unconstrained: modifiers.0,
-                is_open: modifiers.2,
-                // Whether a function is internal or not is now set through `aztec_macros`
-                is_internal: false,
                 visibility: modifiers.1,
                 generics,
                 parameters,
@@ -67,17 +64,14 @@ fn visibility_modifier() -> impl NoirParser<ItemVisibility> {
     choice((is_pub_crate, is_pub, is_private))
 }
 
-/// function_modifiers: 'unconstrained'? (visibility)? 'open'?
+/// function_modifiers: 'unconstrained'? (visibility)?
 ///
-/// returns (is_unconstrained, visibility, is_open) for whether each keyword was present
-fn function_modifiers() -> impl NoirParser<(bool, ItemVisibility, bool)> {
+/// returns (is_unconstrained, visibility) for whether each keyword was present
+fn function_modifiers() -> impl NoirParser<(bool, ItemVisibility)> {
     keyword(Keyword::Unconstrained)
         .or_not()
         .then(visibility_modifier())
-        .then(keyword(Keyword::Open).or_not())
-        .map(|((unconstrained, visibility), open)| {
-            (unconstrained.is_some(), visibility, open.is_some())
-        })
+        .map(|(unconstrained, visibility)| (unconstrained.is_some(), visibility))
 }
 
 /// non_empty_ident_list: ident ',' non_empty_ident_list
diff --git a/compiler/noirc_frontend/src/parser/parser/traits.rs b/compiler/noirc_frontend/src/parser/parser/traits.rs
index df8d50178d5..1e2a6b4d65d 100644
--- a/compiler/noirc_frontend/src/parser/parser/traits.rs
+++ b/compiler/noirc_frontend/src/parser/parser/traits.rs
@@ -120,11 +120,7 @@ pub(super) fn trait_implementation() -> impl NoirParser<TopLevelStatement> {
 
 fn trait_implementation_body() -> impl NoirParser<Vec<TraitImplItem>> {
     let function = function::function_definition(true).validate(|mut f, span, emit| {
-        if f.def().is_internal
-            || f.def().is_unconstrained
-            || f.def().is_open
-            || f.def().visibility != ItemVisibility::Private
-        {
+        if f.def().is_unconstrained || f.def().visibility != ItemVisibility::Private {
             emit(ParserError::with_reason(ParserErrorReason::TraitImplFunctionModifiers, span));
         }
         // Trait impl functions are always public
diff --git a/compiler/wasm/src/types/noir_artifact.ts b/compiler/wasm/src/types/noir_artifact.ts
index 832a6ed9bf9..935c99043da 100644
--- a/compiler/wasm/src/types/noir_artifact.ts
+++ b/compiler/wasm/src/types/noir_artifact.ts
@@ -32,19 +32,16 @@ export interface EventAbi {
   fields: ABIVariable[];
 }
 
-/** The Noir function types. */
-export type NoirFunctionType = 'Open' | 'Secret' | 'Unconstrained';
-
 /**
  * The compilation result of an Noir function.
  */
 export interface NoirFunctionEntry {
   /** The name of the function. */
   name: string;
-  /** The type of the function. */
-  function_type: NoirFunctionType;
-  /** Whether the function is internal. */
-  is_internal: boolean;
+  /** Whether the function is unconstrained. */
+  is_unconstrained: boolean;
+  /** The custom attributes applied to the function. */
+  custom_attributes: string[];
   /** The ABI of the function. */
   abi: Abi;
   /** The bytecode of the function in base64. */
diff --git a/compiler/wasm/test/fixtures/deps/lib-c/src/lib.nr b/compiler/wasm/test/fixtures/deps/lib-c/src/lib.nr
index 5c0b5a621e0..144bcec0532 100644
--- a/compiler/wasm/test/fixtures/deps/lib-c/src/lib.nr
+++ b/compiler/wasm/test/fixtures/deps/lib-c/src/lib.nr
@@ -1 +1 @@
-mod module;
\ No newline at end of file
+mod module;
diff --git a/compiler/wasm/test/fixtures/deps/lib-c/src/module.nr b/compiler/wasm/test/fixtures/deps/lib-c/src/module.nr
index 2746c97edf0..f4ad3bff5c9 100644
--- a/compiler/wasm/test/fixtures/deps/lib-c/src/module.nr
+++ b/compiler/wasm/test/fixtures/deps/lib-c/src/module.nr
@@ -1 +1 @@
-mod foo;
\ No newline at end of file
+mod foo;
diff --git a/compiler/wasm/test/fixtures/deps/lib-c/src/module/foo.nr b/compiler/wasm/test/fixtures/deps/lib-c/src/module/foo.nr
index e0c82fb1960..0376cd4cb87 100644
--- a/compiler/wasm/test/fixtures/deps/lib-c/src/module/foo.nr
+++ b/compiler/wasm/test/fixtures/deps/lib-c/src/module/foo.nr
@@ -1,3 +1,3 @@
 pub fn bar(param: Field) -> Field {
-  dep::std::hash::pedersen_hash([param])
+    dep::std::hash::pedersen_hash([param])
 }
diff --git a/compiler/wasm/test/fixtures/noir-contract/src/main.nr b/compiler/wasm/test/fixtures/noir-contract/src/main.nr
index b980af369cf..fc1dc8a5a17 100644
--- a/compiler/wasm/test/fixtures/noir-contract/src/main.nr
+++ b/compiler/wasm/test/fixtures/noir-contract/src/main.nr
@@ -5,8 +5,7 @@ contract TestContract {
         [foo::bar(param), param + pub_param]
     }
 
-    open fn openFunction() -> pub Field {
+    fn someFunction() -> pub Field {
         42
     }
-
 }
diff --git a/docs/scripts/codegen_nargo_reference.sh b/docs/scripts/codegen_nargo_reference.sh
index 4ff7d43d142..6a9fda9420b 100755
--- a/docs/scripts/codegen_nargo_reference.sh
+++ b/docs/scripts/codegen_nargo_reference.sh
@@ -30,4 +30,4 @@ sidebar_position: 0
 ---
 " > $NARGO_REFERENCE
 
-cargo run -F codegen-docs -- info >> $NARGO_REFERENCE
+cargo run --bin nargo -F codegen-docs -- info >> $NARGO_REFERENCE
diff --git a/test_programs/compile_failure/assert_msg_runtime/src/main.nr b/test_programs/compile_failure/assert_msg_runtime/src/main.nr
index bec3082550a..fa21442e816 100644
--- a/test_programs/compile_failure/assert_msg_runtime/src/main.nr
+++ b/test_programs/compile_failure/assert_msg_runtime/src/main.nr
@@ -4,4 +4,4 @@ fn main(x: Field, y: pub Field) {
     let z = x + y;
     assert(z != y, f"Expected z != y, but got both equal {z}");
     assert_eq(x, y, f"Expected x == y, but x is {x} and y is {y}");
-}
\ No newline at end of file
+}
diff --git a/test_programs/compile_failure/brillig_assert_msg_runtime/src/main.nr b/test_programs/compile_failure/brillig_assert_msg_runtime/src/main.nr
index 428b2006363..bd77551e304 100644
--- a/test_programs/compile_failure/brillig_assert_msg_runtime/src/main.nr
+++ b/test_programs/compile_failure/brillig_assert_msg_runtime/src/main.nr
@@ -7,4 +7,4 @@ unconstrained fn conditional(x: Field) -> Field {
     assert_eq(z, 25, f"Expected 25 but got {z}");
     assert(x == 10, f"Expected x to equal 10, but got {x}");
     1
-}
\ No newline at end of file
+}
diff --git a/test_programs/compile_failure/brillig_mut_ref_from_acir/src/main.nr b/test_programs/compile_failure/brillig_mut_ref_from_acir/src/main.nr
index cf3279cac0d..473ad8e8d6a 100644
--- a/test_programs/compile_failure/brillig_mut_ref_from_acir/src/main.nr
+++ b/test_programs/compile_failure/brillig_mut_ref_from_acir/src/main.nr
@@ -5,4 +5,4 @@ unconstrained fn mut_ref_identity(value: &mut Field) -> Field {
 fn main(mut x: Field, y: pub Field) {
     let returned_x = mut_ref_identity(&mut x);
     assert(returned_x == x);
-}
\ No newline at end of file
+}
diff --git a/test_programs/compile_success_contract/contract_with_impl/src/main.nr b/test_programs/compile_success_contract/contract_with_impl/src/main.nr
index ddcb5d54d78..1c6b6c217c4 100644
--- a/test_programs/compile_success_contract/contract_with_impl/src/main.nr
+++ b/test_programs/compile_success_contract/contract_with_impl/src/main.nr
@@ -2,6 +2,6 @@ contract Foo {
     struct T { x: [Field] }
 
     impl T {
-        fn t(self){}
+        fn t(self) {}
     }
 }
diff --git a/test_programs/compile_success_contract/simple_contract/src/main.nr b/test_programs/compile_success_contract/simple_contract/src/main.nr
index ed90ac8bd1d..7412e1386bf 100644
--- a/test_programs/compile_success_contract/simple_contract/src/main.nr
+++ b/test_programs/compile_success_contract/simple_contract/src/main.nr
@@ -8,12 +8,9 @@ contract Foo {
     fn quadruple(x: Field) -> pub Field {
         x * 4
     }
-    open fn skibbidy(x: Field) -> pub Field {
-        x * 5
-    }
     // Regression for issue #3344
     #[contract_library_method]
-    fn foo(x : u8) -> u8 {
+    fn foo(x: u8) -> u8 {
         x
     }
 }
diff --git a/test_programs/execution_success/brillig_cow_regression/src/main.nr b/test_programs/execution_success/brillig_cow_regression/src/main.nr
index ba51548d9dd..1cae9b1ba41 100644
--- a/test_programs/execution_success/brillig_cow_regression/src/main.nr
+++ b/test_programs/execution_success/brillig_cow_regression/src/main.nr
@@ -8,9 +8,9 @@ global MAX_NEW_CONTRACTS_PER_TX: u64 = 1;
 global NUM_ENCRYPTED_LOGS_HASHES_PER_TX: u64 = 1;
 global NUM_UNENCRYPTED_LOGS_HASHES_PER_TX: u64 = 1;
 global NUM_FIELDS_PER_SHA256 = 2;
-global CALLDATA_HASH_INPUT_SIZE = 169;
-global CALL_DATA_HASH_LOG_FIELDS = 4;
-global CALL_DATA_HASH_FULL_FIELDS = 165;
+global TX_EFFECT_HASH_INPUT_SIZE = 169;
+global TX_EFFECT_HASH_LOG_FIELDS = 4;
+global TX_EFFECT_HASH_FULL_FIELDS = 165;
 
 struct PublicDataUpdateRequest {
     leaf_slot : Field,
@@ -99,7 +99,7 @@ impl U256 {
 }
 
 unconstrained fn main(kernel_data: DataToHash) -> pub [Field; NUM_FIELDS_PER_SHA256] {
-    let mut calldata_hash_inputs = [0; CALLDATA_HASH_INPUT_SIZE];
+    let mut tx_effects_hash_inputs = [0; TX_EFFECT_HASH_INPUT_SIZE];
 
     let new_note_hashes = kernel_data.new_note_hashes;
     let new_nullifiers = kernel_data.new_nullifiers;
@@ -111,65 +111,65 @@ unconstrained fn main(kernel_data: DataToHash) -> pub [Field; NUM_FIELDS_PER_SHA
     let mut offset = 0;
 
     for j in 0..MAX_NEW_NOTE_HASHES_PER_TX {
-        calldata_hash_inputs[offset + j] = new_note_hashes[j];
+        tx_effects_hash_inputs[offset + j] = new_note_hashes[j];
     }
     offset += MAX_NEW_NOTE_HASHES_PER_TX ;
 
     for j in 0..MAX_NEW_NULLIFIERS_PER_TX {
-        calldata_hash_inputs[offset + j] = new_nullifiers[j];
+        tx_effects_hash_inputs[offset + j] = new_nullifiers[j];
     }
     offset += MAX_NEW_NULLIFIERS_PER_TX ;
 
     for j in 0..MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX {
-        calldata_hash_inputs[offset + j * 2] =
+        tx_effects_hash_inputs[offset + j * 2] =
                 public_data_update_requests[j].leaf_slot;
-        calldata_hash_inputs[offset + j * 2 + 1] =
+        tx_effects_hash_inputs[offset + j * 2 + 1] =
                 public_data_update_requests[j].new_value;
     }
     offset += MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX * 2;
 
     for j in 0..MAX_NEW_L2_TO_L1_MSGS_PER_TX {
-        calldata_hash_inputs[offset + j] = newL2ToL1msgs[j];
+        tx_effects_hash_inputs[offset + j] = newL2ToL1msgs[j];
     }
     offset += MAX_NEW_L2_TO_L1_MSGS_PER_TX;
 
     let contract_leaf = kernel_data.new_contracts[0];
-    calldata_hash_inputs[offset] = contract_leaf.hash();
+    tx_effects_hash_inputs[offset] = contract_leaf.hash();
 
     offset += MAX_NEW_CONTRACTS_PER_TX;
 
     let new_contracts = kernel_data.new_contracts;
-    calldata_hash_inputs[offset] = new_contracts[0].contract_address;
+    tx_effects_hash_inputs[offset] = new_contracts[0].contract_address;
 
-    calldata_hash_inputs[offset + 1] = new_contracts[0].portal_contract_address;
+    tx_effects_hash_inputs[offset + 1] = new_contracts[0].portal_contract_address;
 
     offset += MAX_NEW_CONTRACTS_PER_TX * 2;
 
     for j in 0..NUM_FIELDS_PER_SHA256 {
-        calldata_hash_inputs[offset + j] = encryptedLogsHash[j];
+        tx_effects_hash_inputs[offset + j] = encryptedLogsHash[j];
     }
 
     offset += NUM_ENCRYPTED_LOGS_HASHES_PER_TX * NUM_FIELDS_PER_SHA256;
 
     for j in 0..NUM_FIELDS_PER_SHA256 {
-        calldata_hash_inputs[offset + j] = unencryptedLogsHash[j];
+        tx_effects_hash_inputs[offset + j] = unencryptedLogsHash[j];
     }
 
     offset += NUM_UNENCRYPTED_LOGS_HASHES_PER_TX * NUM_FIELDS_PER_SHA256;
-    assert_eq(offset, CALLDATA_HASH_INPUT_SIZE); // Sanity check
+    assert_eq(offset, TX_EFFECT_HASH_INPUT_SIZE); // Sanity check
 
-    let mut hash_input_flattened = [0; CALL_DATA_HASH_FULL_FIELDS * 32 + CALL_DATA_HASH_LOG_FIELDS * 16];
-    for offset in 0..CALL_DATA_HASH_FULL_FIELDS {
-        let input_as_bytes = calldata_hash_inputs[offset].to_be_bytes(32);
+    let mut hash_input_flattened = [0; TX_EFFECT_HASH_FULL_FIELDS * 32 + TX_EFFECT_HASH_LOG_FIELDS * 16];
+    for offset in 0..TX_EFFECT_HASH_FULL_FIELDS {
+        let input_as_bytes = tx_effects_hash_inputs[offset].to_be_bytes(32);
         for byte_index in 0..32 {
             hash_input_flattened[offset * 32 + byte_index] = input_as_bytes[byte_index];
         }
     }
 
-    for log_field_index in 0..CALL_DATA_HASH_LOG_FIELDS {
-        let input_as_bytes = calldata_hash_inputs[CALL_DATA_HASH_FULL_FIELDS + log_field_index].to_be_bytes(16);
+    for log_field_index in 0..TX_EFFECT_HASH_LOG_FIELDS {
+        let input_as_bytes = tx_effects_hash_inputs[TX_EFFECT_HASH_FULL_FIELDS + log_field_index].to_be_bytes(16);
         for byte_index in 0..16 {
-            hash_input_flattened[CALL_DATA_HASH_FULL_FIELDS * 32 + log_field_index * 16 + byte_index] = input_as_bytes[byte_index];
+            hash_input_flattened[TX_EFFECT_HASH_FULL_FIELDS * 32 + log_field_index * 16 + byte_index] = input_as_bytes[byte_index];
         }
     }
 
diff --git a/test_programs/execution_success/double_verify_proof/Nargo.toml b/test_programs/execution_success/double_verify_proof/Nargo.toml
index a4edd2e4288..c5954f54bdb 100644
--- a/test_programs/execution_success/double_verify_proof/Nargo.toml
+++ b/test_programs/execution_success/double_verify_proof/Nargo.toml
@@ -2,4 +2,6 @@
 name = "double_verify_proof"
 type = "bin"
 authors = [""]
-[dependencies]
+compiler_version = ">=0.24.0"
+
+[dependencies]
\ No newline at end of file
diff --git a/test_programs/execution_success/double_verify_proof/src/main.nr b/test_programs/execution_success/double_verify_proof/src/main.nr
index e4c6926efbc..4edf5c5af9f 100644
--- a/test_programs/execution_success/double_verify_proof/src/main.nr
+++ b/test_programs/execution_success/double_verify_proof/src/main.nr
@@ -1,6 +1,5 @@
 use dep::std;
 
-#[recursive]
 fn main(
     verification_key: [Field; 114],
     // This is the proof without public inputs attached.
diff --git a/test_programs/execution_success/double_verify_proof_recursive/Nargo.toml b/test_programs/execution_success/double_verify_proof_recursive/Nargo.toml
new file mode 100644
index 00000000000..b7688b33bfd
--- /dev/null
+++ b/test_programs/execution_success/double_verify_proof_recursive/Nargo.toml
@@ -0,0 +1,5 @@
+[package]
+name = "double_verify_proof_recursive"
+type = "bin"
+authors = [""]
+[dependencies]
diff --git a/test_programs/execution_success/double_verify_proof_recursive/Prover.toml b/test_programs/execution_success/double_verify_proof_recursive/Prover.toml
new file mode 100644
index 00000000000..dff48212e50
--- /dev/null
+++ b/test_programs/execution_success/double_verify_proof_recursive/Prover.toml
@@ -0,0 +1,5 @@
+key_hash = "0x096129b1c6e108252fc5c829c4cc9b7e8f0d1fd9f29c2532b563d6396645e08f"
+proof = ["0x000000000000000000000000000000d62b795bec274279129a71195796825fcc","0x00000000000000000000000000000000000793ab763140f20a68a6bd2721fd74","0x00000000000000000000000000000053141d06d3307b36153f321511199e579c","0x00000000000000000000000000000000000a4b55d6c21f98a9c434911dcb5c67","0x0000000000000000000000000000005f9d324c0abd22cec92d99dbec438e9491","0x0000000000000000000000000000000000240dfafe1b53dc27147cbab14ea893","0x000000000000000000000000000000044a61d3aac32c6931247cf334a19d9611","0x000000000000000000000000000000000003f0f8cf4207bfa85c23ec9f8d0c88","0x00000000000000000000000000000002168a470e39ba2ac266f6b474de12045f","0x000000000000000000000000000000000025791e7d3feab542345c00ec5a30df","0x000000000000000000000000000000dcafd76d4c3640969c80e017b951ef6397","0x00000000000000000000000000000000001d27f75a1256771e88e0c86fc42dbc","0x0000000000000000000000000000007347ae7d2d9d7fc2b8f0baa014ee1fed9f","0x000000000000000000000000000000000018bd927f42bf7caf9555f56f09000d","0x000000000000000000000000000000041f765f83cbe5904c8f453f70a4531d10","0x00000000000000000000000000000000001858aabeeb5331a221419f4fed1c19","0x000000000000000000000000000000d254a54caaedf8287b9af951b2f2611121","0x000000000000000000000000000000000005ab493623c9563cf2e55ba5f18200","0x00000000000000000000000000000014f24cddc1a02440dc63637df8032c8074","0x000000000000000000000000000000000011950c16cef98471b1d78b935195a4","0x000000000000000000000000000000b0340b459e6bd5cc8f031c8654a502897f","0x00000000000000000000000000000000000e1cf3968dac4545a76a2ae58e512c","0x0000000000000000000000000000002adf7218aa06ca0d2c2e600dcc39193a2d","0x00000000000000000000000000000000001302e7e4b0f14749bd885ca25588b6","0x00000000000000000000000000000092009ce4056e79ab815d8cdfd4491138ae","0x000000000000000000000000000000000018af11e853c6cf2f0f6274b0da8133","0x000000000000000000000000000000dd3dc6f49232141718527b3a0e4b26e21d","0x00000000000000000000000000000000001a877853348a8b695c4f9a9aa4ce68","0x000000000000000000000000000000aecfc56ba07155450b368140d6324023b5","0x000000000000000000000000000000000029c11052798c57ece614617d33fcc2","0x000000000000000000000000000000eb106ffc816d16fb84e84b0b61157b2603","0x000000000000000000000000000000000026c3cac16206899a21cb5126841446","0x000000000000000000000000000000a782ed54805fe845068b362b58e2fa34ec","0x00000000000000000000000000000000000cf046a1bfcc666b7f28b572676073","0x000000000000000000000000000000b931c8dda60bb4aca4cc817f5540f1209f","0x000000000000000000000000000000000024ad50c3936fafc3d190e6a4874223","0x000000000000000000000000000000cce90cfbaf5671c8c8652db28a3a9566f7","0x000000000000000000000000000000000003574db9d0f84380c9635660f86354","0x0000000000000000000000000000003eb3e1dc31846a90f721e7a08c6d6dc4f7","0x000000000000000000000000000000000028999a700cd1abae1a288eebb9a91c","0x000000000000000000000000000000c1be4d385b11387e14eb9817050d772f78","0x000000000000000000000000000000000003c56b5bad8b4484c66ac921f1f102","0x000000000000000000000000000000ace245cabf0f00dc7fd253dd8af0377a14","0x0000000000000000000000000000000000107f1731fcf34b364c813599fa1df7","0x035b937d404932b542b706eb810ef4a7dca4566d4dde1ad6a8717f46167ead7e","0x17608cef3dc7960f41cb1295706df663727d45ee598a61e05e989d111449fb65","0x054712a950ad67da3aa860e49e6891f99b586b7f37caff94eb013fdb374b61ee","0x04b755083086c769b7f593e0e48d68dc54be808203351380ca5566a48149d8bb","0x17d7670b0915235f626fdc1d7e1134d2be906ef138d7843384b3ebc23b1d630f","0x064cf544ab5f4e3dab47960502cccc83321fb275068dfbdd3a2fcbc6dddcaa65","0x083338262712e2b66769ea40d9f412b18caa1bc81a51ff5a50b6c41f8c4b3d23","0x0cdd38958cab97defde00f4a5961b6fd676e29d9f2c352f6bb2c68b91f83f8af","0x02c8bdd005c2f43a0a8cbb2744916ce5c322dfa5b23367a829c12699f4036d32","0x25bac73c7e7b659fbea3135b7a0decf9db8dc3045bd2837dae337c64cc722546","0x19eb361aa419d37bce3d2e8b2b7692a02a9559e83d7f3d8fe9169970fbbc2cba","0x2494bd5106d00e05c7ea60e632e9fe03773b7f2c5b662aa37ec512a01f4a0775","0x18c52c2f2c6e7be1d7847c15e452a3a9c64316103d12e4b5b9a82fac4e940ee9","0x0e0342810456ef78f498c1bfa085a5f3cbc06db1f32fabd0ea9ad27dccac1680","0x024c13d6ef56af33ed7164ea8e47ddecc8a487b000d8b1b45edcd3895a503ba2","0x26e0d127f626bd39b55bc5d0c131dbf03fe006dc5c3edc57dda1e629799a4317","0x1b1140061bc52b15c4f5e100729a81968ee79dc03deb966a18850335a8e44a8b","0x1bb76f945199e71d531a89288912087a02dd0e83020e65d671485bf2e5e86e1a","0x29269900859c6d86e404185b415bf3b279cd100f38cfdb0077e8d6a299c4fd35","0x22b5e94bae2f6f0cdb424a3b12c4bf82cec3fb228e012c1974ed457827bbe012","0x18d3543a93249778e7a57936170dae85ffc47c2567f2d0076a32c0bb86fcf10a","0x03721dc2670206cde42a175fd56bcce32cf6cb8801450a8e8e4b3d4e07785973","0x2806db136dd214d3ac1478460855cae6a4324ab45cab35320d104fee26c260e8","0x1c3749f1937082afbbae9375b9be708cf339e1983e57ef4447f36cfa560c685c","0x1067b8cfb90ef08bcb48aea56b2716334241787c2004a95682d68a0685566fd0","0x0f41aee4416398f1d48ffc302403273cddef34a41f98507c53682041d82e51ff","0x10d854c9f0bfbdff7ca91a68f4978e9a79e7b14243d92f465f17bdf88d9f64f8","0x00000000000000000000000000000000018938b11099e0cdc05ddab84a153a97","0x0000000000000000000000000000000001d7dda1471f0dc3b3a3d3438c197982","0x00000000000000000000000000000000022682917da43ab9a6e9cbcece1db86d","0x2453913e6b0f36eab883ac4b0e0604d56aaeb9c55e641135173e63c342f1a660","0x05216c1b58dc43a49d01aaba3113b0e86be450fc17d28016e648e7162a1b67fb","0x152b34845a0222a2b41354c0d395a250d8363dc18748647d85acd89d6934ec56","0x1dfc6e971ce82b7dcda1f7f282713c6e22a8c79258a61209bda69719806da544","0x2968dd8b3af8e3953f1fbbd72f4c49b8270597bb27d4037adc157ac6083bee60","0x1b9425b88a4c7d39b3d75afe66917a9aa1d2055724392bc01fb918d84ff1410e","0x04ab571f236d8e750904dc307dd274003d9130f1a7110e4c1521cfb408877c73","0x2ad84f26fdc5831545272d02b806bb0e6dae44e71f73552c4eb9ff06030748c7","0x020e632b99d325db774b8630fb50b9a4e74d35b7f27d9fc02c65087ee747e42c","0x09a8c5a3171268cb61c02515c01c109889200ed13f415ae54df2078bbb887f92","0x1143281a9451abbb4c34c3fa84e7678c2af2e7ea8c05160a6f7f06988fc91af8","0x000000000000000000000000000000cbda736ca5cf6bc75413c2cc9e28ab0a68","0x00000000000000000000000000000000001ee78c9cc56aa5991062ae2e338587","0x000000000000000000000000000000bc9bfcdebb486f4cb314e681d2cc5f8df6","0x00000000000000000000000000000000000ad538431d04771bca7f633cb659ff","0x000000000000000000000000000000d45b317afcefa466a59bba9e171f1af70c","0x0000000000000000000000000000000000133c50180ea17932e4881124e7a7c6","0x000000000000000000000000000000fc9ed37f543775849f3e84eaa06f77f992","0x00000000000000000000000000000000001372873c9c051d1baff99248b8f70e"]
+public_inputs = ["0x0000000000000000000000000000000000000000000000000000000000000003"]
+verification_key = ["0x2b337de1c8c14f22ec9b9e2f96afef3652627366f8170a0a948dad4ac1bd5e80","0x0000000000000000000000000000000000000000000000000000000000000008","0x0000000000000000000000000000000000000000000000000000000000000005","0x0000000000000000000000000000000000000000000000000000000000000008","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0x00000000000000000000000000000092139c61bae1a44f0fc7689507414be688","0x00000000000000000000000000000000000160ce4e279582f91bde4f03f5e9a2","0x0000000000000000000000000000005dc2d37f658c3b2d60f24740eb13b65d79","0x000000000000000000000000000000000007e3e8a5d98a1177ec85bf88f163a5","0x000000000000000000000000000000dc3035fbd7ff16412a8fd7da587a935298","0x000000000000000000000000000000000023d08e2817ac16990004ed11d8fc66","0x000000000000000000000000000000356a5ad59c646c746a8d09f5d154e47c4f","0x00000000000000000000000000000000000708529196af3c8e16ffa580c26182","0x0000000000000000000000000000002ddfe70eb7a1280596e8e4a804f118a6dd","0x000000000000000000000000000000000013757e15a0905f298303784a161b21","0x000000000000000000000000000000a23a729df796935c7824e3a26be794829b","0x000000000000000000000000000000000005775b6c146c4a59856e869fe5a70e","0x000000000000000000000000000000eef0c9e088fd2d45aa40311082d1f2809b","0x00000000000000000000000000000000001d539ccbfc556d0ad59307a218de65","0x000000000000000000000000000000a2c848beceb6ab7806fd3b88037b8410fc","0x0000000000000000000000000000000000177004deeb1f9d401fd7b1af1a5ac8","0x0000000000000000000000000000002508eb63672a733f20de1a97644be4f540","0x00000000000000000000000000000000000d82d51f2f75d806285fd248c819b8","0x000000000000000000000000000000d002f9100cbba8a29f13b11513c53c59d0","0x000000000000000000000000000000000006cd3b0e3460533b9e5ea2cdc0fcbb","0x000000000000000000000000000000f45ea38a93b2f810c5633ddb54927c1c96","0x000000000000000000000000000000000021791de65f9a28ec7024b1a87ab4f3","0x000000000000000000000000000000926511a0439502c86885a8c6f0327aa7ad","0x000000000000000000000000000000000029fa14a969c5d81ed3abbbfb11220a","0x000000000000000000000000000000b84c3258e8206f560e5b5b18cbeafef87e","0x00000000000000000000000000000000002a910445cd8fc895e5d235cd8ea185","0x000000000000000000000000000000887e67f15e84bcb8507a5064a363f6043b","0x000000000000000000000000000000000014dc6643d801c3ef27c2066b6e2bb4","0x000000000000000000000000000000e38e900b42c314ba803088e8fbf125203f","0x000000000000000000000000000000000020690fd4869db418306046b38161dc","0x0000000000000000000000000000001e2fa856bf7951b8292b1e88185993629c","0x0000000000000000000000000000000000048a85e0bbac7c60ad3d78f601f63c","0x0000000000000000000000000000006f457719495073d3666d77a625aeab0c51","0x00000000000000000000000000000000002623ad892dc62b1fa7d0a650f0d470","0x000000000000000000000000000000dbfcc8a467e021c03b13f74a9f79c3a10c","0x0000000000000000000000000000000000295f6f10976c37bd9c6f96bb7187d5","0x000000000000000000000000000000c13ef9a937cc12420fb38d9ab8e848e85e","0x000000000000000000000000000000000003560a3b334e887532f605c9cb7628","0x0000000000000000000000000000009bcebf08a4599cdda0fb96312d4dc0c7a9","0x000000000000000000000000000000000015adc8bb1e01c835f48959d1237bd6","0x00000000000000000000000000000047762ab839e4ff63c77605a9f383da37c2","0x000000000000000000000000000000000016a8c3c53d89660cf271522cd301fb","0x000000000000000000000000000000f0c8539a0b5f94420a513f9c305b932bfe","0x00000000000000000000000000000000002957ba01d9de5638f808f88a692533","0x000000000000000000000000000000ab17c6189d67d3bf5dd2f3885de0151b6f","0x0000000000000000000000000000000000060d8aa43fdc434d1942263f364d95","0x0000000000000000000000000000005d292333b3adb497f00b4bc32d45229060","0x00000000000000000000000000000000001a1018a66221883639f2898a66f345","0x00000000000000000000000000000006555a806b1993291deba0dc44e2abf431","0x00000000000000000000000000000000000cacff7099a9d5e35a21f4a00b2dc3","0x000000000000000000000000000000f50c11ba95d349c36d143eefd12e494950","0x00000000000000000000000000000000001022e8c5f02d639bc9dd8bc4407f99","0x000000000000000000000000000000c76828795098eda73d50b4b585c60afc60","0x00000000000000000000000000000000002bf09c0ec7011e93888962f2406630","0x00000000000000000000000000000049e5c83a8978d832fb8e144548e3ca1adb","0x00000000000000000000000000000000000e0ec242c2e160a984f61ca5adf5f5","0x0000000000000000000000000000009c5d6e08a6605ab4513748ac0fa017dd1c","0x00000000000000000000000000000000001f54baa07558e5fb055bd9ba49c067","0x0000000000000000000000000000001e1ee7ee29bbb5e4b080c6091c1433ce62","0x000000000000000000000000000000000024aec62a9d9763499267dc98c33428","0x0000000000000000000000000000001a96755946ff16f0d6632365f0eb0ab4d4","0x000000000000000000000000000000000028cf3e22bcd53782ebc3e0490e27e5","0x00000000000000000000000000000043148d7d8c9ba43f2133fab4201435a364","0x0000000000000000000000000000000000234ce541f1f5117dd404cfaf01a229","0x000000000000000000000000000000a7fb95ffb461d9514a1070e2d2403982ef","0x00000000000000000000000000000000003016955028b6390f446c3fd0c5b424","0x00000000000000000000000000000008863c3b7cd7cddc20ba79ce915051c56e","0x000000000000000000000000000000000013ef666111b0be56a235983d397d2a","0x000000000000000000000000000000e3993f465fc9f56e93ac769e597b752c1c","0x0000000000000000000000000000000000217f7c4235161e9a3c16c45b6ca499","0x0000000000000000000000000000008ffa4cd96bc67b0b7df5678271e1114075","0x0000000000000000000000000000000000256467bfcb63d9fdcb5dde397757ad","0x00000000000000000000000000000054e5eb270bb64bde6e6ececadfd8c3236c","0x00000000000000000000000000000000000e52d1bd75812c33c6f3d79ee4b94c","0x000000000000000000000000000000484a2c641dce55bc2dd64ef0cd790a7fea","0x00000000000000000000000000000000000ff417d256be43e73c8b1aa85bdda3","0x0000000000000000000000000000000b72e7b7713ab5da44e0f864182e748a23","0x00000000000000000000000000000000001a221055f1625ad833a44705f5f74e","0x00000000000000000000000000000067a99a34e9b81a17ad001db02e29bcb82a","0x000000000000000000000000000000000018a6c02e398389827568fa960e86e2","0x000000000000000000000000000000bb29f26f9890d6cc6401f4921d5884edca","0x00000000000000000000000000000000000868357b28039385c5a5058b6d358e","0x00000000000000000000000000000036fb6e229dde8edf7ec858b12d7e8be485","0x00000000000000000000000000000000001060afe929554ca473103f5e68193c","0x00000000000000000000000000000015226e07e207744c0857074dcab883af4a","0x00000000000000000000000000000000000b1c02619282755533457230b19b4a","0x0000000000000000000000000000001f2a0277e4807e6e1cbabca21dde5eb5e1","0x00000000000000000000000000000000000d928deafed363659688ed4ccdef52","0x000000000000000000000000000000363f0c994e91cecad25835338edee2294f","0x00000000000000000000000000000000002eea648c8732596b1314fe2a4d2f05","0x000000000000000000000000000000b2671d2ae51d31c1210433c3972bb64578","0x00000000000000000000000000000000000ab49886c2b94bd0bd3f6ed1dbbe2c"]
+proof_b = ["0x000000000000000000000000000000f05c69448ca29bdf52076f9b073bb30fed","0x000000000000000000000000000000000028c86bb3e27b4aaaaef126f7df5349","0x00000000000000000000000000000026ae031fc93594375dfc7f3bbe027f97d5","0x000000000000000000000000000000000000dd12c7290fe7f775796a233b8590","0x000000000000000000000000000000c1ee6631704de424d010c5c4ac8293ac49","0x00000000000000000000000000000000002f41818c9aa83f5c8d9bdd128015b9","0x000000000000000000000000000000b50a5801482f7e3a5de8ab3cce0f10b0d3","0x000000000000000000000000000000000022a0bc69c293dbf293b25bc9eef7f8","0x0000000000000000000000000000003b02abf1967ef394154dc15d763135e903","0x00000000000000000000000000000000000d8a2ee46acc6d1ed8d517b56d47c8","0x00000000000000000000000000000039bf0d1b3d8cf9de898f101c626e978d78","0x0000000000000000000000000000000000008faa7df2451a24d291a9b584f1a5","0x000000000000000000000000000000c1dae329ed7adf63a2d89a5f16fb98b6d8","0x00000000000000000000000000000000001ff0bc16fc0bd4aa2d6255690453c2","0x000000000000000000000000000000d12d7589f853a9b472613efa56689beaf1","0x00000000000000000000000000000000002d6fbc798f4403751df6aeee8bedd3","0x0000000000000000000000000000007c1fa069cb17194fecf88db9dd54a4ee36","0x0000000000000000000000000000000000268e026f9814822a42b2d59eec5d24","0x000000000000000000000000000000c3fb56beab774218cd63498fc050a5fd9b","0x00000000000000000000000000000000000071c014d7b5063f005a0bc2ee1af4","0x000000000000000000000000000000ae12b25371c6af42bbe0a85cddd2eaebc7","0x000000000000000000000000000000000026d270e1ffc9c7c344c694dfadda83","0x00000000000000000000000000000080280858c6be461716921caa3c26f3f6f3","0x000000000000000000000000000000000001dcdd3f39e27d0ce6aa5d14dff4c1","0x000000000000000000000000000000080e1d2c913c834ebcf7e0600c076c08fd","0x00000000000000000000000000000000002df3d142217694e65fb7c355d62764","0x000000000000000000000000000000e5e336f3f59d77e500f49771bfbeb12e83","0x000000000000000000000000000000000028fffe08bdc4c0690643d2e1a1275f","0x000000000000000000000000000000db5618b32afc13e18f21b39f3fbede9d11","0x00000000000000000000000000000000001d244818370d43fb7e8bc67e03787b","0x0000000000000000000000000000006bcc1fd3f9f78449ad1df1bc11bc379edd","0x000000000000000000000000000000000009ac9cbb285edbf5b3a973f3f5f1cb","0x000000000000000000000000000000fd885905b6c0fc95bb4dd0b11f6797d4b3","0x000000000000000000000000000000000021f07995cdd835145e19c38127c562","0x000000000000000000000000000000bbbf2b975c2c97ae4b45c4a52059e53ee3","0x000000000000000000000000000000000024158163788841cf4590bbc1e89a90","0x0000000000000000000000000000009aca93d2b1386ea412d4b36ea5bb9894a8","0x00000000000000000000000000000000002532d1d210e8ed4c2f5c00cbaaa475","0x000000000000000000000000000000634a88caa1d77cb6b5fe77cac31458fc31","0x00000000000000000000000000000000000bdf18bae92fce7cfddab5520cac6e","0x000000000000000000000000000000622e9626255170ccec77602c755aa193e1","0x000000000000000000000000000000000001d4edba370e04436a988bad05dada","0x000000000000000000000000000000b52934323a0aec8f803cdaafee2ab7bfb2","0x0000000000000000000000000000000000155312af5e0e25ca9fd61aef9e58ed","0x06270b517855f6f6a608e432883d1d1030a12a1e33022dc142b7728691421da2","0x2af7c794d7b720b25eb1df0afd8c8e3c15b6e518194c3caea7966a5f8210ff04","0x073fe573aeb27d81a5713be93e1365390dcbc3c8e7439ff1d36a84cc014f5642","0x11351b961147431e54535248b58b35cf5cddb9b13827899167617d7a96794d64","0x297c9421c9c3db286770787c35b86bc41583386491b4ae55e5fa81aefa21efc4","0x0f4eeca3ff4a3495f859898937688652d33f9b4dd3e003e12adf15278e0997c3","0x133e3d8b82721d40d919f2326810ba6f07eff3f7d20d86b2bde692a811522019","0x2c502f53c9698b73bb8c8f9b9cf2d705d16a64a7040348b4b39c637a2064316c","0x0cbc1971e1c566cde9d9125c91cdc88e817db182692f836c1a5170a6246eaf73","0x12c47793e7db706c637cd4b4d96d227f569850176b852b1fe8ad522ddb38ef0e","0x0cd7b300e9309a135285be1aeb02b152f97931a7357ab6d609a2cb1970aab877","0x2a7789dfe286c9d0a7592f1c9316e730cb14c9d843aefc4764d76e7f8571c96a","0x248ac54ce3dbf37796621882a4ac76046df5ab680da487fd85cce76b1ae392d3","0x149d1d07cebe320f77b03533e34912545cedeae62bd9778d37724728762b5710","0x00fe29daebdaed61309790e70e2dcefa3f3af4c6c965ce424b8dbcf09b8e4b49","0x2b75b3bace61b731d7f0c003a144b62b0a4fbe9f0d14ca89b0652b70210014b3","0x2588ef27cfb6e0d8c6f9a969b2da44fead30a02ed70a563fd15aa45bb671de1c","0x2b74d7674b55642697b4a1e226eddb0e4918b2d57aa5b99093dc46cadcdea000","0x244c626845d3a5040f08f01e9611f968ad675ca857789149b13a0cfa83a2e064","0x2cb8d02f90cae33fd7bcfb80af4aff067c4f5fc4b3f9228d5b8f768bc8f6c971","0x1372f3d1f04e0c39a50e823d5da03d70bebe19a1b8e28f8c2ff601cc0bfc0095","0x19af6601d2613426a50b7c35d60562a5f2f2634e6af56dac13459632e15570ee","0x13c2a16ed3b65dcd9414659be79af17995d344de34eaf962343b0f1e76c73a57","0x0dd5dcdbd50b8774831d4f01f930804d38b4266dfee085185530880a0c3903c0","0x07e91848d660b11b722638680ac60f20db9507fdc8d610ce762600f5a1aacd29","0x1f9c2a94d10c0a7fb60292cfc46fd3d2501181bea0ffe1f5f2501d474be3a785","0x14edb9c5bd389eae08a5ea2a7a1662894e1e878c142084d966a625bef68cf7c3","0x00000000000000000000000000000000cecd01810814d175f0a533f0067618c4","0x00000000000000000000000000000000f82935013ce5c82720c63e533af41db8","0x000000000000000000000000000000012185688171b6bed850e748b66f7222ac","0x2dd7f5ff2150155c2ac86ebe28d9ecbca2eea812b0021ab2bceae111cfea8325","0x04ea6c2daf2b9e827d2213c3d03953410dcf1ed67ba34a3c00e772be92606a8b","0x163f2bd18dcde52f99b9867c944780fd718d1612927053b139b280fc55013d1b","0x05e388fd160ccac30a8f7b18a4bd042f705e92b5937e8c0e9478e2ff623907c6","0x00ba3f6f527d6ed3ff17a63b1d5be3c42bdfae88fdf63311fc7b871157939309","0x16187d9daa8c2e5a1a9ab15be7ca6a8feebfb31bea76f9a3ca69381881c70561","0x0f64522e4904edb7377b14a7b9dad848829167324ef5c016346b3ad8251191ee","0x273bbe6000a4001dce369e5a36cc0b0ca3fd351665b688238aa8c556a6ca6b8e","0x022d2232efb2faa8307846c9a4c697aabad1b7f1336b35ad72fa8922975b49d9","0x0d82d478bff3955c4b0a34ef94427ca5f9da23147ad953c89f2e428277ec2825","0x18d886be90343010659c231583be61a138e28e37c24771e3cb61fbe2587d0671","0x000000000000000000000000000000196ba6a58dbeb7c34cb1d6287e23d434de","0x00000000000000000000000000000000001df8ae8a1589590f8863c1fefd8dfd","0x000000000000000000000000000000f30e11b2c5fbefa166cbb9f58c5f8e1a4c","0x000000000000000000000000000000000026420ade7666bc0ab1cf1fd9d0c534","0x0000000000000000000000000000000feb5b7d8260d25a1ee1ce76ff461673fc","0x00000000000000000000000000000000002bd2ac6223a80671b777bf5dca70a4","0x000000000000000000000000000000690f757006d2fa1ddb0114c9f268783537","0x000000000000000000000000000000000023ad36feadd91e50118f32e97a0204"]
\ No newline at end of file
diff --git a/test_programs/execution_success/double_verify_proof_recursive/src/main.nr b/test_programs/execution_success/double_verify_proof_recursive/src/main.nr
new file mode 100644
index 00000000000..e4c6926efbc
--- /dev/null
+++ b/test_programs/execution_success/double_verify_proof_recursive/src/main.nr
@@ -0,0 +1,29 @@
+use dep::std;
+
+#[recursive]
+fn main(
+    verification_key: [Field; 114],
+    // This is the proof without public inputs attached.
+    // 
+    // This means: the size of this does not change with the number of public inputs.
+    proof: [Field; 93],
+    public_inputs: pub [Field; 1],
+    // This is currently not public. It is fine given that the vk is a part of the circuit definition.
+    // I believe we want to eventually make it public too though.
+    key_hash: Field,
+    proof_b: [Field; 93]
+) {
+    std::verify_proof(
+        verification_key.as_slice(),
+        proof.as_slice(),
+        public_inputs.as_slice(),
+        key_hash
+    );
+
+    std::verify_proof(
+        verification_key.as_slice(),
+        proof_b.as_slice(),
+        public_inputs.as_slice(),
+        key_hash
+    );
+}
diff --git a/test_programs/noir_test_failure/should_fail_suite_with_one_failure/Nargo.toml b/test_programs/noir_test_failure/should_fail_suite_with_one_failure/Nargo.toml
new file mode 100644
index 00000000000..3d2cf2c6096
--- /dev/null
+++ b/test_programs/noir_test_failure/should_fail_suite_with_one_failure/Nargo.toml
@@ -0,0 +1,5 @@
+[package]
+name = "should_fail_with_mismatch"
+type = "bin"
+authors = [""]
+[dependencies]
diff --git a/test_programs/test_libraries/exporting_lib/src/lib.nr b/test_programs/test_libraries/exporting_lib/src/lib.nr
index af1fd7a32de..bfb1819132a 100644
--- a/test_programs/test_libraries/exporting_lib/src/lib.nr
+++ b/test_programs/test_libraries/exporting_lib/src/lib.nr
@@ -1,4 +1,3 @@
-
 struct MyStruct {
     inner: Field
 }
diff --git a/tooling/acvm_cli/Cargo.toml b/tooling/acvm_cli/Cargo.toml
new file mode 100644
index 00000000000..72424405d36
--- /dev/null
+++ b/tooling/acvm_cli/Cargo.toml
@@ -0,0 +1,38 @@
+[package]
+name = "acvm_cli"
+description = "The entrypoint for executing the ACVM"
+# x-release-please-start-version
+version = "0.40.0"
+# x-release-please-end
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+rust-version.workspace = true
+repository.workspace = true
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+# Rename binary from `acvm_cli` to `acvm`
+[[bin]]
+name = "acvm"
+path = "src/main.rs"
+
+[dependencies]
+thiserror.workspace = true
+toml.workspace = true
+color-eyre = "0.6.2"
+clap.workspace = true
+acvm.workspace = true
+nargo.workspace = true
+const_format.workspace = true
+bn254_blackbox_solver.workspace = true
+acir.workspace = true
+
+# Logs
+tracing-subscriber.workspace = true
+tracing-appender = "0.2.3"
+
+[dev-dependencies]
+rand = "0.8.5"
+proptest = "1.2.0"
+paste = "1.0.14"
diff --git a/tooling/acvm_cli/src/cli/execute_cmd.rs b/tooling/acvm_cli/src/cli/execute_cmd.rs
new file mode 100644
index 00000000000..255b6131fd6
--- /dev/null
+++ b/tooling/acvm_cli/src/cli/execute_cmd.rs
@@ -0,0 +1,78 @@
+use std::io::{self, Write};
+
+use acir::circuit::Circuit;
+use acir::native_types::WitnessMap;
+use bn254_blackbox_solver::Bn254BlackBoxSolver;
+use clap::Args;
+
+use crate::cli::fs::inputs::{read_bytecode_from_file, read_inputs_from_file};
+use crate::cli::fs::witness::save_witness_to_dir;
+use crate::errors::CliError;
+use nargo::ops::{execute_circuit, DefaultForeignCallExecutor};
+
+use super::fs::witness::create_output_witness_string;
+
+/// Executes a circuit to calculate its return value
+#[derive(Debug, Clone, Args)]
+pub(crate) struct ExecuteCommand {
+    /// Write the execution witness to named file
+    #[clap(long, short)]
+    output_witness: Option<String>,
+
+    /// The name of the toml file which contains the input witness map
+    #[clap(long, short)]
+    input_witness: String,
+
+    /// The name of the binary file containing circuit bytecode
+    #[clap(long, short)]
+    bytecode: String,
+
+    /// The working directory
+    #[clap(long, short)]
+    working_directory: String,
+
+    /// Set to print output witness to stdout
+    #[clap(long, short, action)]
+    print: bool,
+}
+
+fn run_command(args: ExecuteCommand) -> Result<String, CliError> {
+    let bytecode = read_bytecode_from_file(&args.working_directory, &args.bytecode)?;
+    let circuit_inputs = read_inputs_from_file(&args.working_directory, &args.input_witness)?;
+    let output_witness = execute_program_from_witness(&circuit_inputs, &bytecode, None)?;
+    let output_witness_string = create_output_witness_string(&output_witness)?;
+    if args.output_witness.is_some() {
+        save_witness_to_dir(
+            &output_witness_string,
+            &args.working_directory,
+            &args.output_witness.unwrap(),
+        )?;
+    }
+    Ok(output_witness_string)
+}
+
+pub(crate) fn run(args: ExecuteCommand) -> Result<String, CliError> {
+    let print = args.print;
+    let output_witness_string = run_command(args)?;
+    if print {
+        io::stdout().write_all(output_witness_string.as_bytes()).unwrap();
+    }
+    Ok(output_witness_string)
+}
+
+pub(crate) fn execute_program_from_witness(
+    inputs_map: &WitnessMap,
+    bytecode: &[u8],
+    foreign_call_resolver_url: Option<&str>,
+) -> Result<WitnessMap, CliError> {
+    let blackbox_solver = Bn254BlackBoxSolver::new();
+    let circuit: Circuit = Circuit::deserialize_circuit(bytecode)
+        .map_err(|_| CliError::CircuitDeserializationError())?;
+    execute_circuit(
+        &circuit,
+        inputs_map.clone(),
+        &blackbox_solver,
+        &mut DefaultForeignCallExecutor::new(true, foreign_call_resolver_url),
+    )
+    .map_err(CliError::CircuitExecutionError)
+}
diff --git a/tooling/acvm_cli/src/cli/fs/inputs.rs b/tooling/acvm_cli/src/cli/fs/inputs.rs
new file mode 100644
index 00000000000..2a46cfba884
--- /dev/null
+++ b/tooling/acvm_cli/src/cli/fs/inputs.rs
@@ -0,0 +1,54 @@
+use acir::{
+    native_types::{Witness, WitnessMap},
+    FieldElement,
+};
+use toml::Table;
+
+use crate::errors::{CliError, FilesystemError};
+use std::{fs::read, path::Path};
+
+/// Returns the circuit's parameters parsed from a toml file at the given location
+pub(crate) fn read_inputs_from_file<P: AsRef<Path>>(
+    working_directory: P,
+    file_name: &String,
+) -> Result<WitnessMap, CliError> {
+    let file_path = working_directory.as_ref().join(file_name);
+    if !file_path.exists() {
+        return Err(CliError::FilesystemError(FilesystemError::MissingTomlFile(
+            file_name.to_owned(),
+            file_path,
+        )));
+    }
+
+    let input_string = std::fs::read_to_string(file_path)
+        .map_err(|_| FilesystemError::InvalidTomlFile(file_name.clone()))?;
+    let input_map = input_string
+        .parse::<Table>()
+        .map_err(|_| FilesystemError::InvalidTomlFile(file_name.clone()))?;
+    let mut witnesses: WitnessMap = WitnessMap::new();
+    for (key, value) in input_map.into_iter() {
+        let index =
+            Witness(key.trim().parse().map_err(|_| CliError::WitnessIndexError(key.clone()))?);
+        if !value.is_str() {
+            return Err(CliError::WitnessValueError(key.clone()));
+        }
+        let field = FieldElement::from_hex(value.as_str().unwrap()).unwrap();
+        witnesses.insert(index, field);
+    }
+
+    Ok(witnesses)
+}
+
+/// Returns the circuit's bytecode read from the file at the given location
+pub(crate) fn read_bytecode_from_file<P: AsRef<Path>>(
+    working_directory: P,
+    file_name: &String,
+) -> Result<Vec<u8>, FilesystemError> {
+    let file_path = working_directory.as_ref().join(file_name);
+    if !file_path.exists() {
+        return Err(FilesystemError::MissingBytecodeFile(file_name.to_owned(), file_path));
+    }
+    let bytecode: Vec<u8> =
+        read(file_path).map_err(|_| FilesystemError::InvalidBytecodeFile(file_name.clone()))?;
+    Ok(bytecode)
+}
diff --git a/tooling/acvm_cli/src/cli/fs/mod.rs b/tooling/acvm_cli/src/cli/fs/mod.rs
new file mode 100644
index 00000000000..f23ba06fd8b
--- /dev/null
+++ b/tooling/acvm_cli/src/cli/fs/mod.rs
@@ -0,0 +1,2 @@
+pub(super) mod inputs;
+pub(super) mod witness;
diff --git a/tooling/acvm_cli/src/cli/fs/witness.rs b/tooling/acvm_cli/src/cli/fs/witness.rs
new file mode 100644
index 00000000000..2daaa5a3a58
--- /dev/null
+++ b/tooling/acvm_cli/src/cli/fs/witness.rs
@@ -0,0 +1,36 @@
+use std::{
+    collections::BTreeMap,
+    fs::File,
+    io::Write,
+    path::{Path, PathBuf},
+};
+
+use acvm::acir::native_types::WitnessMap;
+
+use crate::errors::{CliError, FilesystemError};
+
+/// Saves the provided output witnesses to a toml file created at the given location
+pub(crate) fn save_witness_to_dir<P: AsRef<Path>>(
+    output_witness: &String,
+    witness_dir: P,
+    file_name: &String,
+) -> Result<PathBuf, FilesystemError> {
+    let witness_path = witness_dir.as_ref().join(file_name);
+
+    let mut file = File::create(&witness_path)
+        .map_err(|_| FilesystemError::OutputWitnessCreationFailed(file_name.clone()))?;
+    write!(file, "{}", output_witness)
+        .map_err(|_| FilesystemError::OutputWitnessWriteFailed(file_name.clone()))?;
+
+    Ok(witness_path)
+}
+
+/// Creates a toml representation of the provided witness map
+pub(crate) fn create_output_witness_string(witnesses: &WitnessMap) -> Result<String, CliError> {
+    let mut witness_map: BTreeMap<String, String> = BTreeMap::new();
+    for (key, value) in witnesses.clone().into_iter() {
+        witness_map.insert(key.0.to_string(), format!("0x{}", value.to_hex()));
+    }
+
+    toml::to_string(&witness_map).map_err(|_| CliError::OutputWitnessSerializationFailed())
+}
diff --git a/tooling/acvm_cli/src/cli/mod.rs b/tooling/acvm_cli/src/cli/mod.rs
new file mode 100644
index 00000000000..a610b08ab77
--- /dev/null
+++ b/tooling/acvm_cli/src/cli/mod.rs
@@ -0,0 +1,41 @@
+use clap::{Parser, Subcommand};
+use color_eyre::eyre;
+use const_format::formatcp;
+
+mod execute_cmd;
+mod fs;
+
+const ACVM_VERSION: &str = env!("CARGO_PKG_VERSION");
+
+static VERSION_STRING: &str = formatcp!("version = {}\n", ACVM_VERSION,);
+
+#[derive(Parser, Debug)]
+#[command(name="acvm", author, version=VERSION_STRING, about, long_about = None)]
+struct ACVMCli {
+    #[command(subcommand)]
+    command: ACVMCommand,
+}
+
+#[non_exhaustive]
+#[derive(Subcommand, Clone, Debug)]
+enum ACVMCommand {
+    Execute(execute_cmd::ExecuteCommand),
+}
+
+#[cfg(not(feature = "codegen-docs"))]
+pub(crate) fn start_cli() -> eyre::Result<()> {
+    let ACVMCli { command } = ACVMCli::parse();
+
+    match command {
+        ACVMCommand::Execute(args) => execute_cmd::run(args),
+    }?;
+
+    Ok(())
+}
+
+#[cfg(feature = "codegen-docs")]
+pub(crate) fn start_cli() -> eyre::Result<()> {
+    let markdown: String = clap_markdown::help_markdown::<NargoCli>();
+    println!("{markdown}");
+    Ok(())
+}
diff --git a/tooling/acvm_cli/src/errors.rs b/tooling/acvm_cli/src/errors.rs
new file mode 100644
index 00000000000..035388d05f7
--- /dev/null
+++ b/tooling/acvm_cli/src/errors.rs
@@ -0,0 +1,52 @@
+use nargo::NargoError;
+use std::path::PathBuf;
+use thiserror::Error;
+
+#[derive(Debug, Error)]
+pub(crate) enum FilesystemError {
+    #[error(
+        " Error: cannot find {0} in expected location {1:?}.\n Please generate this file at the expected location."
+    )]
+    MissingTomlFile(String, PathBuf),
+    #[error(" Error: failed to parse toml file {0}.")]
+    InvalidTomlFile(String),
+    #[error(
+      " Error: cannot find {0} in expected location {1:?}.\n Please generate this file at the expected location."
+    )]
+    MissingBytecodeFile(String, PathBuf),
+
+    #[error(" Error: failed to read bytecode file {0}.")]
+    InvalidBytecodeFile(String),
+
+    #[error(" Error: failed to create output witness file {0}.")]
+    OutputWitnessCreationFailed(String),
+
+    #[error(" Error: failed to write output witness file {0}.")]
+    OutputWitnessWriteFailed(String),
+}
+
+#[derive(Debug, Error)]
+pub(crate) enum CliError {
+    /// Filesystem errors
+    #[error(transparent)]
+    FilesystemError(#[from] FilesystemError),
+
+    /// Error related to circuit deserialization
+    #[error("Error: failed to deserialize circuit")]
+    CircuitDeserializationError(),
+
+    /// Error related to circuit execution
+    #[error(transparent)]
+    CircuitExecutionError(#[from] NargoError),
+
+    /// Input Witness Value Error
+    #[error("Error: failed to parse witness value {0}")]
+    WitnessValueError(String),
+
+    /// Input Witness Index Error
+    #[error("Error: failed to parse witness index {0}")]
+    WitnessIndexError(String),
+
+    #[error(" Error: failed to serialize output witness.")]
+    OutputWitnessSerializationFailed(),
+}
diff --git a/tooling/acvm_cli/src/main.rs b/tooling/acvm_cli/src/main.rs
new file mode 100644
index 00000000000..33cadc73a7c
--- /dev/null
+++ b/tooling/acvm_cli/src/main.rs
@@ -0,0 +1,36 @@
+#![forbid(unsafe_code)]
+#![warn(unreachable_pub)]
+#![warn(clippy::semicolon_if_nothing_returned)]
+#![cfg_attr(not(test), warn(unused_crate_dependencies, unused_extern_crates))]
+
+mod cli;
+mod errors;
+
+use std::env;
+
+use tracing_appender::rolling;
+use tracing_subscriber::{fmt::format::FmtSpan, EnvFilter};
+
+fn main() {
+    // Setup tracing
+    if let Ok(log_dir) = env::var("ACVM_LOG_DIR") {
+        let debug_file = rolling::daily(log_dir, "acvm-log");
+        tracing_subscriber::fmt()
+            .with_span_events(FmtSpan::ACTIVE)
+            .with_writer(debug_file)
+            .with_ansi(false)
+            .with_env_filter(EnvFilter::from_default_env())
+            .init();
+    } else {
+        tracing_subscriber::fmt()
+            .with_span_events(FmtSpan::ACTIVE)
+            .with_ansi(true)
+            .with_env_filter(EnvFilter::from_env("NOIR_LOG"))
+            .init();
+    }
+
+    if let Err(report) = cli::start_cli() {
+        eprintln!("{report}");
+        std::process::exit(1);
+    }
+}
diff --git a/tooling/debugger/ignored-tests.txt b/tooling/debugger/ignored-tests.txt
index 231d4d897a9..854e284dd43 100644
--- a/tooling/debugger/ignored-tests.txt
+++ b/tooling/debugger/ignored-tests.txt
@@ -6,6 +6,7 @@ brillig_to_bytes_integration
 debug_logs
 double_verify_nested_proof
 double_verify_proof
+double_verify_proof_recursive
 modulus
 references
 scalar_mul
diff --git a/tooling/nargo/src/artifacts/contract.rs b/tooling/nargo/src/artifacts/contract.rs
index d928b09fcb9..020ce49662f 100644
--- a/tooling/nargo/src/artifacts/contract.rs
+++ b/tooling/nargo/src/artifacts/contract.rs
@@ -1,6 +1,6 @@
 use acvm::acir::circuit::Circuit;
 use noirc_abi::{Abi, ContractEvent};
-use noirc_driver::{CompiledContract, ContractFunction, ContractFunctionType};
+use noirc_driver::{CompiledContract, ContractFunction};
 use serde::{Deserialize, Serialize};
 
 use noirc_driver::DebugFile;
@@ -43,9 +43,9 @@ impl From<CompiledContract> for ContractArtifact {
 pub struct ContractFunctionArtifact {
     pub name: String,
 
-    pub function_type: ContractFunctionType,
+    pub is_unconstrained: bool,
 
-    pub is_internal: bool,
+    pub custom_attributes: Vec<String>,
 
     pub abi: Abi,
 
@@ -66,8 +66,8 @@ impl From<ContractFunction> for ContractFunctionArtifact {
     fn from(func: ContractFunction) -> Self {
         ContractFunctionArtifact {
             name: func.name,
-            function_type: func.function_type,
-            is_internal: func.is_internal,
+            is_unconstrained: func.is_unconstrained,
+            custom_attributes: func.custom_attributes,
             abi: func.abi,
             bytecode: func.bytecode,
             debug_symbols: func.debug,