Работа с типами AST в Rust

В основе работы SWC лежит строго типизированное представление синтаксического дерева программы. Abstract Syntax Tree (AST) формализует структуру JavaScript и TypeScript кода в виде набора вложенных узлов, где каждый узел соответствует языковой конструкции: выражению, оператору, объявлению или модулю. В Rust-реализации SWC AST определяется как набор структур и перечислений, сгенерированных и организованных в крейтах семейства swc_ecma_ast.

Каждый узел AST реализуется как struct или enum, где форма данных строго фиксирует допустимые варианты синтаксиса. Такой подход позволяет компилятору на этапе компиляции гарантировать корректность обработки дерева и исключить множество классов ошибок, характерных для динамических представлений.


Базовая структура AST: модули и узлы верхнего уровня

Корневой элемент программы в SWC представлен через Module:

pub struct Module {
    pub body: Vec<ModuleItem>,
    pub span: Span,
}

ModuleItem описывает элементы верхнего уровня:

pub enum ModuleItem {
    ModuleDecl(ModuleDecl),
    Stmt(Stmt),
}

Такое разделение позволяет унифицировать обработку ES-модулей и обычных инструкций. Любой файл JavaScript или TypeScript преобразуется в Module, даже если он фактически не содержит импортов или экспортов.

Span используется для хранения информации о позиции в исходном коде. Это ключевой элемент инфраструктуры SWC, обеспечивающий корректную трансформацию, sourcemaps и диагностику.


Выражения и операторы как основа AST

Центральный элемент AST — перечисление Expr, которое охватывает все возможные выражения языка:

pub enum Expr {
    Lit(Lit),
    Ident(Ident),
    Call(CallExpr),
    Binary(BinaryExpr),
    Arrow(ArrowExpr),
    Fn(FnExpr),
    Object(ObjectLit),
    Array(ArrayLit),
    Member(MemberExpr),
    Cond(CondExpr),
    Assign(AssignExpr),
}

Каждый вариант представляет отдельную категорию синтаксических конструкций. Например, BinaryExpr описывает бинарные операции:

pub struct BinaryExpr {
    pub left: Box<Expr>,
    pub op: BinaryOp,
    pub right: Box<Expr>,
    pub span: Span,
}

BinaryOp строго типизирован:

pub enum BinaryOp {
    Add,
    Sub,
    Mul,
    Div,
    EqEq,
    NotEq,
    StrictEq,
    StrictNotEq,
    And,
    Or,
}

Такой дизайн исключает необходимость строкового сравнения операторов и делает обработку выражений предсказуемой и быстрой.


Идентификаторы и привязка имён

Идентификаторы представлены структурой Ident:

pub struct Ident {
    pub sym: JsWord,
    pub optional: bool,
    pub span: Span,
}

JsWord используется как оптимизированный тип строк, основанный на интернировании. Это уменьшает потребление памяти при многократном использовании одинаковых идентификаторов.

Привязка имён в SWC AST не хранится напрямую в узлах. Вместо этого используется отдельная система анализа областей видимости (scope analysis), которая строит таблицы символов поверх AST.


Объявления функций и классов

Функции представлены несколькими уровнями абстракции. Основной тип:

pub struct FnExpr {
    pub function: Function,
}

Структура Function включает параметры, тело и модификаторы:

pub struct Function {
    pub params: Vec<Param>,
    pub body: Option<BlockStmt>,
    pub is_async: bool,
    pub is_generator: bool,
    pub span: Span,
}

Каждый параметр описывается отдельно:

pub struct Param {
    pub pat: Pat,
    pub span: Span,
}

Pat (pattern) является универсальной конструкцией для деструктуризации:

pub enum Pat {
    Ident(BindingIdent),
    Array(ArrayPat),
    Object(ObjectPat),
    Rest(RestPat),
    Assign(AssignPat),
}

Классы представлены через Class и ClassDecl, где учитываются наследование, методы и статические члены.


Устройство операторов и блоков

Инструкции языка представлены перечислением Stmt:

pub enum Stmt {
    Expr(ExprStmt),
    Decl(Decl),
    Block(BlockStmt),
    If(IfStmt),
    For(ForStmt),
    While(WhileStmt),
    Return(ReturnStmt),
}

Блок кода — это контейнер:

pub struct BlockStmt {
    pub stmts: Vec<Stmt>,
    pub span: Span,
}

Такое дерево обеспечивает единообразную обработку любых конструкций управления потоком через рекурсивный обход.


Модульная система и импорты

