Skip to content

Conversation

@xunilrj
Copy link
Contributor

@xunilrj xunilrj commented Dec 1, 2025

Description

This PR introduces some proc-macros to help create visitors to the typed tree. Today, we have four main visitors that were developed using traits, each for its own purpose.

Their main issue is that they mutate their trees only sometimes. We do not have a way to know a priori if the visit will mutate anything or not. So we clone everything beforehand and pass a &mut. Needless to say that this is a huge waste. Cloning TyFunctionDecl is one of the biggest wastes, and we can improve up to 10% of compilation performance by avoiding these clones.

Another issue is that being trait-based, once we change the trait signature for whatever reason, we need to change dozens and dozens of files, just to make the project compile and check if the change works or not. That completely breaks the development loop.

Depending on the change, bugs can be introduced and are very, very hard to find.

To try to alleviate this, this PR introduces the concept of purpose-free visit. The macro will generate a general enough visit that will traverse the tree and allow a "visitor" to do whatever it wants.

We only need to start with the derive macro.

#[derive(Clone, Debug, Serialize, Deserialize, Visit)]
pub struct TyFunctionDecl {
...
}

The generated code is tailored for mutation. For occasional mutation.

And it is also intentionally "non-infectious". This means that decorating one struct with "Visit" does not mandate others to also be derived, like happens with Debug, for example. This means that simply deriving "Visit" in one struct will not traverse the whole tree of objects.

The generated code will be something like:

impl TyFunctionDecl {
    pub fn visit<V: crate::semantic_analysis::Visitor>(
        s: &mut std::borrow::Cow<Self>,                              // <-------------------- "self" will be Cow
        visitor: &mut V,
    ) {
        let Self {
...
                return_type, // <-------------------------------------  all fields will be like this
...
        } = s.as_ref();

        let mut has_changes: bool = false;
        
...
        let mut return_type = std::borrow::Cow::Borrowed(return_type);          // field is projected to Cow also
        visitor.visit_generic_type_argument(&mut return_type);                         // visitor is called (and not Type::visit)
        has_changes |= matches!(return_type, std::borrow::Cow::Owned(_)); // check changes
        <GenericTypeArgument>::visit(&mut return_type, visitor);                    // now Type::visit is called. This traverse the tree
        has_changes |= matches!(return_type, std::borrow::Cow::Owned(_)); // check changes
...

        // only mutate if any field changes
        if has_changes {
            *s = std::borrow::Cow::Owned(Self {
...
                return_type: return_type.into_owned(), // <-------------------------------------  all fields will be like this.
...
            })
        }
    }
}

Each field behaviour can be configured with "call_visit", "do_not_call_visit", "call_visitor", "do_not_call_visitor", "iter_visit", "iter_visitor", "skip", "optional".

For example:

#[visit(iter_visit, do_not_call_visitor)]
pub type_parameters: Vec<TypeParameter>,
#[visit(call_visit)]
pub return_type: GenericTypeArgument,

The other missing piece is the visitor which is built in two steps. We first generate one generate Visitor trait. Which will be used by all visitors:

generate_visitor! {
    ...
}

And each specific visitor can be built like:

pub struct ReplaceTypesVisitor<'ctx, 'eng, 'tsm> {
    pub ctx: &'ctx SubstTypesContext<'eng, 'tsm>,
}

impl<'ctx, 'eng, 'tsm> Visitor for ReplaceTypesVisitor<'ctx, 'eng, 'tsm> {
    const VISIT_GENERIC_TYPE_ARGUMENT_INITIAL_TYPE_ID: bool = false;

    fn visit_type_id<'a>(&mut self, type_id: &'a TypeId) -> Cow<'a, TypeId> {
        ...
    }
}

Checklist

  • I have linked to any relevant issues.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have updated the documentation where relevant (API docs, the reference, and the Sway book).
  • I have added tests that prove my fix is effective or that my feature works.
  • I have added (or requested a maintainer to add) the necessary Breaking* or New Feature labels where relevant.
  • I have done my best to ensure that my PR adheres to the Fuel Labs Code Review Standards.
  • I have requested a review from the relevant team or maintainers.

@xunilrj xunilrj self-assigned this Dec 1, 2025
@codspeed-hq
Copy link

codspeed-hq bot commented Dec 3, 2025

CodSpeed Performance Report

Merging #7505 will not alter performance

Comparing xunilrj/sway-macros-visit (da7f414) with master (c73137d)

Summary

✅ 25 untouched

@xunilrj xunilrj deployed to fuel-sway-bot December 5, 2025 13:14 — with GitHub Actions Active
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.

2 participants