[request] Move `Visit` and `VisitMut` default implementations to another method
milesj opened this issue · comments
The Visit
and VisitMut
traits are great but there's 1 giant usability issue, and that is that each method defines its own implementation.
Now why is this a problem? Well, if I implement my own visitor and override a method, I lost the default implementation, and there's no easy way (besides dynamic dispatch below) to reuse the implementation, so I end up copying and pasting a bunch of code. For example, say I have my own visitor that needs to do something with Program
, I would do this:
pub struct Example<'a>;
impl<'a> Visit<'a> for Example<'a> {
fn visit_program(&mut self, program: &Program<'a>) {
for stmt in &program.body {
// Do something here
}
}
}
But I still want the visitor to work correctly, so then I have to copy the default code like so:
pub struct Example<'a>;
impl<'a> Visit<'a> for Example<'a> {
fn visit_program(&mut self, program: &Program<'a>) {
for stmt in &program.body {
// Do something here
}
// Default
let kind = AstKind::Program(self.alloc(program));
self.enter_scope({
let mut flags = ScopeFlags::Top;
if program.is_strict() {
flags |= ScopeFlags::StrictMode;
}
flags
});
self.enter_node(kind);
for directive in &program.directives {
self.visit_directive(directive);
}
self.visit_statements(&program.body);
self.leave_node(kind);
self.leave_scope();
}
}
You can sort of work around this be using dynamic dispatch, like the following.
pub struct Example<'a>;
impl<'a> Visit<'a> for Example<'a> {
fn visit_program(&mut self, program: &Program<'a>) {
for stmt in &program.body {
// Do something here
}
// Default
Visit::visit_program(self, program);
}
}
This works for some methods, but causes warnings for others. A warning I see often is "function cannot return without recursing". I'm not sure if this actually causes problems though.
Regardless, I have a suggestion here. Instead of defining a default implementation for methods, why not define another method like do_visit_program
that has the implementation, and then update visit_program
to call do_visit_program
. That way, when the method is overridden, we can still call it normally.
For example:
// Before
pub trait Visit<'a>: Sized {
fn visit_program(&mut self, program: &Program<'a>) {
let kind = AstKind::Program(self.alloc(program));
self.enter_scope({
let mut flags = ScopeFlags::Top;
if program.is_strict() {
flags |= ScopeFlags::StrictMode;
}
flags
});
self.enter_node(kind);
for directive in &program.directives {
self.visit_directive(directive);
}
self.visit_statements(&program.body);
self.leave_node(kind);
self.leave_scope();
}
}
// After
pub trait Visit<'a>: Sized {
fn do_visit_program(&mut self, program: &Program<'a>) {
let kind = AstKind::Program(self.alloc(program));
self.enter_scope({
let mut flags = ScopeFlags::Top;
if program.is_strict() {
flags |= ScopeFlags::StrictMode;
}
flags
});
self.enter_node(kind);
for directive in &program.directives {
self.visit_directive(directive);
}
self.visit_statements(&program.body);
self.leave_node(kind);
self.leave_scope();
}
fn visit_program(&mut self, program: &Program<'a>) {
self.do_visit_program(program);
}
}
I had a similar idea before, I need an enter_program
and a leave_program
method in the transformer
The implementation looks like this
pub trait VisitMut<'a>: Sized {
fn enter_program(&mut self, program: &mut Program<'a>) {
// do something
}
fn leave_program(&mut self, program: &mut Program<'a>) {
// After the program body is visited, we may need to modify the program body.
}
fn visit_program(&mut self, program: &mut Program<'a>) {
let kind = AstKind::Program(self.alloc(program));
self.enter_scope({
let mut flags = ScopeFlags::Top;
if program.is_strict() {
flags |= ScopeFlags::StrictMode;
}
flags
});
self.enter_node(kind);
// enter here
self.enter_program(program);
for directive in &program.directives {
self.visit_directive(directive);
}
self.visit_statements(&mut program.body);
// leave here
self.leave_program(program);
self.leave_node(kind);
self.leave_scope();
}
}
This is similar to { visitor: { program() {}, "program:exit"() {} }
in babel
Been tinkering around with this more, and the dynamic dispatch approach seems to always trigger a stack overflow.
Try:
- use the ModuleRecord data https://github.com/oxc-project/oxc/blob/main/crates/oxc_semantic/src/module_record/builder.rs
- use the enter / leave callbacks
oxc/crates/oxc_ast/src/visit.rs
Lines 15 to 16 in 8ab667c
This requires a lot of manual work, and given we have some workarounds, I'm going to close this as not planned for now.