長い間、Robloxが選択した言語はLua 5.1でした。 当社が成長するに従い、より良いツールのサポートとさらに性能のいいバーチャルマシン(VM) への要求も高まりました。 具体的にいくつか挙げると、型チェッカー、新しいlinterフレームワーク、より高速なインタプリタを含むモダンな言語が提供してくれるだろうとプログラマーが期待する機能を包含する「Luau」(ルワウと発音)という名前の当社のLuaスタックを再構築する構想を、これに答えるために実行し始めました。
そのすべてを可能にするために、最初から当社のスタックのほとんどを書き直さなければなりませんでした。 問題は、Lua 5.1パーサがバイトコード生成と密接に結合していることで、それは当社のニーズにとっては不十分なのです。 より深い分析のためにASPをトラバースできるようにしたいと考えたので、そのシンタックスツリーを作り出すパーサが必要なのです。 そこから、そのAST上で当社が行いたい操作を自由に実行できます。
運が良かったのは、基本的なlintパスのためにのみ使われる既存のLua 5.1パーサがStudio内に転がっていました。 それによって、当社がそのパーサを採用し、Luau固有のシンタックスを認識するように拡張することが非常に簡単にできました。それが、微妙な方法で解析結果が変更されてしまうかもしれないリスクを最小化します。 Robloxにおける神聖な価値観の一つは後方互換性であるために、重要な詳細情報です。 何百万行ものLuaコードをすでに書いてありますが、これらを永遠に機能させ続けることに専念しています。
これらの要素を念頭に置くため、要件は明確です。 必要なのは
- バックトラッキングが必要な文法的な不規則さを回避
- 効果的なパーサ(構文解析機能)を持つ
- 前方互換性のあるシンタックス(構文論)を維持
- Lua 5.1との後方互換性を保持
シンプルに聞こえますよね?
型推論エンジンがどのようにシンタックス選択に影響したか
まずは、どのようにこの状況に到達したかに関する文脈がを理解する必要があります。 これらのシンタックスを選んだのは、大多数のプログラマーにとってすぐに馴染むものであり、実際に業界基準でもあったからです。 新しいことは何も学ぶ必要はありません。
Luauがこのような型注釈を書くのを許容するところがいくつもあります。
- local foo: string
- function add(x: number, y: number): number … end
- type Foo = (number, number) -> number
- local foo = bar as string
バインディングに注釈をつけるためにシンタックスを追加することは、型推論エンジンが意図した型をよりよく理解するのに非常に重要です。 Luaは、特定の言語においてほぼどのオペレータでもオーバーロードできる非常にパワフルな言語です。 何が何を意味するかの注釈をつける方法がない限り、x + yという式が数字を出すべきだということすら自信を持って言うことができません!
型キャスト式
TypeScriptで私たちが本当に気に入っているものは、型アサーションと呼ばれるものです。 これは、チェッカが確認するように基本的に追加の型情報をプログラムに追加する一つの方法です。 TypeScriptでは、シンタックスは
bar as string
運が悪いことに、当社がこれを試したところ、悪い意味で驚かされることになりました。これは、既存のコードを破壊したのです! 当社のユーザーのゲームの一つに、asと名付けられた関数がありました。 そのため、そのスクリプトは以下のようなスニペットに含まれていました。
local x = y
as(w, z) — 関数型の構文解析のときに予測された ‘->’ が得たのは <eof でした>
もう一つの難題がなければ、これを機能させることはおそらくできたでしょう。当社は一つのルックアヘッドトークンだけでパーサを機能させたかったのです。 性能は当社にとって重要であり、非常に効率的なパーサを書くことの中にバックトラッキングしなければならない量を最小限にすることが含まれています。 当社のパーサにとって、式が本当は何を意味するかを決めるために任意ではるばる前後へとスキャンしなければならないということは効率的ではありません。
また、TypeScriptはJavaScriptの自動セミコロン挿入ルールのおかげでこれが無料でできることに感謝したほうがいいということが判明しています。 このスニペットをTypeScriptまたはJavaScriptで書くとき、各行にセミコロンを挿入しますが、これは2つの異なる文として構文解析される原因となります。 一行の中であろうと、JavaScriptにあるasトークンでのシンタックスエラーですが、TypeScriptにおける有効な型アサーション式です。 Luaはこれをせず、セミコロンも強制しないため、それらが複数の行にまたがっていたとしても、各最長の文に対して構文解析を試みなければなりません。
let x = y
as(w, z)
Luauの元の型キャスト式は、性能は当社の望み通りでしたが後方互換性はありませんでした。 残念なことに、これはLuauがLua 5.1の上位集合であるという当社の約束を破ることになるため、特定の文脈においてはカッコを必要とするなどの追加制約なしではできないのです。
関数呼び出しにおける型引数
Luaの文法におけるもう一つの残念な詳細は、他の不確定要素を導入することなしに関数呼び出しでは型引数の追加ができないことです。
someFunction<A, B>(c) を返す
これは、2つの異なることを意味する可能性があります。
- someFunction < AとB > Cを評価し、そして結果を返します
- AとBの2つの型引数とcの引数のあるsomeFunctionを呼び出し、返します
この曖昧さは、リスト表現の文脈においてのみ起きます。 これは、どちらも事前にコンパイルできるという利点があるため、TypeScriptとC#ではあまり問題ではありません。 そのため、2つの選択肢のうちの1つに絞ってこの式の曖昧性を解消しようとするのに、両方ともいくつかのサイクルを経る余裕があります。
構文解析や型検査の時に発見的手法を適用するなど、同じことをできるように思えますが、実際はできません。 Lua 5.1は、動的にグローバルをどの環境にもインジェクションする能力があり、それは発見的手法を断ち切る可能性があります。 すべてのクライアントが解釈を開始するには、可能な限り素早くバイトコードを生成しなければならないため、当社には全くその利点はないのです。
型エイリアス文
この型エイリアス文をの構文解析は、すでに無効なLuaシンタックスなため、 変更点ではありません。
type Foo = number
当社がすることはシンプルなものです。 型のみに関しての構文解析のみになる主要な式の構文解析をし、それからその式の構文解析の結果に基づいてどうするかを決定します。
- もし、これが関数呼び出しなら、このexpression-as-statementをさらに構文解析しようとすることをやめます。
- それ以外は、もし次のトークンがコンマか=であったら、代入文を構文解析します。
上記に欠けているものは、非常に明らかです。 識別子が他の識別子によって導かれるブランチはありません。 それから、しなければならないのは式上のパターン照合だけです
- それは、識別子ですか?
- その識別子の名前は、「型」と同じですか?
- 次のトークンは、任意の識別子ですか?
はい。これで、状況依存キーワードにある後方互換性のあるシンタックスが得られます。
type Foo = number — type alias
type(x) — function call
type = {x = 1} — assignment
type.x = 2 — assignment
おまけのスニペットとして、文の文脈から構文解析していなかったため、これはまだLua 5.1と全く同じように構文解析をします。
local foo = type
bar = 1
学んだこと
ここで学んだことは、Luauのためのシンタックスを前方互換性があり、少なくとも状況依存構文解析パスがあるようにデザインしなければならないようだということです。 それは、パーサをバックトラックして、失敗した地点から違うことを試さなければならないような再検討の必要性をなくしてくれます。 それは、ソースコードの最後まで順調に進む速いパーサを与えてくれるという利点があるだけでなく、曖昧さをなくすためにその他の種類の段階を必要とすることなくASTを戻してくれます。
また、新しいシンタックスを追加するときには概して注意する必要があるという意味です。これは、必ずしも悪いことではありません。 よく考慮された言語は、デザイナーが長い目で取り組むことを要求します。
Robloxコーポレーションとこのブログは、いかなる企業もサービスも推奨も支持もしません。 また、このブログに含まれる情報の正確さや完全性について、いかなる保証または約束もしません。
このブログ記事は、元はRobloxテックブログ に掲載されたものです。