Árvore sintática abstrata (AST)
A AST do Oxc é a base de todas as ferramentas. Entender sua estrutura e como trabalhar com ela é essencial para contribuir com parser, linter, transformer e demais componentes.
Arquitetura da AST
Princípios de desenho
A AST do Oxc segue estes princípios:
- Desempenho em primeiro lugar: otimizada para velocidade e uso de memória
- Segurança de tipos: aproveita o sistema de tipos do Rust para evitar erros comuns
- Conformidade com a spec: alinhada à especificação ECMAScript
- Semântica clara: reduz ambiguidades presentes em outros formatos de AST
Trabalhando com a AST
Gerar código relacionado à AST
Quando alterar definições da AST, rode o gerador:
just astIsso gera:
- Padrões visitor: para percorrer a AST
- Métodos builder: para construir nós
- Implementações de traits: para operações comuns
- Tipos TypeScript: para bindings Node.js
Estrutura de um nó da AST
Todo nó segue um padrão consistente:
#[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>>,
}Componentes-chave:
span: posição no código-fonte#[ast(visit)]: gera métodos de visitor- Lifetime
'a: referências à memória alocada na arena
Gestão de memória
A AST usa arena para alocar com eficiência:
use oxc_allocator::Allocator;
let allocator = Allocator::default();
let ast = parser.parse(&allocator, source_text, source_type)?;Benefícios:
- Alocação rápida: sem
mallocpor nó - Liberação rápida: descarta a arena inteira de uma vez
- Amigável ao cache: layout linear na memória
- Sem RC: gestão simples por lifetime
Percursos na AST
Padrão Visitor
Use o visitor gerado para percorrer a AST:
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);Visitor mutável
Para transformações, use o visitor mutável:
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);
}
}Construção da AST
Padrão Builder
Use o builder da AST para criar nós:
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,
);Funções auxiliares
Padrões comuns vêm como helpers:
impl<'a> AstBuilder<'a> {
pub fn expression_numeric_literal(&self, span: Span, value: f64) -> Expression<'a> {
self.alloc(Expression::NumericLiteral(
self.alloc(NumericLiteral { span, value, raw: None })
))
}
}Fluxo de desenvolvimento
Adicionar novos nós à AST
Defina a struct:
rust#[ast(visit)] pub struct MyNewNode<'a> { pub span: Span, pub name: Atom<'a>, pub value: Expression<'a>, }Inclua no enum:
rustpub enum Statement<'a> { // ... existing variants MyNewStatement(Box<'a, MyNewNode<'a>>), }Rode a geração de código:
bashjust astImplemente o parsing:
rustimpl<'a> Parser<'a> { fn parse_my_new_node(&mut self) -> Result<MyNewNode<'a>> { // Parsing implementation } }
Comparar formatos de AST
AST Explorer
Para comparar com outros parsers, use ast-explorer.dev:
- Interface melhor: UI moderna com destaque de sintaxe
- Atualizado: versões recentes dos parsers
- Vários parsers: Oxc, Babel, TypeScript etc.
- Exportação: JSON, geração de código
Considerações de desempenho
Layout na memória
A AST privilegia eficiência de cache:
// 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 */ },
}Alocação na arena
Todos os nós vão para a arena:
// Automatically handled by #[ast] macro
let node = self.ast.alloc(MyNode {
span: SPAN,
value: 42,
});Testes de tamanho de enum
Impedimos enums grandes demais:
#[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);
}Tópicos avançados
Atributos customizados na AST
Adicione atributos para ferramentas específicas:
#[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>,
}Integração com análise semântica
Ligue nós da AST a informação semântica:
#[ast(visit)]
pub struct IdentifierReference<'a> {
pub span: Span,
pub name: Atom<'a>,
#[ast(ignore)]
pub reference_id: Cell<Option<ReferenceId>>,
}Assim as ferramentas acessam bindings, escopo e tipos durante o traverse.
Dicas de depuração
Pretty print
Use o formatter de debug para inspecionar a AST:
println!("{:#?}", ast_node);Informação de span
Rastreie posições para relatórios de erro:
let span = node.span();
println!("Error at {}:{}", span.start, span.end);