Try   HackMD

チームNo.6 Utability 進捗報告

レポジトリ:https://doss-gitlab.eidos.ic.i.u-tokyo.ac.jp/utability
slide: 未作成


プロジェクト概要

TypeScriptに「====」の評価を実装する。
動作イメージはjson的なオブジェクトの中身の比較(暫定)
現在はTypeScriptコードベースのデバッグ環境構築段階。


10/11 第3回進捗

  • レポジトリとhackMDの準備
  • 対象ソフトの選定
    • React, Vue, JavaScript,TypeScriptが候補に上がる
    • Javascriptエンジンは機械語が絡みそうで回避
    • TypeScriptに新機能(====の評価)を実装する方向性で仮決め
  • TypeSciptのダウンロードとビルド

インストール

TypeScript V4.5.0 -devをインストールした。
Microsoft/TypeScriptのmainブランチ:https://github.com/microsoft/TypeScript

ビルド

TypeScript フォルダ内のREADMEにしたがってビルドした。
推奨されているコマンドを使用すると、TypeScript/built/local内にビルドされる。

# ビルド #TypeScriptフォルダの中に入る #node.js, npmがある前提。ここでは、node:v14.16.0 npm:v8.0.0 sudo npm install -g gulp #gulpのインストール sudo npm ci #依存パッケージのインストール gulp local #ビルドの実行

テスト実行

#実行 #TypeScriptフォルダの中に、test.tsを作成したとする。 sudo node ./built/local/tsc.js test.ts node test.js

バージョン確認

sudo node ./built/local/tsc.js --version

10/12 第4回進捗

ビルドには成功したので、次はvscodeにtypescript用のデバッガーを準備する。

https://github.com/denoland/deno/issues/1404
上のissueによるとTypescriptはgdbでデバッグできないので、vscodeにビルドインされているTypescriptのデバッグ機能を使用する。

Debugging Typescript(公式ドキュメント):
https://code.visualstudio.com/docs/typescript/typescript-debugging

Debugging the TypeScript Codebase(Typescriptのコンパイラをデバッグする方法について): https://blog.andrewbran.ch/debugging-the-type-script-codebase/

お互いデバッグ環境を構築するのに苦戦したので次回までに終わらせたい。

(10/13追記) コンパイラをデバッグできるようになった。

デバッグ開始方法

  1. .vscodeフォルダ内のlaunch.template.jsonファイルをコピーし、launch.jsonとして.vscodeフォルダ内に配置

  2. 下記bashコードによりデバッガーlisten状態にする

#TypeScriptフォルダ内に入り、同じ階層にtest.tsを用意 sudo node --inspect-brk ./built/local/tsc.js test.ts
  1. vscodeの虫マークのデバッグツールタグから、"Attach to VS Code TS Server via Port"を選択して実行。これはlaunch.jsonに記述されているもの。

  2. よくわかんないけどなにかが動く。動いてるものが本当にコンパイル過程なのかは知らない。


10/14 第5回進捗

デバッグできるようになったので今日から実際にコンパイラの中身を見ていくことにする。
scanner.ts 165行目 textToTokenが怪しい?
1609行目 scan関数で文章解析?

tsc.ts で実行される
performCompilation()


10/18 第6回進捗

字句解析と構文解析の該当場所を意識。
字句解析→Token, 構文解析→Parserがキーワードか。

字句解析・Tokenの定義に関係してそうな場所

コード全体に対し、"EqualsEqualsEqualsToken"と検索すると、該当しそうな箇所がたくさん出てきた。

  • src>lib>typescript.d.ts
    SyntaxKindが定義されている
    ここにEqualsEqualsEqualsEqualsTokenを追加して、400とした。
export enum SyntaxKind { Unknown = 0, ... CommaToken = 27, QuestionDotToken = 28, LessThanToken = 29, LessThanSlashToken = 30, GreaterThanToken = 31, LessThanEqualsToken = 32, GreaterThanEqualsToken = 33, EqualsEqualsToken = 34, ExclamationEqualsToken = 35, EqualsEqualsEqualsToken = 36, EqualsEqualsEqualsEqualsToken = 400,
  • types.ts :

    • 64行目 SyntaxKindというオブジェクトの中身がいろいろ定義されている。
    ​​​​export const enum SyntaxKind = { ​​​​Unkown, ​​​​... , ​​​​EqualsEqualsEqualsToken, ​​​​... ​​​​}
    • 503行目 PunctuationSyntaxKindの定義があり、EqualsEqualsEqualsTokenを含む多くの演算子の集合として定義している
    ​​​​export type PunctionationSyntaxKind = ​​​​| SyntaxKind.OpenBraceToken ​​​​| ... ​​​​| SyntaxKind.EqualsEqualsEuqalsToken ​​​​
    • 1933行目 同じく、EqualityOperatorがいくつかの等号系Tokenの集合として定義されている
  • scanner.ts :

    • 183行目 "==="という文字列に対し、EqualsEqualsEqualsTokenという名前のSyntaxKindが割り当てられている(Map)
    ​​​​const textToToken = new Map(getEntries({ ​​​​...textToKeywordObj, ​​​​"{": SyntaxKind.OpenBraceToken, ​​​​. ​​​​. ​​​​. ​​​​"===": SyntaxKind.EqualsEqualsEqualsToken, ​​​​})) ​​​​
    • 同1945行目 scan()関数が定義されており、===を読みよるときの字句解析のコードと思われる。

構文解析

  • parser.ts :

    • 5549行目 構文解析において、===の次に何が来ていいのかチェックする関数
  • src>services>classifier.ts

    • 375行目 isBinaryExpressionOperatorToken(): 引数二つの演算子である場合、trueを返す
  • src>services>codefixs>interFromUsage.ts

    • 798行目
  • src>compiler>factory>utilities.ts

    • 963行目 isEqualityOperator()
  • src>compiler>utilities.ts

    • 3760行目
  • src>services>utilities.ts

    • 2664行目

各Tokenに対応する処理をよびだす場所

createStrictEquality"も大事そう。

  • nodefactory.ts :

    • 462行目 factory methodの呼び出し

      getってどういう意味だっけ。あとcreateStrictEquality()どこで使ってるのかあとで確認

    ​​​​get createStrictEquality() { ​​​​return getBinaryCreateFunction(Syntax.EqualsEqualsEqualsToken); ​​​​}
    • 31行目 getBinaryCreateFunctionの定義。処理自体の定義を呼び出してそう
    ​​​​const getBinaryCreateFunction = memoizeOne( ​​​​(operator: BinaryOperator) => ​​​​(left: Expression, right: Expression) => ​​​​createBinaryExpression(left, operator, right));
    • 2773行目 createBinaryExpressionの定義。めっちゃちゃんと処理書いてる!ここが大事?
    ​​​​//引数では ​​​​operator: BinaryOperater | BinaryOperatorToken ​​​​ ​​​​//コード内では ​​​​const operatorToken = asToken(operator)

    asTokenの中身はCreateToken

    • 973行目 CreateToken()
      中身はcreateBaseToken()
      CreateBaseTokenの中身はCreateBaseTokenNode

    • createStrictEqualityの使いどころ

typescript独自の構文をJavaScriptに通用する構文に書き換えてる部分

  • typescript独自のclass構文をfunction構文に書き換えてる部分

    src>compilers>transformers>classFields.ts

  • よくわかんないけど

    src>services>refactors>convertToOptionalChainExpressions.ts

成果:====をエラー吐くことなくそのままコンパイルさせることに成功。

上記で列挙した場所が全てではない。
ソースコード内で"EqualsEqualsEqualsToken"と検索して、出てきたsrcファイルないのすべての場所で"EqualsEqualsEqualsEqualsToken"を追加した。

またscannerでは、====を一つのTokenとして認識できるように書き換え

if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { if (text.charCodeAt(pos + 2) === CharacterCodes.equals) { if (text.charCodeAt(pos + 3)=== CharacterCodes.equals) { return pos += 4, token = SyntaxKind.EqualsEqualsEqualsEqualsToken; } return pos += 3, token = SyntaxKind.EqualsEqualsEqualsToken; } return pos += 2, token = SyntaxKind.EqualsEqualsToken; }

さらに、Token定義では、数字を割り当て。

あとは、tsにおける====文を、適切なjsテンプレートに置き換える。
class構文→function構文の変換を参考にしたい


10/19 第7回進捗

??演算子がコンパイラーによって置換されることを発見

//コンパイル前 console.log(null ?? 'NULL'); //コンパイル後 console.log(null !== null && null !== void 0 ? null : 'NULL');

調べると、??はQuestionQuestionTokenとして定義されているようだ

QuestionQUestionTokenの挙動を追跡する。

nodeFactory.ts 2784行目

node.transformFlags |= propagateChildFlags(node.left) | propagateChildFlags(node.operatorToken) | propagateChildFlags(node.right); if (operatorKind === SyntaxKind.QuestionQuestionToken) { node.transformFlags |= TransformFlags.ContainsES2020; }

propagateChildFlagsとはなにか、es2020の中身、Transformflagsオブジェクトの中身が気になる。
まずはES2020に関連ありそうなものを見る。

src>compiler>transformers>es2020.ts 38行目

case SyntaxKind.BinaryExpression: if ((node as BinaryExpression).operatorToken.kind === SyntaxKind.QuestionQuestionToken) { return transformNullishCoalescingExpression(node as BinaryExpression); }

transformNullishCcoalescingExpression()怪しい

同es2020.ts 196行目

function transformNullishCoalescingExpression(node: BinaryExpression) { let left = visitNode(node.left, visitor, isExpression); let right = left; if (!isSimpleCopiableExpression(left)) { right = factory.createTempVariable(hoistVariableDeclaration); left = factory.createAssignment(right, left); } return setTextRange(factory.createConditionalExpression( createNotNullCondition(left, right), /*questionToken*/ undefined, right, /*colonToken*/ undefined, visitNode(node.right, visitor, isExpression), ), node); }

createNotNullConditionが怪しい

同じくes2020.ts 180行目

function createNotNullCondition(left: Expression, right: Expression, invert?: boolean) { return factory.createBinaryExpression( factory.createBinaryExpression( left, factory.createToken(invert ? SyntaxKind.EqualsEqualsEqualsToken : SyntaxKind.ExclamationEqualsEqualsToken), factory.createNull() ), factory.createToken(invert ? SyntaxKind.BarBarToken : SyntaxKind.AmpersandAmpersandToken), factory.createBinaryExpression( right, factory.createToken(invert ? SyntaxKind.EqualsEqualsEqualsToken : SyntaxKind.ExclamationEqualsEqualsToken), factory.createVoidZero() ) ); }

全体の流れとして、
emit()がtransformES2020()を呼び出し
transformES2020()が内部関数visitor()を呼び出し
visitor()が条件分岐の中で、transformNullishCoalescingExpression()を呼び出している。

transformNullishCoalescingExpression -> visitor -> transformES2020 ->
getScriptTransformers -> scriptTransformers in getTransformers -> emitFiles in emitter.ts

作業

  1. nodefactory.ts のcreateBinaryExpressionを編集。operatorKindがEqualsEqualsEqualsEqualsTokenのときにES2020を参照するよう結びつける

  2. es2020.ts内のtransformES2020関数内のvisitor関数を編集。EqualsEqualsEqualsEqualsTokenの時に、transformObjectEqualsExpressionを行うと定義。

  3. es2020.ts内のtransformES2020関数内に、新たな内部関数として前述のtransformObjectEqualsExpression()を追加。とりあえず中身は、"??"の処理を行うtransformNullishCoalescingExpression()と全く同じにした。

成果:====をほかの適当な表現に変換することに成功

//コンパイル前 console.log(a ==== b); //コンパイル後 console.log(a !== null && a !== void 0 ? a : b);

10/21 第8回進捗

作業:transformObjectEqualsExpression()の編集

目標は、自分たちが欲しいかたちを自由に出力することだが、まだ関数の構造がわからないので試行錯誤

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); } // left.parent.right.escapedText = "hoge"; console.log(left.parent); return setTextRange(factory.createBinaryExpression( left, factory.createToken(SyntaxKind.EqualsEqualsEqualsToken), right ), node); }

上記のように関数を変更すると、コンパイルは以下のようになった

//コンパイル前 console.log(a ==== b); //コンパイル後 console.log(a === b);

10/25 第9回進捗

実際に「====」を目的の処理ができる形にコンパイルできるように、transformObjectEqualsExpression()を変更していく。
構文木が実際にどのようにしてjsファイルに出力されているかは完全にわかっているわけではないが、中身はNodeという単位で管理されており、出力されるコードとNodeは一対一に対応していることがわかった。
(例)
createVariableStatement: 変数定義
createForStatement: for文生成
createPrefixIncrement: インクリメント
createReturnStatement: return文生成
createTrue: True生成

これらNodeを生成するAPIを活用し、実際に出力したいコードに対応する
入れ子構造を作った。

factory.createFunctionExpression( /*modifiers*/ undefined, /*asteriskToken*/ undefined, /*name*/ "objectEquals", /*typeParameters*/ undefined, /*parameters*/ [], /*type*/ undefined, factory.createBlock([ factory.createVariableStatement( /* modifier */ undefined, /* declarationList */ factory.createVariableDeclarationList([ factory.createVariableDeclaration(lengthOfLeft,undefined,undefined,factory.createNumericLiteral(0)), factory.createVariableDeclaration(lengthOfRight,undefined,undefined,factory.createNumericLiteral(0)) ]) ), factory.createForInStatement( /* initializer */ factory.createVariableDeclarationList([ factory.createVariableDeclaration(n) ]), left, factory.createBlock([ factory.createExpressionStatement( factory.createPrefixIncrement(lengthOfLeft) ) ]) ), factory.createForInStatement( /* initializer */ factory.createVariableDeclarationList([ factory.createVariableDeclaration(m) ]), right, factory.createBlock([ factory.createExpressionStatement( factory.createPrefixIncrement(lengthOfRight) ) ]) ), factory.createIfStatement( /* expression */ factory.createBinaryExpression( lengthOfLeft, factory.createToken(SyntaxKind.ExclamationEqualsToken), lengthOfRight ), /* then */ factory.createReturnStatement( factory.createFalse() ) ), factory.createForInStatement( /* initializer */ factory.createVariableDeclarationList([ factory.createVariableDeclaration(n) ]), left, /* statement */ factory.createBlock([ factory.createVariableStatement( /* modifier */ undefined, /* declarationList */ factory.createVariableDeclarationList([ factory.createVariableDeclaration(temp,undefined,undefined,factory.createFalse()) ]) ), factory.createForInStatement( factory.createVariableDeclarationList([ factory.createVariableDeclaration(m) ]), right, /* statement */ factory.createBlock([ 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( left, n ), factory.createToken(SyntaxKind.EqualsEqualsToken), factory.createElementAccessExpression( right, m ) ) ) ) )) ]) ), factory.createIfStatement( /* expression */ factory.createLogicalNot(temp), /* then */ factory.createReturnStatement(factory.createFalse()) ) ]) ), factory.createReturnStatement(factory.createTrue()) ]) )

生成されるコードは以下

function objectEquals() { var _a = 0, _b = 0; for (var n in a) { ++_a; } for (var m in b) { ++_b; } if (_a != _b) return false; for (var n in a) { var _c = false; for (var m in b) { _c = _c || n == m && a[n] == b[m]; } if (!_c) return false; } return true; } .call(this)


:100: :muscle: :tada: