Abstract Syntax Tree (AST)
The Oxc AST is the foundation of all Oxc tools. Understanding its structure and how to work with it is essential for contributing to parser, linter, transformer, and other components.
AST Architecture
Design Principles
The Oxc AST is designed with the following principles:
- Performance First: Optimized for speed and memory efficiency
- Type Safety: Leverages Rust's type system to prevent common errors
- Spec Compliance: Closely follows ECMAScript specification
- Clear Semantics: Removes ambiguity present in other AST formats
Working with the AST
Generate AST Related Code
When you modify AST definitions, run the code generation tool:
just astThis generates:
- Visitor patterns: For traversing the AST
- Builder methods: For constructing AST nodes
- Trait implementations: For common operations
- TypeScript types: For Node.js bindings
AST Node Structure
Every AST node follows a consistent pattern:
#[ast(visit)]
pub struct FunctionDeclaration<'a> {
pub span: Span,
pub id: Option<BindingIdentifier<'a>>,
pub generator: bool,
pub r#async: bool,
pub params: FormalParameters<'a>,
pub body: Option<FunctionBody<'a>>,
pub type_parameters: Option<TSTypeParameterDeclaration<'a>>,
pub return_type: Option<TSTypeAnnotation<'a>>,
}Key components:
span: Source location information#[ast(visit)]: Generates visitor methods- Lifetime
'a: References to arena-allocated memory
Memory Management
The AST uses a memory arena for efficient allocation:
use oxc_allocator::Allocator;
let allocator = Allocator::default();
let ast = parser.parse(&allocator, source_text, source_type)?;Benefits:
- Fast allocation: No individual malloc calls
- Fast deallocation: Drop entire arena at once
- Cache friendly: Linear memory layout
- No reference counting: Simple lifetime management
AST Traversal
Visitor Pattern
Use the generated visitor for AST traversal:
use oxc_ast::visit::{Visit, walk_mut};
struct MyVisitor;
impl<'a> Visit<'a> for MyVisitor {
fn visit_function_declaration(&mut self, func: &FunctionDeclaration<'a>) {
println!("Found function: {:?}", func.id);
walk_mut::walk_function_declaration(self, func);
}
}
// Usage
let mut visitor = MyVisitor;
visitor.visit_program(&program);Mutable Visitor
For transformations, use the mutable visitor:
use oxc_ast::visit::{VisitMut, walk_mut};
struct MyTransformer;
impl<'a> VisitMut<'a> for MyTransformer {
fn visit_binary_expression(&mut self, expr: &mut BinaryExpression<'a>) {
// Transform the expression
if expr.operator == BinaryOperator::Addition {
// Modify the AST node
}
walk_mut::walk_binary_expression_mut(self, expr);
}
}AST Construction
Builder Pattern
Use the AST builder for creating nodes:
use oxc_ast::AstBuilder;
let ast = AstBuilder::new(&allocator);
// Create a binary expression: a + b
let left = ast.expression_identifier_reference(SPAN, "a");
let right = ast.expression_identifier_reference(SPAN, "b");
let expr = ast.expression_binary_expression(
SPAN,
left,
BinaryOperator::Addition,
right,
);Helper Functions
Common patterns are provided as helpers:
impl<'a> AstBuilder<'a> {
pub fn expression_number_literal(&self, span: Span, value: f64) -> Expression<'a> {
self.alloc(Expression::NumericLiteral(
self.alloc(NumericLiteral { span, value, raw: None })
))
}
}Development Workflow
Adding New AST Nodes
Define the struct:
rust#[ast(visit)] pub struct MyNewNode<'a> { pub span: Span, pub name: Atom<'a>, pub value: Expression<'a>, }Add to enum:
rustpub enum Statement<'a> { // ... existing variants MyNewStatement(Box<'a, MyNewNode<'a>>), }Run code generation:
bashjust astImplement parsing logic:
rustimpl<'a> Parser<'a> { fn parse_my_new_node(&mut self) -> Result<MyNewNode<'a>> { // Parsing implementation } }
Comparing AST Formats
Use AST Explorer
For comparing with other parsers, use ast-explorer.dev:
- Better UI: Modern interface with syntax highlighting
- Up-to-date: Latest parser versions
- Multiple parsers: Compare Oxc, Babel, TypeScript, etc.
- Export formats: JSON, code generation
Performance Considerations
Memory Layout
The AST is designed for cache efficiency:
// Good: Compact representation
struct CompactNode<'a> {
span: Span, // 8 bytes
flags: u8, // 1 byte
name: Atom<'a>, // 8 bytes
}
// Avoid: Large enums without boxing
enum LargeEnum {
Small,
Large { /* 200 bytes of data */ },
}Arena Allocation
All AST nodes are allocated in the arena:
// Automatically handled by #[ast] macro
let node = self.ast.alloc(MyNode {
span: SPAN,
value: 42,
});Enum Size Testing
We enforce small enum sizes:
#[cfg(all(target_arch = "x86_64", target_pointer_width = "64"))]
#[test]
fn no_bloat_enum_sizes() {
use std::mem::size_of;
assert_eq!(size_of::<Statement>(), 16);
assert_eq!(size_of::<Expression>(), 16);
assert_eq!(size_of::<Declaration>(), 16);
}Advanced Topics
Custom AST Attributes
Add custom attributes for specific tools:
#[ast(visit)]
#[cfg_attr(feature = "serialize", derive(Serialize))]
pub struct MyNode<'a> {
#[cfg_attr(feature = "serialize", serde(skip))]
pub internal_data: u32,
pub public_field: Atom<'a>,
}Integration with Semantic Analysis
Link AST nodes with semantic information:
#[ast(visit)]
pub struct IdentifierReference<'a> {
pub span: Span,
pub name: Atom<'a>,
#[ast(ignore)]
pub reference_id: Cell<Option<ReferenceId>>,
}This allows tools to access binding information, scope context, and type information during AST traversal.
Debugging Tips
Pretty Printing
Use the debug formatter to inspect AST:
println!("{:#?}", ast_node);Span Information
Track source locations for error reporting:
let span = node.span();
println!("Error at {}:{}", span.start, span.end);