🧊

OXCのJS AST(Rust版)を眺める

長らく触ってはいつつも、なんとなくしか把握してなかったので、もう少しちゃんと見ておこうと思い。

その前にESTree

JavaScriptのASTといえば、やっぱデファクトのESTreeですよねということで先に。

estree/estree: The ESTree Spec https://github.com/estree/estree

ただここにある.mdは、ES世代ごとにファイルが分かれており、俯瞰するには不便・・・。

というわけで、ざっと眺めるにはTSの型で。

DefinitelyTyped/types/estree/index.d.ts at master · DefinitelyTyped/DefinitelyTyped https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/estree/index.d.ts

実際には利用するパーサー実装による(パーサー側で型定義持ってたりとか)けど、それはさておきほんと最小限って感じね。

その前にBabel

BabelのASTは、ESTreeのASTとちょっと違う。って書いてある。

汎用的なノードがより具体的なものになってるとのこと。

・ Literal token is replaced with StringLiteral, NumericLiteral, BigIntLiteral, BooleanLiteral, NullLiteral, RegExpLiteral ・ Property token is replaced with ObjectProperty and ObjectMethod ・ MethodDefinition is replaced with ClassMethod and ClassPrivateMethod ・ PropertyDefinition is replaced with ClassProperty and ClassPrivateProperty ・ PrivateIdentifier is replaced with PrivateName ・ Program and BlockStatement contain additional directives field with Directive and DirectiveLiteral ・ ClassMethod, ClassPrivateMethod, ObjectProperty, and ObjectMethod value property’s properties in FunctionExpression is coerced/brought into the main method node. ・ ChainExpression is replaced with OptionalMemberExpression and OptionalCallExpression ・ ImportExpression is replaced with a CallExpression whose callee is an Import node. This change will be reversed in Babel 8. ・ ExportAllDeclaration with exported field is replaced with an ExportNamedDeclaration containing an ExportNamespaceSpecifier node. https://babeljs.io/docs/babel-parser#output

そのほか、()のためのParenthesizedExpressionみたいな、ESTreeには存在しないノードもある。(当然、プラグインやらいろいろ有効にすると、もっとたくさんBabel独自のASTノードが生える)

babel/packages/babel-parser/ast/spec.md at main · babel/babel https://github.com/babel/babel/blob/main/packages/babel-parser/ast/spec.md

いい感じにまとまってるSpecがあるのは見やすくて良い。

OXCのASTは2種類ある

さて本題。

OXCのASTを利用すると言った場合、JSから使うのか、Rustから使うのかで、そのASTは少し違う。

元々はRustで書かれたパーサーなので、そのASTについてもRustで定義されてるけど、JSから使う場合は、ESTree互換(を目指してる)形式になる。

