oxc / oxc_ast

This crate deserves a chapter of itself.

It is split in 3 parts:

  1. The structs that describe each node of the AST.
  2. A few implementations for those structs.
  3. Generated implementations under oxc_ast/src/generated generated by scripts in tasks/ast_tools/src, based on decorators like #[ast(...)]

A few concepts/keywords to be known:

The Oxc AST differs slightly from the estree AST by removing ambiguous nodes and introducing distinct types. For example, instead of using a generic estree Identifier, the Oxc AST provides specific types such as BindingIdentifier, IdentifierReference, and IdentifierName. This clear distinction greatly enhances the development experience by aligning more closely with the ECMAScript specification.

  • AST: Abstract Syntax Tree
  • estree: one of the standard for representing an AST for JavaScript programs
    • There are other kinds of ASTs

Structs

The oxc_ast module exposes multiple structs such as Program, IdentifierName, ObjectProperty ...

They represent each node of the AST of a modern JavaScript program. Since oxc supports jsx and TypeScript by default, those nodes specific to their syntax are also present, like: JSXElement, JSXFragment, JSXExpression, TSEnumDeclaration, TSUnionType ...

https://github.com/oxc-project/oxc/tree/main/crates/oxc_ast/src/ast

Implementations

A few implementations specific to each of these structs are handcoded in crates/oxc_ast/src/ast_impl.

Generated implementations

You can read this comment on top of the structs

#![allow(unused)]
fn main() {
// NB: `#[span]`, `#[scope(...)]`,`#[visit(...)]` and `#[generate_derive(...)]` do NOT do anything to the code.
// They are purely markers for codegen used in `tasks/ast_tools` and `crates/oxc_traverse/scripts`. See docs in those crates.
// Read [`macro@oxc_ast_macros::ast`] for more information.
}

Here is an example of structs where you can see some macros applied:

#![allow(unused)]
fn main() {
#[ast(visit)]
#[derive(Debug, Clone)]
#[generate_derive(CloneIn, GetSpan, GetSpanMut, ContentEq, ContentHash, ESTree)]
#[estree(type = "Identifier")]
pub struct BindingIdentifier<'a> {
    pub span: Span,
    pub name: Atom<'a>,
    #[estree(skip)]
    #[clone_in(default)]
    pub symbol_id: Cell<Option<SymbolId>>,
}
}

Like said in the comment above, those macros don't really do anything by themselves (like regular macros would), they are markers for the internal tool tasks/ast_tools which:

  • goes through the structs using the macros above, in different crates like oxc_ast, oxc_regular_expression, oxc_span, oxc_syntax ...
  • generate the implementations for those structs in a ./generated folder for those structs

The generators are located in tasks/ast_tools/src.

Why was it done like that, instead of regular use of macros ?

Because of performance and maintenance reasons. See the following links for more informations: