TypeScriptに新しい演算子を追加してみた
※この記事はEEIC(東京大学工学部 電気電子工学科/電子情報工学科)3年の後期実験「大規模ソフトウェアを手探る」のレポートとして書かれています。
変数を比較するとき、[===]を使えばTrue/Falseを返してくれます。
しかしこれでオブジェクトを比較するときは、オブジェクトのアドレスを比較するだけなので、あまり役に立ちません。
人間の直観的には、aとbは同じです。できればTrueを返してほしい。
そこで、今回はオブジェクトの比較をいい感じにしてくれる新しい「====」演算子をTypeScriptのソース基盤に追加します。
目次
初期準備
前提知識:JavaScriptとの関係
TypeScriptとはJavaScriptをベースに、静的型付けやオブジェクト指向などいろいろな便利な機能を追加して使えるようにするために、Microsoft社が開発した言語である。tsコンパイラによって.tsファイルを.jsファイルに翻訳し、出来た.jsファイルを通常のjs環境で実行することでコードを走らせる。
最終的には.jsファイルに翻訳するので、例えば.tsファイルにJavaScriptの文法で書いてもなんの問題もなくそのまま実行される。つまりすべての.jsプログラムは有効な.tsプログラムであり、互換性がある。
インストールとビルド
インストール
TypeScript V4.5.0 -devをインストールした。
Microsoft/TypeScriptのmainブランチ:https://github.com/microsoft/TypeScript
ビルド
TypeScript フォルダ内のREADMEにしたがってビルドした。
推奨されているコマンドを使用すると、TypeScript/built/local内にビルドされる。
テスト実行
バージョン確認
デバッガ―の準備
-
「.vscodeフォルダ」内のlaunch.template.jsonファイルをコピーし、launch.jsonとして.vscodeフォルダ内に配置
-
下記bashコードによりデバッガーlisten状態にする
-
vscodeの虫マークのデバッグツールタグから、"Attach to VS Code TS Server via Port"を選択して実行。これはlaunch.jsonに記述されているもの。
-
コンパイラが動く

