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 ast
This 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 ast
Implement 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);