feat(ast_codegen): add visit generator (#3954)

~~The generated code is only here for the sake of my own comparison (instead of manually keeping a backup of the old generated file). I would clean this up as soon as it is ready, submit some parts of it as the down stack, and stack the actual generated code on top of this. So please don't let the huge diff distract you, It won't have many conflicts since almost all of these are the generated visit code, which is completely contained to its own module(other than some minor renaming refactors).~~

The order of function definitions is a bit different, I've used a depth-first search, We can switch to a breadth-first one to align functions more closely to the original.
This commit is contained in:
rzvxa 2024-07-02 10:18:45 +00:00
parent 1df6ac0427
commit 7538af12d8
13 changed files with 874 additions and 11 deletions

1
Cargo.lock generated
View file

@ -1337,6 +1337,7 @@ dependencies = [
name = "oxc_ast_codegen" name = "oxc_ast_codegen"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"convert_case",
"itertools 0.13.0", "itertools 0.13.0",
"lazy_static", "lazy_static",
"prettyplease", "prettyplease",

View file

@ -78,6 +78,7 @@ pub enum Expression<'a> {
ChainExpression(Box<'a, ChainExpression<'a>>) = 16, ChainExpression(Box<'a, ChainExpression<'a>>) = 16,
ClassExpression(Box<'a, Class<'a>>) = 17, ClassExpression(Box<'a, Class<'a>>) = 17,
ConditionalExpression(Box<'a, ConditionalExpression<'a>>) = 18, ConditionalExpression(Box<'a, ConditionalExpression<'a>>) = 18,
#[visit_args(flags = None)]
FunctionExpression(Box<'a, Function<'a>>) = 19, FunctionExpression(Box<'a, Function<'a>>) = 19,
ImportExpression(Box<'a, ImportExpression<'a>>) = 20, ImportExpression(Box<'a, ImportExpression<'a>>) = 20,
LogicalExpression(Box<'a, LogicalExpression<'a>>) = 21, LogicalExpression(Box<'a, LogicalExpression<'a>>) = 21,
@ -250,6 +251,7 @@ pub enum ArrayExpressionElement<'a> {
/// <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Trailing_commas#arrays> /// <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Trailing_commas#arrays>
Elision(Elision) = 65, Elision(Elision) = 65,
// `Expression` variants added here by `inherit_variants!` macro // `Expression` variants added here by `inherit_variants!` macro
// TODO: support for attributes syntax here so we can use `#[visit_as(ExpressionArrayElement)]`
@inherit Expression @inherit Expression
} }
} }
@ -967,6 +969,7 @@ pub struct BlockStatement<'a> {
#[cfg_attr(feature = "serialize", serde(untagged))] #[cfg_attr(feature = "serialize", serde(untagged))]
pub enum Declaration<'a> { pub enum Declaration<'a> {
VariableDeclaration(Box<'a, VariableDeclaration<'a>>) = 32, VariableDeclaration(Box<'a, VariableDeclaration<'a>>) = 32,
#[visit_args(flags = None)]
FunctionDeclaration(Box<'a, Function<'a>>) = 33, FunctionDeclaration(Box<'a, Function<'a>>) = 33,
ClassDeclaration(Box<'a, Class<'a>>) = 34, ClassDeclaration(Box<'a, Class<'a>>) = 34,
UsingDeclaration(Box<'a, UsingDeclaration<'a>>) = 35, UsingDeclaration(Box<'a, UsingDeclaration<'a>>) = 35,
@ -1291,6 +1294,7 @@ pub struct TryStatement<'a> {
pub span: Span, pub span: Span,
pub block: Box<'a, BlockStatement<'a>>, pub block: Box<'a, BlockStatement<'a>>,
pub handler: Option<Box<'a, CatchClause<'a>>>, pub handler: Option<Box<'a, CatchClause<'a>>>,
#[visit_as(FinallyClause)]
pub finalizer: Option<Box<'a, BlockStatement<'a>>>, pub finalizer: Option<Box<'a, BlockStatement<'a>>>,
} }
@ -1432,7 +1436,7 @@ pub struct BindingRestElement<'a> {
#[visited_node] #[visited_node]
#[scope( #[scope(
// TODO: `ScopeFlags::Function` is not correct if this is a `MethodDefinition` // TODO: `ScopeFlags::Function` is not correct if this is a `MethodDefinition`
flags(ScopeFlags::Function), flags(flags.unwrap_or(ScopeFlags::empty()) | ScopeFlags::Function),
strict_if(self.body.as_ref().is_some_and(|body| body.has_use_strict_directive())), strict_if(self.body.as_ref().is_some_and(|body| body.has_use_strict_directive())),
)] )]
#[derive(Debug)] #[derive(Debug)]
@ -1586,6 +1590,7 @@ pub struct Class<'a> {
pub decorators: Vec<'a, Decorator<'a>>, pub decorators: Vec<'a, Decorator<'a>>,
#[scope(enter_before)] #[scope(enter_before)]
pub id: Option<BindingIdentifier<'a>>, pub id: Option<BindingIdentifier<'a>>,
#[visit_as(ClassHeritage)]
pub super_class: Option<Expression<'a>>, pub super_class: Option<Expression<'a>>,
pub body: Box<'a, ClassBody<'a>>, pub body: Box<'a, ClassBody<'a>>,
pub type_parameters: Option<Box<'a, TSTypeParameterDeclaration<'a>>>, pub type_parameters: Option<Box<'a, TSTypeParameterDeclaration<'a>>>,
@ -1635,6 +1640,12 @@ pub struct MethodDefinition<'a> {
pub span: Span, pub span: Span,
pub decorators: Vec<'a, Decorator<'a>>, pub decorators: Vec<'a, Decorator<'a>>,
pub key: PropertyKey<'a>, pub key: PropertyKey<'a>,
#[visit_args(flags = Some(match self.kind {
MethodDefinitionKind::Get => ScopeFlags::GetAccessor,
MethodDefinitionKind::Set => ScopeFlags::SetAccessor,
MethodDefinitionKind::Constructor => ScopeFlags::Constructor,
MethodDefinitionKind::Method => ScopeFlags::empty(),
}))]
pub value: Box<'a, Function<'a>>, // FunctionExpression pub value: Box<'a, Function<'a>>, // FunctionExpression
pub kind: MethodDefinitionKind, pub kind: MethodDefinitionKind,
pub computed: bool, pub computed: bool,
@ -1946,9 +1957,11 @@ inherit_variants! {
#[cfg_attr(feature = "serialize", derive(Serialize, Tsify))] #[cfg_attr(feature = "serialize", derive(Serialize, Tsify))]
#[cfg_attr(feature = "serialize", serde(untagged))] #[cfg_attr(feature = "serialize", serde(untagged))]
pub enum ExportDefaultDeclarationKind<'a> { pub enum ExportDefaultDeclarationKind<'a> {
#[visit_args(flags = None)]
FunctionDeclaration(Box<'a, Function<'a>>) = 64, FunctionDeclaration(Box<'a, Function<'a>>) = 64,
ClassDeclaration(Box<'a, Class<'a>>) = 65, ClassDeclaration(Box<'a, Class<'a>>) = 65,
#[visit(ignore)]
TSInterfaceDeclaration(Box<'a, TSInterfaceDeclaration<'a>>) = 66, TSInterfaceDeclaration(Box<'a, TSInterfaceDeclaration<'a>>) = 66,
// `Expression` variants added here by `inherit_variants!` macro // `Expression` variants added here by `inherit_variants!` macro

View file

@ -802,7 +802,7 @@ pub enum TSTypePredicateName<'a> {
#[visited_node] #[visited_node]
#[scope( #[scope(
flags(ScopeFlags::TsModuleBlock), flags(ScopeFlags::TsModuleBlock),
strict_if(self.body.as_ref().is_some_and(|body| body.is_strict())), strict_if(self.body.as_ref().is_some_and(TSModuleDeclarationBody::is_strict)),
)] )]
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(feature = "serialize", derive(Serialize, Tsify))] #[cfg_attr(feature = "serialize", derive(Serialize, Tsify))]

