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 と混ぜる話はまた別の機会に。