RomeはLinterでありCompilerでありBundlerでありFormatterでありTesterでありMinifierであり……
ともかく、フロントエンドの統合ツールチェーンです。
これまでBabel、ESLint、webpack、Prettier、Jestなど様々なツールでバラバラに行っていた作業が、Romeひとつ使うだけで完結するようになるという、これまで何度再発明されてきたかわからない究極のフロントエンドツールです。
まあ、今のところ日本での知名度はさっぱりなのですがね。
で、これまでRomeはJavaScriptで書かれていたのですが、これをRustで書き直すことが発表されました。
以下はRome公式サイトの記事、Rome will be written in Rustの日本語訳です。
Rome will be written in Rust 🦀
RomeはJavaScriptで始まりましたが、それは我々のチームが選択した結果であり、またコミュニティの人たちが参加しやすくするための選択でもありました。
我々はJavaScriptとTypeScript(あとHTMLとCSSも)を愛しており、それらのために最高のツールを作りたいと考えています。
様々な理由から、その理想を求めるためにはRustに移動するのが最もよい選択であると判断しました。
また、移行ついでにRomeのアーキテクチャを根本的に変更するつもりです。
この変更によって柔軟性を増し、JavaScriptとWebの世界にこれまで存在しなかったツールが現れることでしょう。
Why write Rome in Rust?
何故Rustなのか?
多くの先人たちがRustのパフォーマンス、メモリ、安全性について語っています。
それらの主張は全て正しいのかもしれません。
しかし、我々が最も懸念していたのは、我々の生産性についてです。
我々はJavaScript開発者の小さなチームであり、今から全く新しい言語に移行して、既に運用されている複雑なツールの生産性を上げられるほどに習熟できるかはかなり疑問でした。
しかし、幾つかプロトタイプを作ってみた結果、Rustに移行した方が生産性が上がるにちがいないと感じました。
Romeで最初に決めたことのひとつが、サードパーティに依存するコードを使用しないということでした。
NodeのAPIを我々自身でラップしたほどです。
この決定は、パフォーマンス、メモリ、型安全性など様々な要因に対するため、Rome内部のコードをすべて我々自身で厳密に制御したいという意志から来るものでした。
しかし、これらの問題の多くは、Rustとそのコミュニティでは既に解決されていました。
Fewer tradeoffs for third-party dependencies
サードパーティに依存する際のトレードオフが少ない。
JavaScriptやnpmのライブラリの多くは、多くのユーザの需要に応えるために、我々には必要のない機能が詰め込まれることが多々あります。
そのためコードサイズやパフォーマンスが犠牲になることを余儀なくされます。
一方でRustのcrateは、我々のニーズに近しい思想になっています。
Correctness is built-in to the standard library and in most popular crates
標準ライブラリや有名crateの多くはCorrectnessがビルトインされている。1
我々はサードパーティJavaScriptライブラリを使用せず、Correctnessを求めた独自のAPIを使用していました。
一方RustとそのコミュニティはCorrectnessに重点を置いており、Romeで採用するにあたって心配するところはありませんでした。
Trait/Module system allows us to make better use of dependencies
Trait/Moduleシステムにより、依存関係をより適切に活用できる。
Rustのtraitは非常にパワフルであり、あらゆるデータをオーバーヘッドなしに抽象化することができます。
これにより、サードパーティのライブラリを深く結合することができます。
また、よりインクリメンタルなAPIをつくることができ、破壊的変更を行わずにより広い範囲を安全に公開することができます。
JavaScriptでサードパーティの依存関係を避けていた原因が、Rustでは当てはまらないことに気付きました。
トレードオフを考えることなく高度な依存関係を構築することで、より生産性を高め、より良い、そしてより速いRomeを実現することが可能になります。
What are we changing?
Rustのプロトタイピングを始めたついでにアーキテクチャを見直し、基本的な設計の多くを再検討しました。
非常に大きな変更を加えたいところが幾つもあることに、すぐに気が付きました。
コンパイラの仕組みを調べたことがある人なら、以下のようなモデルを知っているでしょう。
・ソースコード → Lexical Analysis → トークン
・トークン → Syntactic Analysis → 抽象構文木
・抽象構文木 → Various Transformations → 中間表現
・中間表現 → Code Generation → バイトコード・機械語
これはコンパイラがどのように動作するかについて解説するための良いモデルではありますが、実際のコンパイラはこれよりもはるかに複雑なことを行っています。
追加で必要となることはいくつもあります。
・開発者がコードを変更するたびに追加でビルドを行う
・コンパイラが処理を完了する前にコードの情報を要求する
・構文エラーがあっても処理を続けたうえでフィードバックを返す
JavaScriptやWebのコミュニティでは、これらの担当が複数のツールに分散されているため、それぞれのツールが同じことを少しづつ異なる方法で何度も再実装しています。
Romeはそれら全てのツールをRomeひとつだけにしたいと考えているので、異なるツールの全てに耐えうる基盤が必要なのです。
我々は、他のコンパイラがこれらの問題をどのように解決したのかを調べました。
すぐに気付いた重要な観点が、抽象構文木ASTに対するアプローチでした。
What’s the deal with Abstract Syntax Trees?
抽象構文木 is 何?
一般的には、そして具体的には我々のオリジナルデザインにとっては、抽象構文木は以下のような特徴を持つものです。
・完全かつ有効なプログラムでなければならない。
・ソースコードが変更されると全てを一から再構築する必要がある。
・複数回の変換がある場合は全ての段階において完全かつ有効なプログラムでなければならない。
・元のソースコードを復元することはできない。
# `let value = 42;`
Node(Program,
body: Node(StatementList, statements: [
Node(VariableDeclaration, kind: "let", declarations: [
Node(VariableDeclarator,
id: Node(BindingIdentifier, name: "value")
initializer: Node(NumericLiteral, value: 42)
)
])
])
)
これを字句解析後のトークンと比較すると、その違いがわかります。
・不完全あるいは無効なプログラムを表現することができる。
・他の場所にあるソースコードを壊すことなく一部分だけ変更が可能。
しかし、トークンを直接扱うことは、構文の複雑さをコードに押し付けることになります。
# `let value = 42;`
Token(LetKeyword, "let")
Token(Whitespace, " ")
Token(Identifier, "value")
Token(Whitespace, " ")
Token(Equals, "=")
Token(Whitespace, " ")
Token(NumericLiteral, "42")
Token(Semicolon, ";")
この2つの間にあるもうひとつの選択肢が、具象構文木 - Concrete Syntax Treeです。
具象構文木は抽象構文木に似ていますが、元のソースを復元するのに十分な情報を所持しています。
# `let value = 42;`
Node(Program,
body: Node(StatementList, statements: [
Node(VariableDeclaration,
kind: Token(LetKeyword, trailing_trivia: Token(Whitespace, " "))
declarations: [
Node(VariableDeclarator,
id: Node(BindingIdentifier,
name: "value",
trailing_trivia: Token(Whitespace, " ")
)
equals: Token(Equals, trailing_trivia: Token(Whitespace, " "))
initializer: Node(NumericLiteral, value: 42)
)
]
semicolon: Token(Semicolon)
)
])
)
とはいえこの具象構文木にも、やはりASTと同様の問題点があります。
相変わらず有効な構文である必要があり、また構文木を編集する際にオリジナルのソースコードを失いやすいです。
抽象構文木と具象構文木の両方の利点を再現するためには、これまでとは異なるデータ構造が必要になります。
・ノードが抽象構文木を表すツリーである。
・リテラルソースコードを表すレキシカルトークンを含む。
・抽象構文とリテラルソースコードを表すレキシカルトークンの両方を保持する。
・無効なプログラムや不完全なプログラムも表現できる。
・ツリーを編集しても壊れない。
結果として、枝として構文木を持つ木であり、それぞれの枝は一貫したデータ構造を持つデータ構造となりました。
type Token(SyntaxKind, source_text: String)
type Node(SyntaxKind, children: List<Node | Token>)
このデータ構造は、走査することでプログラムの正確なソースを復元することが可能になります。
# `let value = 42;`
Node(VariableDeclaration, children: [
Token(LetKeyword, "let"),
Token(Whitespce, " "),
Node(VariableDeclarator, children: [
Node(BindingIdentifier, children: [
Token(Identifier, "value"),
Token(Whitespace, " "),
]),
Token(Equals, "="),
Token(Whitespace, " "),
Node(NumericLiteral, children: [
Token(NumericLiteral, "42"),
]),
]),
Token(Semicolon, ";"),
])
意味と構文の両方を同じツリーに表現したわけです。
Nodeからは構文がわかり、Tokenからはソースコードがわかります。
さらにこれを使いやすくするために、このデータ構造をASTに似た別のAPIで包んでおきます。
以下のコードはASTを扱ったことのある人にとっては馴染み深いものですが、裏では現在のNodeを継続的にチェックしており、有効で完全な構文のみを受け付けます。
fn visitor(node: Node) -> Option<_> {
// checks if the current node is a `VariableDeclarator` and returns if it's not
let variable_declarator = node.cast::<VariableDeclarator>()?;
// checks if the variable_declarator has a valid id field and returns if not
let id = variable_declarator.id()?;
// ...
}
CSTと、このASTっぽいAPIを組み合わせることで、無効なプログラムや不完全なプログラムも簡単に表現することができるようになり、さらにはツリーに直接エラーを書き込むこともできます。
type Error(String)
type Token(SyntaxKind, source_text: String)
type Node(SyntaxKind, children: List<Node | Token | Error>)
# `let value =`
Node(VariableDeclaration, children: [
Token(LetKeyword, "let")
Token(Whitespace, " ")
Node(VariableDeclarator, children: [
Node(BindingIdentifier, children: [
Token(Identifier, "value")
Token(Whitespace, " ")
])
Token(Equals, "=")
Error("Unexpected EOF") # << our program ends too early
])
])
エラーの検出を行わないかぎり、ツリーの操作や変更は変わらず可能です。
先述のvisitor
はVariableDeclarator.id
フィールドをチェックしているだけなので、エラーに遭遇することなく正常に処理が完了します。
もしエラーが発生した場合は早期returnすることで、visitor
は成功できなかったことを知ることができます。
このような特殊な構造のCSTはRed-Green Treeと呼ばれています。
C#/Roslyn
コンパイラチームによってつくられたもので、幾つかの言語で利用されています。
・C#/Visual Basic/Roslyn
・Rust Analyzer
・Swift
・RSLint for JavaScript
Rustについて検討した結果、このCSTを導入することで大きなメリットが生じることがわかりました。
それに伴いRomeを作る際のアプローチや、Romeが解決できる問題の範囲が大きく変わりました。
このCSTをベースとすることで、これまで不可能であったワールドクラスのコードエディタを提供することが可能になります。
So what’s next?
次の展開は?
我々は、Rustを使ってRomeを開発することにしました。
この数週間、実験と試作を繰り返しましたが、将来に向けて期待できることがたくさんあります。
プロトタイプの実装は終わりに近づいており、今後はこれをオープンにして作業を進めていきます。
我々は、JavaScriptとWebツールが長年にわたって直面してきた大きな課題に取り組み、その未来のための大きな基盤を構築していきます。
もしあなたがRustの開発者であり、このプロジェクトに参加したいと考えているのであれば是非連絡ください。
現在、Rustやコンパイラ、言語ツールに精通した開発者を募集しています。
感想
実物はまだできていません。
お気持ち表明です。
とはいえここまで大々的に発表したからには、多少の障害があったとしてもRustに移行することは間違いないでしょう。
JavaScriptの開発環境構築は本当に面倒臭くて死ぬので、これがRomeいっこいれるだけでさくっとどうにかなる、ってなるなら非常に楽でいいですね。
もっと言うとVSCでRome拡張機能をインストールしたら構築終わり、くらいまで行ってくれればいいんですけどね。
さあみんなもっとRomeを流行らすのだ。
なおASTとかCSTとかのあたりは、どうしてここで主張しているのか正直よくわからなかった。
-
Correctnessがどういう意味なのかいまいちよくわからない。 ↩