View file

@ -22,7 +22,7 @@ pub fn visited_node(_args: TokenStream, input: TokenStream) -> TokenStream {
/// Dummy derive macro for a non-existent trait `VisitedNode`. /// Dummy derive macro for a non-existent trait `VisitedNode`.
/// ///
/// Does not generate any code, only purpose is to allow using `#[scope]` attr in the type def. /// Does not generate any code, only purpose is to allow using `#[scope]` attr in the type def.
#[proc_macro_derive(VisitedNode, attributes(scope))] #[proc_macro_derive(VisitedNode, attributes(scope, visit, visit_as, visit_args))]
pub fn visited_node_derive(_item: TokenStream) -> TokenStream { pub fn visited_node_derive(_item: TokenStream) -> TokenStream {
TokenStream::new() TokenStream::new()
} }

View file

@ -30,3 +30,4 @@ serde = { workspace = true, features = ["derive"] }
regex = { workspace = true } regex = { workspace = true }
prettyplease = { workspace = true } prettyplease = { workspace = true }
lazy_static = { workspace = true } lazy_static = { workspace = true }
convert_case = { workspace = true }

View file

@ -1,5 +1,5 @@
use super::{REnum, RStruct, RType}; use super::{REnum, RStruct, RType};
use crate::{schema::Inherit, TypeName}; use crate::{schema::Inherit, util::TypeExt, TypeName};
use quote::ToTokens; use quote::ToTokens;
use serde::Serialize; use serde::Serialize;
@ -95,7 +95,7 @@ impl From<&Inherit> for EnumInheritDef {
fn from(inherit: &Inherit) -> Self { fn from(inherit: &Inherit) -> Self {
match inherit { match inherit {
Inherit::Linked { super_, variants } => Self { Inherit::Linked { super_, variants } => Self {
super_name: super_.into(), super_name: super_.get_ident().as_ident().unwrap().to_string(),
variants: variants.iter().map(Into::into).collect(), variants: variants.iter().map(Into::into).collect(),
}, },
Inherit::Unlinked(_) => { Inherit::Unlinked(_) => {

View file

@ -8,7 +8,7 @@ use super::generated_header;
pub struct AstKindGenerator; pub struct AstKindGenerator;
const BLACK_LIST: [&str; 69] = [ pub const BLACK_LIST: [&str; 69] = [
"Expression", "Expression",
"ObjectPropertyKind", "ObjectPropertyKind",
"TemplateElement", "TemplateElement",
@ -102,6 +102,7 @@ impl Generator for AstKindGenerator {
let have_kinds: Vec<(Ident, Type)> = ctx let have_kinds: Vec<(Ident, Type)> = ctx
.ty_table .ty_table
.iter() .iter()
.filter(|it| it.borrow().visitable())
.filter_map(|maybe_kind| match &*maybe_kind.borrow() { .filter_map(|maybe_kind| match &*maybe_kind.borrow() {
kind @ (RType::Enum(_) | RType::Struct(_)) if kind.visitable() => { kind @ (RType::Enum(_) | RType::Struct(_)) if kind.visitable() => {
let ident = kind.ident().unwrap().clone(); let ident = kind.ident().unwrap().clone();

View file

@ -1,6 +1,7 @@
mod ast; mod ast;
mod ast_kind; mod ast_kind;
mod impl_get_span; mod impl_get_span;
mod visit;
/// Inserts a newline in the `TokenStream`. /// Inserts a newline in the `TokenStream`.
#[allow(unused)] #[allow(unused)]
@ -42,3 +43,4 @@ pub(crate) use insert;
pub use ast::AstGenerator; pub use ast::AstGenerator;
pub use ast_kind::AstKindGenerator; pub use ast_kind::AstKindGenerator;
pub use impl_get_span::ImplGetSpanGenerator; pub use impl_get_span::ImplGetSpanGenerator;
pub use visit::VisitGenerator;

View file

@ -0,0 +1,668 @@
use std::{
borrow::Cow,
collections::{HashMap, HashSet},
iter::Cloned,
};
use convert_case::{Case, Casing};
use itertools::Itertools;
use proc_macro2::{TokenStream, TokenTree};
use quote::{format_ident, quote, ToTokens};
use syn::{
parenthesized,
parse::{Parse, ParseStream},
parse2, parse_quote,
punctuated::Punctuated,
spanned::Spanned,
token::Paren,
Arm, Attribute, Expr, Field, GenericArgument, Ident, Meta, MetaNameValue, Path, PathArguments,
Token, Type, Variant,
};
use crate::{
generators::{ast_kind::BLACK_LIST as KIND_BLACK_LIST, insert},
schema::{Inherit, REnum, RStruct, RType},
util::{StrExt, TokenStreamExt, TypeExt, TypeIdentResult},
CodegenCtx, Generator, GeneratorOutput, Result, TypeRef,
};
use super::generated_header;
pub struct VisitGenerator;
impl Generator for VisitGenerator {
fn name(&self) -> &'static str {
"VisitGenerator"
}
fn generate(&mut self, ctx: &CodegenCtx) -> GeneratorOutput {
let visit = (String::from("visit"), generate_visit(ctx));
GeneratorOutput::Many(HashMap::from_iter(vec![visit]))
}
}
fn generate_visit(ctx: &CodegenCtx) -> TokenStream {
let header = generated_header!();
// we evaluate it outside of quote to take advantage of expression evaluation
// otherwise the `\n\` wouldn't work!
let file_docs = insert! {"\
//! Visitor Pattern\n\
//!\n\
//! See:\n\
//! * [visitor pattern](https://rust-unofficial.github.io/patterns/patterns/behavioural/visitor.html)\n\
//! * [rustc visitor](https://github.com/rust-lang/rust/blob/master/compiler/rustc_ast/src/visit.rs)\n\
"};
let (visits, walks) = VisitBuilder::new(ctx).build();
quote! {
#header
#file_docs
insert!("#![allow(clippy::self_named_module_files, clippy::semicolon_if_nothing_returned, clippy::match_wildcard_for_single_variants)]");
endl!();
use oxc_allocator::Vec;
use oxc_syntax::scope::ScopeFlags;
endl!();
use crate::{ast::*, ast_kind::AstKind};
endl!();
use walk::*;
endl!();
/// Syntax tree traversal
pub trait Visit<'a>: Sized {
#[allow(unused_variables)]
fn enter_node(&mut self, kind: AstKind<'a>) {}
#[allow(unused_variables)]
fn leave_node(&mut self, kind: AstKind<'a>) {}
endl!();
#[allow(unused_variables)]
fn enter_scope(&mut self, flags: ScopeFlags) {}
fn leave_scope(&mut self) {}
endl!();
fn alloc<T>(&self, t: &T) -> &'a T {
insert!("// SAFETY:");
insert!("// This should be safe as long as `src` is an reference from the allocator.");
insert!("// But honestly, I'm not really sure if this is safe.");
#[allow(unsafe_code)]
unsafe {
std::mem::transmute(t)
}
}
#(#visits)*
}
endl!();
pub mod walk {
use super::*;
#(#walks)*
}
}
}
struct VisitBuilder<'a> {
ctx: &'a CodegenCtx,
visits: Vec<TokenStream>,
walks: Vec<TokenStream>,
cache: HashMap<Ident, [Option<Cow<'a, Ident>>; 2]>,
}
impl<'a> VisitBuilder<'a> {
fn new(ctx: &'a CodegenCtx) -> Self {
Self { ctx, visits: Vec::new(), walks: Vec::new(), cache: HashMap::new() }
}
fn build(mut self) -> (/* visits */ Vec<TokenStream>, /* walks */ Vec<TokenStream>) {
let program = {
let types: Vec<&TypeRef> =
self.ctx.ty_table.iter().filter(|it| it.borrow().visitable()).collect_vec();
TypeRef::clone(
types
.iter()
.find(|it| it.borrow().ident().is_some_and(|ident| ident == "Program"))
.expect("Couldn't find the `Program` type!"),
)
};
self.get_visitor(&program, false, None);
(self.visits, self.walks)
}
fn get_visitor(
&mut self,
ty: &TypeRef,
collection: bool,
visit_as: Option<&Ident>,
) -> Cow<'a, Ident> {
let cache_ix = usize::from(collection);
let (ident, as_type) = {
let ty = ty.borrow();
debug_assert!(ty.visitable(), "{ty:?}");
let ident = ty.ident().unwrap();
let as_type = ty.as_type().unwrap();
let ident = visit_as.unwrap_or(ident);
(ident.clone(), if collection { parse_quote!(Vec<'a, #as_type>) } else { as_type })
};
// is it already generated?
if let Some(cached) = self.cache.get(&ident) {
if let Some(cached) = &cached[cache_ix] {
return Cow::clone(cached);
}
}
let ident_snake = {
let it = ident.to_string().to_case(Case::Snake);
let it = if collection {
// edge case for `Vec<FormalParameter>` to avoid conflicts with `FormalParameters`
// which both would generate the same name: `visit_formal_parameters`.
// and edge case for `Vec<TSImportAttribute>` to avoid conflicts with
// `TSImportAttributes` which both would generate the same name: `visit_formal_parameters`.
if matches!(it.as_str(), "formal_parameter" | "ts_import_attribute") {
let mut it = it;
it.push_str("_list");
it
} else {
it.to_plural()
}
} else {
it
};
format_ident!("{it}")
};
let as_param_type = quote!(&#as_type);
let (extra_params, extra_args) = if ident == "Function" {
(quote!(, flags: Option<ScopeFlags>,), quote!(, flags))
} else {
(TokenStream::default(), TokenStream::default())
};
let visit_name = {
let visit_name = format_ident!("visit_{}", ident_snake);
if !self.cache.contains_key(&ident) {
debug_assert!(self.cache.insert(ident.clone(), [None, None]).is_none());
}
let cached = self.cache.get_mut(&ident).unwrap();
assert!(cached[cache_ix].replace(Cow::Owned(visit_name)).is_none());
Cow::clone(cached[cache_ix].as_ref().unwrap())
};
let walk_name = format_ident!("walk_{}", ident_snake);
self.visits.push(quote! {
endl!();
#[inline]
fn #visit_name (&mut self, it: #as_param_type #extra_params) {
#walk_name(self, it #extra_args);
}
});
// We push an empty walk first, because we evaluate - and generate - each walk as we go,
// This would let us to maintain the order of first visit.
let this_walker = self.walks.len();
self.walks.push(TokenStream::default());
let walk_body = if collection {
let singular_visit = self.get_visitor(ty, false, None);
quote! {
for el in it {
visitor.#singular_visit(el);
}
}
} else {
match &*ty.borrow() {
RType::Enum(enum_) => self.generate_enum_walk(enum_, visit_as),
RType::Struct(struct_) => self.generate_struct_walk(struct_, visit_as),
_ => panic!(),
}
};
// replace the placeholder walker with the actual one!
self.walks[this_walker] = quote! {
endl!();
pub fn #walk_name <'a, V: Visit<'a>>(visitor: &mut V, it: #as_param_type #extra_params) {
#walk_body
}
};
visit_name
}
fn generate_enum_walk(&mut self, enum_: &REnum, visit_as: Option<&Ident>) -> TokenStream {
let ident = enum_.ident();
let mut non_exhaustive = false;
let variants_matches = enum_
.item
.variants
.iter()
.filter(|it| !it.attrs.iter().any(|a| a.path().is_ident("inherit")))
.filter(|it| {
if it.attrs.iter().any(|a| {
a.path().is_ident("visit")
&& a.meta
.require_list()
.unwrap()
.parse_args::<Path>()
.unwrap()
.is_ident("ignore")
}) {
// We are ignoring some variants so the match is no longer exhaustive.
non_exhaustive = true;
false
} else {
true
}
})
.filter_map(|it| {
let typ = it
.fields
.iter()
.exactly_one()
.map(|f| &f.ty)
.map_err(|_| "We only support visited enum nodes with exactly one field!")
.unwrap();
let variant_name = &it.ident;
let typ = self.ctx.find(&typ.get_ident().inner_ident().to_string())?;
let borrowed = typ.borrow();
let visitable = borrowed.visitable();
if visitable {
let visit = self.get_visitor(&typ, false, None);
let (args_def, args) = it
.attrs
.iter()
.find(|it| it.path().is_ident("visit_args"))
.map(|it| it.parse_args_with(VisitArgs::parse))
.map(|it| {
it.into_iter()
.flatten()
.fold((Vec::new(), Vec::new()), Self::visit_args_fold)
})
.unwrap_or_default();
let body = quote!(visitor.#visit(it #(#args)*));
let body = if args_def.is_empty() {
body
} else {
// if we have args wrap the result in a block to prevent ident clashes.
quote! {{
#(#args_def)*
#body
}}
};
Some(quote!(#ident::#variant_name(it) => #body))
} else {
None
}
})
.collect_vec();
let inherit_matches = enum_.meta.inherits.iter().filter_map(|it| {
let Inherit::Linked { super_, .. } = it else { panic!("Unresolved inheritance!") };
let type_name = super_.get_ident().as_ident().unwrap().to_string();
let typ = self.ctx.find(&type_name)?;
if typ.borrow().visitable() {
let snake_name = type_name.to_case(Case::Snake);
let match_macro = format_ident!("match_{snake_name}");
let match_macro = quote!(#match_macro!(#ident));
// HACK: edge case till we get attributes to work with inheritance.
let visit_as = if ident == "ArrayExpressionElement"
&& super_.get_ident().inner_ident() == "Expression"
{
Some(format_ident!("ExpressionArrayElement"))
} else {
None
};
let to_child = format_ident!("to_{snake_name}");
let visit = self.get_visitor(&typ, false, visit_as.as_ref());
Some(quote!(#match_macro => visitor.#visit(it.#to_child())))
} else {
None
}
});
let matches = variants_matches.into_iter().chain(inherit_matches).collect_vec();
let with_node_events = |tk| {
let ident = visit_as.unwrap_or(ident);
if KIND_BLACK_LIST.contains(&ident.to_string().as_str()) {
tk
} else {
quote! {
let kind = AstKind::#ident(visitor.alloc(it));
visitor.enter_node(kind);
#tk
visitor.leave_node(kind);
}
}
};
let non_exhaustive = if non_exhaustive { Some(quote!(,_ => {})) } else { None };
with_node_events(quote!(match it { #(#matches),* #non_exhaustive }))
}
fn generate_struct_walk(&mut self, struct_: &RStruct, visit_as: Option<&Ident>) -> TokenStream {
let ident = visit_as.unwrap_or_else(|| struct_.ident());
let scope_attr = struct_.item.attrs.iter().find(|it| it.path().is_ident("scope"));
let (scope_enter, scope_leave) = scope_attr
.map(parse_as_scope)
.transpose()
.unwrap()
.map_or_else(Default::default, |scope_args| {
let cond = scope_args.r#if.map(|cond| {
let cond = cond.to_token_stream().replace_ident("self", &format_ident!("it"));
quote!(let scope_events_cond = #cond;)
});
let maybe_conditional = |tk: TokenStream| {
if cond.is_some() {
quote! {
if scope_events_cond {
#tk
}
}
} else {
tk
}
};
let flags = scope_args
.flags
.map_or_else(|| quote!(ScopeFlags::empty()), |it| it.to_token_stream());
let args = if let Some(strict_if) = scope_args.strict_if {
let strict_if =
strict_if.to_token_stream().replace_ident("self", &format_ident!("it"));
quote! {{
let mut flags = #flags;
if #strict_if {
flags |= ScopeFlags::StrictMode;
}
flags
}}
} else {
flags
};
let mut enter = cond.as_ref().into_token_stream();
enter.extend(maybe_conditional(quote!(visitor.enter_scope(#args);)));
let leave = maybe_conditional(quote!(visitor.leave_scope();));
(Some(enter), Some(leave))
});
let mut entered_scope = false;
let fields_visits: Vec<TokenStream> = struct_
.item
.fields
.iter()
.filter_map(|it| {
let (typ, typ_wrapper) = self.analyze_type(&it.ty)?;
let visit_as: Option<Ident> =
it.attrs.iter().find(|it| it.path().is_ident("visit_as")).map(|it| {
match &it.meta {
Meta::List(meta) => {
parse2(meta.tokens.clone()).expect("wrong `visit_as` input!")
}
_ => panic!("wrong use of `visit_as`!"),
}
});
// TODO: make sure it is `#[scope(enter_before)]`
let have_enter_scope = it.attrs.iter().any(|it| it.path().is_ident("scope"));
let args = it.attrs.iter().find(|it| it.meta.path().is_ident("visit_args"));
let (args_def, args) = args
.map(|it| it.parse_args_with(VisitArgs::parse))
.map(|it| {
it.into_iter()
.flatten()
.fold((Vec::new(), Vec::new()), Self::visit_args_fold)
})
.unwrap_or_default();
let visit = self.get_visitor(
&typ,
matches!(
typ_wrapper,
TypeWrapper::Vec | TypeWrapper::VecBox | TypeWrapper::OptVec
),
visit_as.as_ref(),
);
let name = it.ident.as_ref().expect("expected named fields!");
let mut result = match typ_wrapper {
TypeWrapper::Opt | TypeWrapper::OptBox | TypeWrapper::OptVec => quote! {
if let Some(ref #name) = it.#name {
visitor.#visit(#name #(#args)*);
}
},
TypeWrapper::VecOpt => quote! {
for #name in (&it.#name).into_iter().flatten() {
visitor.#visit(#name #(#args)*);
}
},
_ => quote! {
visitor.#visit(&it.#name #(#args)*);
},
};
if have_enter_scope {
assert!(!entered_scope);
result = quote! {
#scope_enter
#result
};
entered_scope = true;
}
if args_def.is_empty() {
Some(result)
} else {
// if we have args wrap the result in a block to prevent ident clashes.
Some(quote! {{
#(#args_def)*
#result
}})
}
})
.collect();
let body = if KIND_BLACK_LIST.contains(&ident.to_string().as_str()) {
let unused =
if fields_visits.is_empty() { Some(quote!(let _ = (visitor, it);)) } else { None };
quote! {
insert!("// NOTE: AstKind doesn't exists!");
#(#fields_visits)*
#unused
}
} else {
quote! {
let kind = AstKind::#ident(visitor.alloc(it));
visitor.enter_node(kind);
#(#fields_visits)*
visitor.leave_node(kind);
}
};
match (scope_enter, scope_leave, entered_scope) {
(_, Some(leave), true) => quote! {
#body
#leave
},
(Some(enter), Some(leave), false) => quote! {
#enter
#body
#leave
},
_ => body,
}
}
fn analyze_type(&self, ty: &Type) -> Option<(TypeRef, TypeWrapper)> {
fn analyze<'a>(res: &'a TypeIdentResult) -> Option<(&'a Ident, TypeWrapper)> {
let mut wrapper = TypeWrapper::None;
let ident = match res {
TypeIdentResult::Ident(inner) => inner,
TypeIdentResult::Box(inner) => {
wrapper = TypeWrapper::Box;
let (inner, inner_kind) = analyze(inner)?;
assert!(inner_kind == TypeWrapper::None,);
inner
}
TypeIdentResult::Vec(inner) => {
wrapper = TypeWrapper::Vec;
let (inner, inner_kind) = analyze(inner)?;
if inner_kind == TypeWrapper::Opt {
wrapper = TypeWrapper::VecOpt;
} else if inner_kind != TypeWrapper::None {
panic!();
}
inner
}
TypeIdentResult::Option(inner) => {
wrapper = TypeWrapper::Opt;
let (inner, inner_kind) = analyze(inner)?;
if inner_kind == TypeWrapper::Vec {
wrapper = TypeWrapper::OptVec;
} else if inner_kind == TypeWrapper::Box {
wrapper = TypeWrapper::OptBox;
} else if inner_kind != TypeWrapper::None {
panic!();
}
inner
}
TypeIdentResult::Reference(_) => return None,
};
Some((ident, wrapper))
}
let type_ident = ty.get_ident();
let (type_ident, wrapper) = analyze(&type_ident)?;
let type_ref = self.ctx.find(&type_ident.to_string())?;
if type_ref.borrow().visitable() {
Some((type_ref, wrapper))
} else {
None
}
}
fn visit_args_fold(
mut accumulator: (Vec<TokenStream>, Vec<TokenStream>),
arg: VisitArg,
) -> (Vec<TokenStream>, Vec<TokenStream>) {
let VisitArg { ident: id, value: val } = arg;
let val = val.to_token_stream().replace_ident("self", &format_ident!("it"));
accumulator.0.push(quote!(let #id = #val;));
accumulator.1.push(quote!(, #id));
accumulator
}
}
#[derive(PartialEq)]
enum TypeWrapper {
None,
Box,
Vec,
Opt,
VecBox,
VecOpt,
OptBox,
OptVec,
}
#[derive(Debug)]
struct VisitArgs(Punctuated<VisitArg, Token![,]>);
impl IntoIterator for VisitArgs {
type Item = VisitArg;
type IntoIter = syn::punctuated::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
#[derive(Debug)]
struct VisitArg {
ident: Ident,
value: Expr,
}
#[derive(Debug, Default)]
struct ScopeArgs {
r#if: Option<Expr>,
flags: Option<Expr>,
strict_if: Option<Expr>,
}
impl Parse for VisitArgs {
fn parse(input: ParseStream) -> std::result::Result<Self, syn::Error> {
input.parse_terminated(VisitArg::parse, Token![,]).map(Self)
}
}
impl Parse for VisitArg {
fn parse(input: ParseStream) -> std::result::Result<Self, syn::Error> {
let nv: MetaNameValue = input.parse()?;
Ok(Self {
ident: nv.path.get_ident().map_or_else(
|| Err(syn::Error::new(nv.span(), "Invalid `visit_args` input!")),
|it| Ok(it.clone()),
)?,
value: nv.value,
})
}
}
impl Parse for ScopeArgs {
fn parse(input: ParseStream) -> std::result::Result<Self, syn::Error> {
fn parse(input: ParseStream) -> std::result::Result<(String, Expr), syn::Error> {
let ident = if let Ok(ident) = input.parse::<Ident>() {
ident.to_string()
} else if input.parse::<Token![if]>().is_ok() {
String::from("if")
} else {
return Err(syn::Error::new(input.span(), "Invalid `#[scope]` input."));
};
let content;
parenthesized!(content in input);
Ok((ident, content.parse()?))
}
let parsed = input.parse_terminated(parse, Token![,])?;
Ok(parsed.into_iter().fold(Self::default(), |mut acc, (ident, expr)| {
match ident.as_str() {
"if" => acc.r#if = Some(expr),
"flags" => acc.flags = Some(expr),
"strict_if" => acc.strict_if = Some(expr),
_ => {}
}
acc
}))
}
}
fn parse_as_visit_args(attr: &Attribute) -> Vec<(Ident, TokenStream)> {
debug_assert!(attr.path().is_ident("visit_args"));
let mut result = Vec::new();
let args: MetaNameValue = attr.parse_args().expect("Invalid `visit_args` input!");
let ident = args.path.get_ident().unwrap().clone();
let value = args.value.to_token_stream();
result.push((ident, value));
result
}
fn parse_as_scope(attr: &Attribute) -> std::result::Result<ScopeArgs, syn::Error> {
debug_assert!(attr.path().is_ident("scope"));
if matches!(attr.meta, Meta::Path(_)) {
// empty!
Ok(ScopeArgs::default())
} else {
attr.parse_args_with(ScopeArgs::parse)
}
}

View file

@ -1,5 +1,7 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use syn::parse_quote;
use super::{CodegenCtx, Cow, Inherit, Itertools, RType, Result}; use super::{CodegenCtx, Cow, Inherit, Itertools, RType, Result};
pub trait Linker<'a> { pub trait Linker<'a> {
@ -75,19 +77,27 @@ pub fn linker(ty: &mut RType, ctx: &CodegenCtx) -> Result<bool> {
.map(|it| match it { .map(|it| match it {
Inherit::Unlinked(ref sup) => { Inherit::Unlinked(ref sup) => {
let linkee = ctx.find(&Cow::Owned(sup.to_string())).unwrap(); let linkee = ctx.find(&Cow::Owned(sup.to_string())).unwrap();
let variants = match &*linkee.borrow() { let linkee = linkee.borrow();
let inherit_value = format!(r#""{}""#, linkee.ident().unwrap());
let variants = match &*linkee {
RType::Enum(enum_) => { RType::Enum(enum_) => {
if enum_.meta.inherits.unresolved() { if enum_.meta.inherits.unresolved() {
return Err(it); return Err(it);
} }
enum_.item.variants.clone() enum_.item.variants.clone().into_iter().map(|mut v| {
v.attrs = vec![parse_quote!(#[inherit = #inherit_value])];
v
})
} }
_ => { _ => {
panic!("invalid inheritance, you can only inherit from enums and in enums.") panic!("invalid inheritance, you can only inherit from enums and in enums.")
} }
}; };
ty.item.variants.extend(variants.clone()); ty.item.variants.extend(variants.clone());
Ok(Inherit::Linked { super_: sup.clone(), variants }) Ok(Inherit::Linked {
super_: linkee.as_type().unwrap(),
variants: variants.collect(),
})
} }
Inherit::Linked { .. } => Ok(it), Inherit::Linked { .. } => Ok(it),
}) })

View file

@ -5,6 +5,7 @@ mod fmt;
mod generators; mod generators;
mod linker; mod linker;
mod schema; mod schema;
mod util;
use std::{ use std::{
borrow::Cow, borrow::Cow,
@ -22,7 +23,7 @@ use proc_macro2::TokenStream;
use syn::parse_file; use syn::parse_file;
use defs::TypeDef; use defs::TypeDef;
use generators::{AstGenerator, AstKindGenerator}; use generators::{AstGenerator, AstKindGenerator, VisitGenerator};
use linker::{linker, Linker}; use linker::{linker, Linker};
use schema::{Inherit, Module, REnum, RStruct, RType, Schema}; use schema::{Inherit, Module, REnum, RStruct, RType, Schema};
@ -191,6 +192,7 @@ fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
.with(AstGenerator) .with(AstGenerator)
.with(AstKindGenerator) .with(AstKindGenerator)
.with(ImplGetSpanGenerator) .with(ImplGetSpanGenerator)
.with(VisitGenerator)
.generate()?; .generate()?;
let output_dir = output_dir()?; let output_dir = output_dir()?;
@ -218,6 +220,17 @@ fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
file.write_all(span_content.as_bytes())?; file.write_all(span_content.as_bytes())?;
} }
{
// write `visit.rs` file
let output = outputs[VisitGenerator.name()].as_many();
let span_content = pprint(&output["visit"]);
let path = format!("{output_dir}/visit.rs");
let mut file = fs::File::create(path)?;
file.write_all(span_content.as_bytes())?;
}
cargo_fmt(".")?; cargo_fmt(".")?;
// let schema = serde_json::to_string_pretty(&schema).map_err(|e| e.to_string())?; // let schema = serde_json::to_string_pretty(&schema).map_err(|e| e.to_string())?;

View file

@ -27,7 +27,7 @@ pub struct Definitions {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Inherit { pub enum Inherit {
Unlinked(String), Unlinked(String),
Linked { super_: String, variants: Punctuated<Variant, Token![,]> }, Linked { super_: Type, variants: Punctuated<Variant, Token![,]> },
} }
impl From<Ident> for Inherit { impl From<Ident> for Inherit {

View file

@ -0,0 +1,154 @@
use itertools::Itertools;
use proc_macro2::{Group, TokenStream, TokenTree};
use quote::{quote, ToTokens};
use syn::{GenericArgument, Ident, PathArguments, Type, TypePath};
pub trait TokenStreamExt {
fn replace_ident(self, needle: &str, replace: &Ident) -> TokenStream;
}
pub trait TypeExt {
fn get_ident(&self) -> TypeIdentResult;
}
pub trait StrExt: AsRef<str> {
/// Dead simple, just adds either `s` or `es` based on the last character.
/// doesn't handle things like `sh`, `x`, `z`, etc. It also creates wrong results when the word
/// ends with `y` but there is a preceding vowl similar to `toys`,
/// It WILL output the WRONG result `toies`!
/// As an edge case would output `children` for the input `child`.
fn to_plural(self) -> String;
}
#[derive(Debug)]
pub enum TypeIdentResult<'a> {
Ident(&'a Ident),
Vec(Box<TypeIdentResult<'a>>),
Box(Box<TypeIdentResult<'a>>),
Option(Box<TypeIdentResult<'a>>),
Reference(Box<TypeIdentResult<'a>>),
}
impl<'a> TypeIdentResult<'a> {
fn boxed(inner: Self) -> Self {
Self::Box(Box::new(inner))
}
fn vec(inner: Self) -> Self {
Self::Vec(Box::new(inner))
}
fn option(inner: Self) -> Self {
Self::Option(Box::new(inner))
}
fn reference(inner: Self) -> Self {
Self::Reference(Box::new(inner))
}
pub fn inner_ident(&self) -> &'a Ident {
match self {
Self::Ident(it) => it,
Self::Vec(it) | Self::Box(it) | Self::Option(it) | Self::Reference(it) => {
it.inner_ident()
}
}
}
pub fn as_ident(&self) -> Option<&'a Ident> {
if let Self::Ident(it) = self {
Some(it)
} else {
None
}
}
}
impl TypeExt for Type {
fn get_ident(&self) -> TypeIdentResult {
match self {
Type::Path(TypePath { path, .. }) => {
let seg1 = path.segments.first().unwrap();
match &seg1.arguments {
PathArguments::None => TypeIdentResult::Ident(&seg1.ident),
PathArguments::AngleBracketed(it) => {
let args = &it.args.iter().collect_vec();
assert!(args.len() < 3, "Max path arguments here is 2, eg `Box<'a, Adt>`");
if let Some(second) = args.get(1) {
let GenericArgument::Type(second) = second else { panic!() };
let inner = second.get_ident();
if seg1.ident == "Box" {
TypeIdentResult::boxed(inner)
} else if seg1.ident == "Vec" {
TypeIdentResult::vec(inner)
} else {
panic!();
}
} else {
match args.first() {
Some(GenericArgument::Type(it)) => {
let inner = it.get_ident();
if seg1.ident == "Option" {
TypeIdentResult::option(inner)
} else {
inner
}
}
Some(GenericArgument::Lifetime(_)) => {
TypeIdentResult::Ident(&seg1.ident)
}
_ => panic!("unsupported type!"),
}
}
}
PathArguments::Parenthesized(_) => {
panic!("Parenthesized path arguments aren't supported!")
}
}
}
Type::Reference(typ) => TypeIdentResult::reference(typ.elem.get_ident()),
_ => panic!("Unsupported type."),
}
}
}
impl<T: AsRef<str>> StrExt for T {
fn to_plural(self) -> String {
let txt = self.as_ref();
if txt.is_empty() {
return String::default();
}
let mut txt = txt.to_string();
if txt.ends_with("child") {
txt.push_str("ren");
} else {
match txt.chars().last() {
Some('s') => {
txt.push_str("es");
}
Some('y') => {
txt.pop();
txt.push_str("ies");
}
_ => txt.push('s'),
}
}
txt
}
}
impl TokenStreamExt for TokenStream {
fn replace_ident(self, needle: &str, replace: &Ident) -> TokenStream {
self.into_iter()
.map(|it| match it {
TokenTree::Ident(ident) if ident == needle => replace.to_token_stream(),
TokenTree::Group(group) => {
Group::new(group.delimiter(), group.stream().replace_ident(needle, replace))
.to_token_stream()
}
_ => it.to_token_stream(),
})
.collect()
}
}