Skip to content

Абстрактное синтаксическое дерево (AST)

AST Oxc — основа всех инструментов. Его структура важна для парсера, линтера, трансформера и остальных частей.

Архитектура AST

Принципы

  1. Производительность — скорость и экономия памяти
  2. Типобезопасность — преимущества системы типов Rust
  3. Близость к спецификации ECMAScript
  4. Ясная семантика — меньше двусмысленностей, чем у других форматов 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

  1. Описать struct:

    rust
    #[ast(visit)]
    pub struct MyNewNode<'a> {
        pub span: Span,
        pub name: Atom<'a>,
        pub value: Expression<'a>,
    }
  2. Добавить в enum:

    rust
    pub enum Statement<'a> {
        // ...
        MyNewStatement(Box<'a, MyNewNode<'a>>),
    }
  3. just ast

  4. Логика разбора:

    rust
    impl<'a> Parser<'a> {
        fn parse_my_new_node(&mut self) -> Result<MyNewNode<'a>> {
            // ...
        }
    }

Сравнение с другими AST

AST Explorer

Для сравнения парсеров: ast-explorer.dev

  1. Современный интерфейс и подсветка
  2. Актуальные версии парсеров
  3. Несколько движков: Oxc, Babel, TypeScript и др.
  4. Экспорт в 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);