TypeScript Compiler APIでC＋＋を生成してみる

## 結論

めっちゃつらい

## やってみたこと

TypeScriptのファイルをC++にトランスパイルする。

```ts:test.ts
function main() {
  const txt = 'world';
  const num = 100;
  console.log('hello', txt, 2, num);
}
```

を

```cpp:test.cpp
#include <string>
#include <iostream>

int main () {
  const char* txt = "world";
  const int num = 100;
  std::cout << "hello" << txt << 2 << num << std::endl;
  return 0;
};
```

`printf`にするつもりで始めましたが、書式指定子に手間がかかるのでやめました...
ちなみに、実行結果は(TSはmainを実行するよう書き換える必要があるが)下記

- TS: `hello world 2 100`
- cpp: `helloworld2100`

## 環境

- MacBook Air(M1)
- Node.js: 18系(古かった...)
- g++: Apple clang version 15(clangがエイリアスになってる？)

## 準備

```bash
npm init
npm install typescript
npm install --save-dev @types/node
```

## やったこと

Nodeをconsole.logで出力しながら、typeをtypescript.d.tsとにらめっこしてどの`ts.isXXXX`で判定できるのか推測しながら実装。

<details>
<summary>実装結果</summary>

```ts
import * as ts from 'typescript';
import * as fs from 'node:fs';
const FILE = 'test.ts';

function visitAll(node: ts.Node, callback: (child: ts.Node) => void) {
  callback(node);
  if (ts.isFunctionDeclaration(node)) return;
  node.forEachChild(child => visitAll(child, callback));
}

const DEBUG = false;
function dbgLog(...args: unknown[]) {
  if (DEBUG) console.log(...args);
}
const output: string[] = [];

function main () {
  // 変換対象のファイルを読み込み
  const program = ts.createProgram([FILE], {});
  const sourceFile = program.getSourceFile(FILE);
  if (!sourceFile) return;

  // C++のinclude
  output.push('#include <string>');
  output.push('#include <iostream>');
  output.push('');

  // TS解析後のNodeごとの処理
  visitAll(sourceFile, (child) => {
    // ソースファイルなら終了
    if (ts.isSourceFile(child)) return;

    // 関数定義のとき
    if (ts.isFunctionDeclaration(child)) {
      if (child.name) dbgLog(`関数: ${child.name.text}`);
      if (!child.body) return;
      // C++メイン関数準備
      if (child.name?.text == 'main') output.push('int main () {');

      // 各ステートメントでループ
      for (const statement of child.body.statements) {
        // 変数定義
        if (ts.isVariableStatement(statement)) {
          dbgLog('-- variable statement --');
          const dl = statement.declarationList;
          let type = '';
          if (dl.flags == ts.NodeFlags.Const) type = 'const';
          else if (dl.flags == ts.NodeFlags.Let) type = 'let';
          else {
            dbgLog(dl);
            continue;
          }

          for (const dec of dl.declarations) {
            const name = dec.name;
            if (!ts.isIdentifier(name)) {
              dbgLog(dec);
              continue;
            }
            // TODO: TS変数定義時の型情報
            if (dec.type) { 
              dbgLog(dec.type);
              if (ts.isTypeLiteralNode(dec.type)) {
                for (const member of dec.type.members) dbgLog(member);
              }
            }

            // 変数定義のC++置き換え準備
            const val = (() => {
              const init = dec.initializer;
              if (!init) return;
              if (ts.isStringLiteral(init)) return {
                type: type == 'const' ? 'const char*' : 'std::string',
                val: `"${init.text}"`,
              };
              if (ts.isNumericLiteral(init)) {
                const num = Number(init.text);
                if (isNaN(num)) throw new Error('invalid');
                const typePrefix = type == 'const' ? 'const ' : '';
                return {
                  type: `${typePrefix}${Number.isInteger(num) ? 'int' : 'double'}`,
                  val: init.text,
                };
              }
            })();
            dbgLog(`${type} ${name.escapedText}${val !== undefined ? ` = ${val.val}` : ''};`);

            // 変数定義をC++として出力(型は初期値から推測)
            if (val) output.push(`  ${val.type} ${name.escapedText} = ${val.val};`);
          }
        }
        // 処理
        else if (ts.isExpressionStatement(statement)) {
          const expression = statement.expression;
          // 関数呼び出し
          if (ts.isCallExpression(expression)) {
            let call = '';

            dbgLog('-- call expression --');
            dbgLog(expression.expression);
            if (ts.isPropertyAccessExpression(expression.expression)) {
              const callExpExp = expression.expression;
              if (ts.isIdentifier(callExpExp.expression) && ts.isIdentifier(callExpExp.name)) {
                call = `${callExpExp.expression.escapedText}.${callExpExp.name.escapedText}`;
              }
            }

            // 引数解析
            const args: string[] = [];
            for (const arg of expression.arguments) {
              if (ts.isStringLiteral(arg)) args.push(`"${arg.text}"`);
              else if (ts.isNumericLiteral(arg) || ts.isIdentifier(arg)) args.push(arg.text);
              else dbgLog(arg);
            }
            dbgLog(`${call}(${args.join(', ')});`);

            // console.logの引数内容をC++の出力に置き換える
            if (call == 'console.log') {
              output.push(`  std::cout << ${args.join(' << ')} << std::endl;`);
            }
          } else {
            dbgLog('-- unknown expression --')
            dbgLog(expression);
          }
        }
        else {
          dbgLog('-- unknown statement --')
          dbgLog(statement);
          continue;
        }
      }

      // C++メイン関数の終わり
      if (child.name?.text == 'main') output.push('  return 0;\n};');
      return;
    }

    // console.log(child);
  });

  try { fs.unlinkSync('test.cpp'); } catch(_) {}
  fs.writeFileSync('test.cpp', output.join('\n'));
}
main();
```
</details>

大したことやってないのに100ステップ以上ある...

## 参考

- [TypeScript Compiler APIに入門する](https://qiita.com/suke083/items/878e9d275305ae18c30b)