ESM-конструкции представлены через ModuleDecl:

pub enum ModuleDecl {
    Import(ImportDecl),
    ExportDecl(ExportDecl),
    ExportNamed(NamedExport),
    ExportDefaultDecl(ExportDefaultDecl),
}

Импорт:

pub struct ImportDecl {
    pub specifiers: Vec<ImportSpecifier>,
    pub src: Str,
    pub type_only: bool,
    pub span: Span,
}

Спецификаторы импорта:

pub enum ImportSpecifier {
    Named(ImportNamedSpecifier),
    Default(ImportDefaultSpecifier),
    Namespace(ImportStarAsSpecifier),
}

Такая модель позволяет точно различать формы импорта и корректно трансформировать их при транспиляции в CommonJS или другие форматы.


Посетители AST: паттерн Visitor в Rust

Для обхода дерева используется система трейтов Visit и Fold, реализованных в крейте swc_ecma_visit.

Базовый трейт обхода:

pub trait Visit {
    fn visit_expr(&mut self, n: &Expr) { ... }
    fn visit_stmt(&mut self, n: &Stmt) { ... }
}

Пример реализации:

impl Visit for MyVisitor {
    fn visit_expr(&mut self, n: &Expr) {
        match n {
            Expr::Call(call) => {
                // обработка вызова функции
            }
            _ => {}
        }
    }
}

Этот механизм обеспечивает эффективный рекурсивный обход дерева без необходимости вручную управлять стеком вызовов.


Мутации дерева через Fold

В отличие от Visit, трейт Fold используется для трансформации AST:

pub trait Fold {
    fn fold_expr(&mut self, n: Expr) -> Expr;
}

Пример:

impl Fold for MyTransformer {
    fn fold_expr(&mut self, n: Expr) -> Expr {
        match n {
            Expr::Binary(mut bin) => {
                bin.left = Box::new(self.fold_expr(*bin.left));
                bin.right = Box::new(self.fold_expr(*bin.right));
                Expr::Binary(bin)
            }
            _ => n,
        }
    }
}

Этот подход позволяет строить трансформационные пайплайны: минификация, транспиляция, преобразование синтаксиса.


Связь AST и системы типов TypeScript

SWC поддерживает TypeScript через расширенные узлы AST, включающие типовую информацию:

pub enum TsType {
    TsKeywordType(TsKeywordType),
    TsTypeRef(TsTypeRef),
    TsUnionOrIntersectionType(TsUnionOrIntersectionType),
}

Типы не смешиваются с выражениями JavaScript AST, но интегрируются через отдельные поля в декларациях:

pub struct TsTypeAnn {
    pub type_ann: Box<TsType>,
}

Такой дизайн позволяет отделять синтаксический анализ от типовой семантики, сохраняя возможность последующего анализа без усложнения базовой модели AST.


Span и система позиционирования

Каждый узел содержит Span:

pub struct Span {
    pub lo: BytePos,
    pub hi: BytePos,
}

Эта структура обеспечивает:

  • точное указание диапазона в исходном файле
  • поддержку sourcemap генерации
  • диагностику ошибок
  • корректные трансформации кода без потери контекста

Оптимизации представления AST в Rust

Rust-реализация SWC использует несколько ключевых оптимизаций:

  • Box<T> для уменьшения размера enum-ов
  • интернированные строки (JsWord)
  • минимизация аллокаций через переиспользование структур
  • разделение AST на модули (swc_ecma_ast, swc_ecma_parser, swc_ecma_transforms)

Такой дизайн позволяет SWC достигать производительности, сравнимой с низкоуровневыми компиляторами, при сохранении строгой типизации и безопасности памяти.


Взаимодействие AST с трансформационным пайплайном

AST в SWC является центральным объектом всех этапов обработки:

  1. Парсер (swc_ecma_parser) строит дерево
  2. Анализатор (scope, type systems) дополняет структуру
  3. Трансформеры (swc_ecma_transforms) изменяют AST
  4. Генератор (swc_ecma_codegen) преобразует AST обратно в код

Каждый этап работает с одними и теми же структурами Rust, что исключает необходимость сериализации промежуточных форм и повышает скорость обработки.


Расширяемость модели AST

Добавление новых конструкций в SWC требует расширения:

  • enum Expr или Stmt
  • соответствующих visitor-методов
  • трансформеров
  • генератора кода

Rust-система типов гарантирует, что все места использования нового узла будут обработаны на этапе компиляции, что критически важно для поддерживаемости большого компилятора.