手順1:まず「====」がエラーを出すことなく.jsファイルに出力されるようにする
コンパイラは通常、「字句解析⇒構文解析⇒意味解析」という手順で動作する。
以下、その流れに沿って、必要な変更箇所を上げていく。
字句解析
1. Tokenの定義の追加
「===」という演算子が「EqualsEqualsEqualsToken」と定義されているのを発見した。そこでコード全体に対し、「EqualsEqualsEqualsToken」と検索すると、該当しそうな箇所がたくさん出てきた。これらの箇所において、「EqualsEqualsEqualsEqualsToken」を新たに追加する。
- src>lib>typescript.d.ts
.d.tsとはTypeScriptの型定義ファイルである。
これはlibフォルダ内のものなので、本命のcompilerとはあまり関係ないが、
ここにEqualsEqualsEqualsEqualsTokenを追加して、適当に400とした。
2.「====」の読み取り
構文解析:構文木が生成されるようにする
以上の過程で、良い感じに構文木が生成されるようになっているので、コンパイラのこの機能に対し手を加える必要はない。
意味解析:構文としてのエラーチェック
===を真似して、「EqualsEqualsEqualsEqualsToken」用の文を追加すればいい。
-
src>compiler>parser.ts :
- 5549行目 構文解析において、===の次に何が来ていいのかチェックする関数
-
src>services>classifier.ts
- 375行目 isBinaryExpressionOperatorToken(): 引数二つの演算子である場合、trueを返す
-
src>services>codefixs>interFromUsage.ts
-
src>compiler>factory>utilities.ts
- 963行目 isEqualityOperator()
-
src>compiler>utilities.ts
-
src>services>utilities.ts
ここまでやれば、以下のようなtest用のtsファイルを翻訳するのに成功するはず!
手順2:「====」という演算子を含む個所を、適切なJavaScript構文に置き換えられるようにする
ここまででjsファイルにエラーなく「====」を吐き出すことができるようになったがこれでは当然動くはずはなく、次はjsが認識できる形に書き直さなければならない。
書き直している箇所をどう手探ればいいか考えていたところ、Null合体演算子を追っていけばいいのではと気付いた。
Null合体演算子とはa ?? b
の形で表されるもので、a
がundefinedやnullであればb
を返し、そうでなければa
を返すものである。これはコンパイルするとa !== null && a !== void 0 ? a : b
の形に変換されるので、それが実際にどこで変換されるのかを見つければうまく活用できるはずである。
追っていくと、次の関数に行き着いた。
名前からして明らかにNull合体演算子に関わっていそうである。引数のnode
のoperatorToken
がQuestionQuestionToken
(??
のトークンか)であればこの関数にとび、新たなnode
を返すようである。ここで返されるnode
が、jsに変換されるときに生成されるコードに対応している。つまり、createConditionalExpression
は三項演算子に対応するnode
を作る関数で、nullでないという判定部分を作るcreateNotNullCondition
と??
の左右のnode
を渡している。(questionToken
とcolonToken
にundefinedを渡しているがそうするとcreateConditionalExpression
内でそれに対応するTokenが作られるようである。)
それなら====
でも、a ==== b
に対応するnode
を受け取って新たなnode
を返す関数を生成し、EqualsEqualsEqualsEqualsToken
のときにこの関数にとぶようにすればよさそうである。
では次にどのようなnodeを返すようにすればいいのか。そもそもどのようにして作ればいいのかというところに興味が移るが、createConditionalExpression
がどこでexportされているかを探してみると、nodeFactory.tsといういかにもなファイルにnode
を生成する関数がたくさんあることを発見した。
以下長いが、実際に作成した関数を示す。もし新しいクラスを作ったりするとなるとかなりの作業量になるのではと危惧していたが実際は既存の関数を組み合わせればよかったのでそこは安心した。
function transformObjectEqualsExpression(node: BinaryExpression){
let left = visitNode(node.left, visitor, isExpression);
let right = visitNode(node.right, visitor, isExpression);
if (!isSimpleCopiableExpression(left)) {
right = factory.createTempVariable(hoistVariableDeclaration);
left = factory.createAssignment(right, left);
}
const n = factory.createIdentifier("n");
const m = factory.createIdentifier("m");
const objectEquals = factory.createIdentifier("objectEquals")
const a = factory.createIdentifier("a");
const b = factory.createIdentifier("b");
const i = factory.createIdentifier("i");
const temp = factory.createTempVariable( undefined);
const lengthOfLeft = factory.createTempVariable( undefined);
const lengthOfRight = factory.createTempVariable( undefined);
const functionExpression = factory.createFunctionExpression(
undefined,
undefined,
objectEquals,
undefined,
[
factory.createParameterDeclaration(undefined,undefined,undefined,a),
factory.createParameterDeclaration(undefined,undefined,undefined,b),
factory.createParameterDeclaration(undefined,undefined,undefined,i)
],
undefined,
factory.createBlock([
factory.createVariableStatement(
undefined,
factory.createVariableDeclarationList([
factory.createVariableDeclaration(lengthOfLeft,undefined,undefined,factory.createNumericLiteral(0)),
factory.createVariableDeclaration(lengthOfRight,undefined,undefined,factory.createNumericLiteral(0))
])
),
factory.createIfStatement(
factory.createBinaryExpression(
i,
factory.createToken(SyntaxKind.EqualsEqualsToken),
factory.createNumericLiteral(100)
),
factory.createReturnStatement(
factory.createFalse()
)
),
factory.createForInStatement(
factory.createVariableDeclarationList([
factory.createVariableDeclaration(n)
]),
a,
factory.createBlock([
factory.createExpressionStatement(
factory.createPrefixIncrement(lengthOfLeft)
)
])
),
factory.createForInStatement(
factory.createVariableDeclarationList([
factory.createVariableDeclaration(m)
]),
b,
factory.createBlock([
factory.createExpressionStatement(
factory.createPrefixIncrement(lengthOfRight)
)
])
),
factory.createIfStatement(
factory.createBinaryExpression(
lengthOfLeft,
factory.createToken(SyntaxKind.ExclamationEqualsToken),
lengthOfRight
),
factory.createReturnStatement(
factory.createFalse()
)
),
factory.createForInStatement(
factory.createVariableDeclarationList([
factory.createVariableDeclaration(n)
]),
a,
factory.createBlock([
factory.createVariableStatement(
undefined,
factory.createVariableDeclarationList([
factory.createVariableDeclaration(temp,undefined,undefined,factory.createFalse())
])
),
factory.createForInStatement(
factory.createVariableDeclarationList([
factory.createVariableDeclaration(m)
]),
b,
factory.createBlock([
factory.createIfStatement(
factory.createLogicalAnd(
factory.createBinaryExpression(
n,
factory.createToken(SyntaxKind.EqualsEqualsToken),
m
),
factory.createLogicalAnd(
factory.createBinaryExpression(
factory.createTypeOfExpression(
factory.createElementAccessExpression(
a,
n
),
),
factory.createToken(SyntaxKind.EqualsEqualsEqualsToken),
factory.createStringLiteral("object")
),
factory.createBinaryExpression(
factory.createTypeOfExpression(
factory.createElementAccessExpression(
b,
m
),
),
factory.createToken(SyntaxKind.EqualsEqualsEqualsToken),
factory.createStringLiteral("object")
)
)
),
factory.createExpressionStatement(
factory.createBinaryExpression(
temp,
factory.createToken(SyntaxKind.EqualsToken),
factory.createFunctionCallCall(
objectEquals,
factory.createThis(),
[
factory.createElementAccessExpression(
a,
n
),
factory.createElementAccessExpression(
b,
m
),
factory.createBinaryExpression(
i,
factory.createToken(SyntaxKind.PlusToken),
factory.createNumericLiteral(1)
)
]
)
)
),
factory.createExpressionStatement(factory.createBinaryExpression(
temp,
factory.createToken(SyntaxKind.EqualsToken),
factory.createLogicalOr(
temp,
factory.createLogicalAnd(
factory.createBinaryExpression(
n,
factory.createToken(SyntaxKind.EqualsEqualsToken),
m
),
factory.createBinaryExpression(
factory.createElementAccessExpression(
a,
n
),
factory.createToken(SyntaxKind.EqualsEqualsToken),
factory.createElementAccessExpression(
b,
m
)
)
)
)
))
)
])
),
factory.createIfStatement(
factory.createLogicalNot(temp),
factory.createReturnStatement(factory.createFalse())
)
])
),
factory.createReturnStatement(factory.createTrue())
])
);
return setTextRange(factory.createFunctionCallCall(
functionExpression,
factory.createThis(),
[left, right, factory.createNumericLiteral(0)]
),node);
}
結果、以下のようにコンパイルできた。
これを実行することで、オブジェクトの中身を直接比較することができる。
以下のようにプロパティの順番が違っていても、プロパティの値がオブジェクトになっていても演算子ひとつで比較できる。
課題
課題としては以下のように自分を値に持つプロパティがある場合の対応についてである。
無限ループにはまるのを防ぐために上の実装では関数にカウンタを渡し、再帰の深さをカウントしていき、それが100に達した時点でfalseを返すようにしたが、果たしてfalseでいいのか、他にもっとよい実装はないかという点に関しては考える余地があると思われる。
終わりに
TypeScriptのコンパイラを実際にいじってみた系のサイトもなく本当に手探り状態で挑んた実験であったが実際に目標となるものが実装できて勉強になったし自信に繋がった。