0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

結論

めっちゃつらい

やってみたこと

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

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

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がエイリアスになってる?)

準備

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

やったこと

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

実装結果
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();

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

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?