Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: nargo expand to show code after macro expansions #7613

Open
wants to merge 121 commits into
base: master
Choose a base branch
from

Conversation

asterite
Copy link
Collaborator

@asterite asterite commented Mar 6, 2025

Description

Problem

Resolves #7552

Summary

By pure coincidence, nargo expand works almost the same way as cargo expand:

  1. We type-check the code as usual
  2. All code, that which exists in source files and that which is created by macros, is stored in "def maps" and the node interner, so we can use those sources to rebuild the final code
  3. Because of this, comments (but not doc comments) are lost (this is the same in cargo expand)
  4. All of the generated code is printed to the output as a single string (again, exactly like cargo expand)

In a follow-up PR we could try to somehow put back comments (we can know where they are and insert them as we traverse the HIR).

For example, if you run it for this code:

fn main() {
    let _ = std::as_witness(1);
    println("Hello world");
}

#[foo]
comptime fn foo(f: FunctionDefinition) -> Quoted {
    quote {
        pub fn bar(x: i32) -> i32  {  
            let y = x + 1;
            y + 2
        }
    }
}

#[mutate_add_one]
fn add_one() {}

comptime fn mutate_add_one(f: FunctionDefinition) {
    f.set_parameters(&[(quote { x }, quote { Field }.as_type())]);
    f.set_return_type(quote { Field }.as_type());
    f.set_body(quote { x + 1 }.as_expr().unwrap());
}

we get this output:

fn main() {
    let _: () = std::as_witness(1);
    println("Hello world");
}

comptime fn foo(f: FunctionDefinition) -> Quoted {
    quote {
        pub fn bar(x: i32) -> i32 {
            let y = x + 1;
            y + 2
        }
    }
}

pub fn bar(x: i32) -> i32 {
    let y: i32 = x + 1;
    y + 2
}

fn add_one(x: Field) -> Field {
    x + 1
}

comptime fn mutate_add_one(f: FunctionDefinition) {
    f.set_parameters(&[(quote { x }, quote { Field }.as_type())]);
    f.set_return_type(quote { Field }.as_type());
    f.set_body(quote { x + 1 }.as_expr().unwrap());
}

Additional Context

Documentation

Check one:

  • No documentation needed.
  • Documentation included in this PR.
  • [For Experimental Features] Documentation to be submitted in a separate PR.

PR Checklist

  • I have tested the changes locally.
  • I have formatted the changes with Prettier and/or cargo fmt on default settings.

@jfecher
Copy link
Contributor

jfecher commented Mar 6, 2025

Eventually a directory with all the expanded code could be created, with one file per module to match the original source code.

How would we do this though? The reason we only have a CLI flag currently and only output the new code is that while we're elaborating we are losing information about the source code. We throw away all type and trait definitions for example. By monomorphization we're already left with only functions. How would we output any types/traits/imports/etc back into the program? I don't think just skipping past them is an option either since metaprogramming may modify a type definition such that it differs from the source.

@asterite
Copy link
Collaborator Author

asterite commented Mar 7, 2025

How would we do this though?

Oh, I just meant that if we have a function foo defined in src/main.nr we know that its location is in that file. Then we'd output it (after it has been potentially changed) in the same file, but a different directory. For code generated by macros... it would land on the file that has the macro attribute, I think.

We throw away all type and trait definitions for example. How would we output any types/traits/imports/etc back into the program?

They are in the NodeInterner, for example get_trait, get_type, etc. Imports are a bit more tricky but they are there in a module's scope (probably the ones that aren't in definitions). It's still tricky because if we need to output a type or a call to a function we might need to either fully-qualify it or use an existing import, but I think it's doable.

By monomorphization we're already left with only functions.

Right, we can do this before monomoprhization (or put another way: we don't monomorphize for this tool)

@asterite
Copy link
Collaborator Author

I can't get past this wall. There's this code:

trait LibTrait<N> {
    fn broadcast();
    fn get_constant() -> Field;
}

pub struct StructA;

impl LibTrait<u32> for StructA {
    fn broadcast() {
        let _ = Self::get_constant();
    }

    fn get_constant() -> Field {
        1
    }
}

fn main() {}

In this call:

Self::get_constant()

Self::get_constant is a HirIdent that points to a function related to a trait. That HirIdent has an assumed impl kind... but I can't figure out how to turn that into Self or StructA as the only thing we know is that it points to a trait. I can't find where a reference to StructA is stored there in order to print "StructA" or "Self". A bunch of programs in test_programs fail with nargo expand because of this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add a tool to show code after macro expansions
2 participants