2023年11月、rust_ts_runtime で「Rust で TypeScript のランタイムを自分で書いたらどうなるか」を試した。連携というより、字句解析から実行まで一通り手書きしてみる勉強用リポ。
依存ライブラリは何も入れず(Cargo.toml の dependencies は空)、Rust の std だけで lexer → parser → AST → executor を順番に作っていった。
構成
src/main.rs がそのまま流れになっている。
utils::read_fileで.tsファイルを読むlexer::tokenizeでトークン列にparser::parseで AST にexcutor::execute(綴りミスもそのまま)で実行
let tokens = lexer::tokenize(&content);
match parser::parse(&tokens) {
Ok(ast) => match excutor::execute(&ast) {
Ok(_) => {}
Err(e) => { eprint!("Error: {}", e); std::process::exit(1); }
},
Err(e) => { eprint!("Error: {}", e); std::process::exit(1); }
}
ハマったところ
lexer の Token は最小限で、Identifier / Period / OpenParen / CloseParen / StringLiteral / Semicolon しか定義していない。要するに console.log('...'); を解釈して実行できれば勝ち、というところまで絞った。
console.log('Hello in Rust ts runtime.');
console.log('11111111111111111111');
入力の ts/main.ts がこれで、これを Rust 側がトークンに割って、console . log ( 文字列 ) ; と並べて、最後に標準出力に出す。フルの TypeScript を実装しようとすると沼なので、「ドットでメソッド呼び出し1個」だけ動くところまで に範囲を切ったのが、最後まで走り切れた理由だと思う。
文字列リテラルの読み取りで、開きクォートと同じ文字が来たら閉じる、というだけの素朴なループを書いた。エスケープとかは当然見ていない。
学び
個人開発に Rust を本格投入すると、ビルド・デプロイの複雑さが一気に上がるので得失は冷静に見たい。でも「言語処理系を手で書く」題材としての Rust は、enum と match が気持ちよくて思った以上に楽しかった。
lexer → parser → executor の型を一度自分で通しておくと、あとで他の小さい DSL やテンプレート言語を書きたくなったときに効きそう。フルの言語じゃなくて「自分の用途に絞った極小ランタイム」なら、こういう手書きで十分なケースもありそう。
おわり
console.log しか動かないランタイムだけど、字句解析から実行までを一周できたのは大きかった。Wasm 経由で TS と混ぜる話はまた別の機会に。