Абстрактное синтаксическое дерево (AST)
AST Oxc — основа всех инструментов. Его структура важна для парсера, линтера, трансформера и остальных частей.
Архитектура AST
Принципы
- Производительность — скорость и экономия памяти
- Типобезопасность — преимущества системы типов Rust
- Близость к спецификации ECMAScript
- Ясная семантика — меньше двусмысленностей, чем у других форматов AST
Работа с AST
Генерация кода по AST
После изменения описаний AST:
bash
just astГенерируется:
- Visitor для обхода
- Builder для построения узлов
- Реализации трейтов для типовых операций
- TypeScript-типы для привязок Node.js
Структура узла
Типичный узел:
rust
#[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>>,
}span: позиция в исходнике#[ast(visit)]: генерирует методы visitor'a: время жизни данных в арене
Память
AST живёт в арене:
rust
use oxc_allocator::Allocator;
let allocator = Allocator::default();
let ast = parser.parse(&allocator, source_text, source_type)?;Плюсы:
- быстрые аллокации без частых malloc
- освобождение всей арены одним drop
- дружелюбный к кэшу линейный доступ
- без подсчёта ссылок
Обход AST
Visitor
rust
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);
}
}
let mut visitor = MyVisitor;
visitor.visit_program(&program);Изменяемый visitor
Для трансформаций — VisitMut:
rust
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>) {
if expr.operator == BinaryOperator::Addition {
// изменить узел
}
walk_mut::walk_binary_expression_mut(self, expr);
}
}Построение AST
Builder
rust
use oxc_ast::AstBuilder;
let ast = AstBuilder::new(&allocator);
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,
);Вспомогательные методы
rust
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 })
))
}
}Рабочий процесс разработки
Новый узел AST
Описать struct:
rust#[ast(visit)] pub struct MyNewNode<'a> { pub span: Span, pub name: Atom<'a>, pub value: Expression<'a>, }Добавить в enum:
rustpub enum Statement<'a> { // ... MyNewStatement(Box<'a, MyNewNode<'a>>), }just astЛогика разбора:
rustimpl<'a> Parser<'a> { fn parse_my_new_node(&mut self) -> Result<MyNewNode<'a>> { // ... } }
Сравнение с другими AST
AST Explorer
Для сравнения парсеров: ast-explorer.dev
- Современный интерфейс и подсветка
- Актуальные версии парсеров
- Несколько движков: Oxc, Babel, TypeScript и др.
- Экспорт в JSON и генерация кода
Производительность
Раскладка в памяти
Компактные узлы предпочтительнее больших enum без boxing:
rust
struct CompactNode<'a> {
span: Span,
flags: u8,
name: Atom<'a>,
}
enum LargeEnum {
Small,
Large { /* много данных */ },
}Арена
Узлы создаются через арену (часто через макрос #[ast]):
rust
let node = self.ast.alloc(MyNode {
span: SPAN,
value: 42,
});Размер enum
На x86_64 проверяют компактность:
rust
#[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);
}Дополнительно
Свои атрибуты AST
rust
#[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>,
}Связь с семантикой
rust
#[ast(visit)]
pub struct IdentifierReference<'a> {
pub span: Span,
pub name: Atom<'a>,
#[ast(ignore)]
pub reference_id: Cell<Option<ReferenceId>>,
}Так инструменты получают привязки, области и типы при обходе.
Отладка
Вывод структуры
rust
println!("{:#?}", ast_node);Span
rust
let span = node.span();
println!("Error at {}:{}", span.start, span.end);