たとえば真偽値は、Rust版ではBabel同様にBooleanLiteralというstructになってるけど、JS版ではESTree同様にLiteralになるって感じ。(#[estree()]がその指定)

/// Boolean literal
///
/// <https://tc39.es/ecma262/#prod-BooleanLiteral>
#[ast(visit)]
#[derive(Debug, Clone)]
#[generate_derive(CloneIn, GetSpan, GetSpanMut, ContentEq, ESTree)]
#[estree(type = "Literal", via = crate::serialize::ESTreeLiteral, add_ts = "raw: string | null")]
pub struct BooleanLiteral {
    /// Node location in source code
    pub span: Span,
    /// The boolean value itself
    pub value: bool,
}

https://github.com/oxc-project/oxc/blob/405b73d8e72b92fb329d777dd157c78decf4f5c2/crates/oxc_ast/src/ast/literal.rs#L17C1-L29C2

OXCのAST(Rust版)

今度こそ本題。

RustのコードにおけるASTの定義はここにある。

https://github.com/oxc-project/oxc/blob/main/crates/oxc_ast/src/ast/js.rs https://github.com/oxc-project/oxc/blob/main/crates/oxc_ast/src/ast/literal.rs

で、Rust版のASTを触ってて最初に引っかかるのは、structenumの区別かなと。

たとえばBabelでは、Programノードのbodyの型はArray<Statement>になってて、このStatementはただのUnion型である。

interface Program extends BaseNode {
    type: "Program";
    body: Array<Statement>;
    directives: Array<Directive>;
    sourceType: "script" | "module";
    interpreter?: InterpreterDirective | null;
}

// ...

type Statement = BlockStatement | BreakStatement | ContinueStatement | DebuggerStatement // ...

つまり、必ずなんらかの具体的なノードの実体(という表現が正しいかはわからんけど)がそこにあるってこと。

一方、OXCのRust版では、Statementenumで定義されてて、さらには実装もちょっと持ってたりする。

https://github.com/oxc-project/oxc/blob/405b73d8e72b92fb329d777dd157c78decf4f5c2/crates/oxc_ast/src/ast/js.rs#L983 https://github.com/oxc-project/oxc/blob/405b73d8e72b92fb329d777dd157c78decf4f5c2/crates/oxc_ast/src/ast_impl/js.rs#L762

なので、この1クッションがあるってことを最初に理解しておかないと、延々とRustコンパイラに怒られることになる・・・。(自戒)

ASTノード一覧

そんなenumを除いたASTノードの顔ぶれはこのような感じ。並びは定義されてる順のまま、なんとなく行間で整理してみた状態。

BooleanLiteral
NullLiteral
NumericLiteral
StringLiteral
BigIntLiteral
RegExpLiteral

Program

IdentifierName
IdentifierReference
BindingIdentifier
LabelIdentifier

ThisExpression
ArrayExpression
Elision

ObjectExpression
ObjectProperty

TemplateLiteral
TaggedTemplateExpression
TemplateElement
TemplateElementValue

ComputedMemberExpression
StaticMemberExpression
PrivateFieldExpression

CallExpression
NewExpression
MetaProperty
SpreadElement

UpdateExpression
UnaryExpression
BinaryExpression
PrivateInExpression
LogicalExpression
ConditionalExpression
AssignmentExpression

ArrayAssignmentTarget
ObjectAssignmentTarget
AssignmentTargetRest
AssignmentTargetWithDefault
AssignmentTargetPropertyIdentifier
AssignmentTargetPropertyProperty

SequenceExpression
Super
AwaitExpression
ChainExpression
ParenthesizedExpression

Directive
Hashbang
BlockStatement

VariableDeclaration
VariableDeclarator

EmptyStatement
ExpressionStatement
IfStatement
DoWhileStatement
WhileStatement
ForStatement
ForInStatement
ForOfStatement
ContinueStatement
BreakStatement
ReturnStatement
WithStatement
SwitchStatement
SwitchCase
LabeledStatement
ThrowStatement
TryStatement
CatchClause
CatchParameter
DebuggerStatement

BindingPattern
AssignmentPattern
ObjectPattern
BindingProperty
ArrayPattern
BindingRestElement

Function
FormalParameters
FormalParameter
FunctionBody
ArrowFunctionExpression
YieldExpression

Class
ClassBody
MethodDefinition
PropertyDefinition
PrivateIdentifier
StaticBlock
AccessorProperty

ImportExpression

ImportDeclaration
ImportSpecifier
ImportDefaultSpecifier
ImportNamespaceSpecifier
WithClause
ImportAttribute
ExportNamedDeclaration
ExportDefaultDeclaration
ExportAllDeclaration
ExportSpecifier

BabelとESTreeを足して2で割ったって印象かな?Babelよりも具体的か。

Babelではオプションが必要だったParenthesizedExpressionも、デフォルトで出力されるようになってる。

あとはこういう記録も見つけた。

Grammar | The JavaScript Oxidation Compiler https://oxc.rs/docs/learn/ecmascript/grammar.html#assignmentpattern-vs-bindingpattern

大変そうだ・・・。