banner
飞天御剑流

飞天御剑流

前端,jAVAsCRIPT
github

前端コンパイラの原理 the-super-tiny-compiler

4 ヶ月ぶりの更新#

実際には、後続の再帰ステップは、JS の参照データ型の特性を利用して、C 言語のポインタのような機能を模倣しているだけです😐単純にポインタを渡し、リンクリストのようなデータ構造を生成し、データを埋め込んで、最終的に目標コードを得るだけです... それだけです。

最初#

the-super-tiny-compiler は、GitHub で JS で書かれたコンパイラで、コードのコメントでは、最小のコンパイラかもしれないと言われています。Lisp スタイルの言語を C スタイルの言語に変換することができます。

このコンパイラプロジェクトは、小さながらも完全です。

ただし、私はソースコードを読んでいる間に、新しい AST を生成するプロセスで疑問を抱きました。

したがって、今からソースコードを分析する必要があります。

コンパイラの原理は、次のように簡単に説明できます。
レキシカルアナライザ
構文解析(AST の生成)
oldAst -> newAst への変換
最後に、生成された newAst をターゲット言語の構文に変換して出力します。
(add 2 (subtract 4 2))を入力として、以下のような ast 構造が得られます。

const ast = {
  type: "Program",
  body: [
    {
      type: "CallExpression",
      name: "add",
      params: [
        {
          type: "NumberLiteral",
          value: "2",
        },
        {
          type: "CallExpression",
          name: "subtract",
          params: [
            {
              type: "NumberLiteral",
              value: "4",
            },
            {
              type: "NumberLiteral",
              value: "2",
            },
          ],
        },
      ],
    },
  ],
};

この構造を得た後、次の関数が実行されます。


function transformer(ast) {
  let newAst = {
    type: "Program",
    body: []
  }

  ast._context = newAst.body
}

ast オブジェクトに新しいプロパティを追加し、そのプロパティを newAst の body プロパティに指定します。
その後、次のように進行します。

function transformer(ast) {
  let newAst = {
    type: "Program",
    body: []
  }

// ここでastのプロパティを変更すると、newAstに直接影響を与えることができます
  ast._context = newAst.body

  traverser(ast, {
    // 数字の処理
    NumberLiteral: {
      enter(node, parent) {
        parent._context.push({
          type: 'NumberLiteral',
          value: node.value
        })
      }
    },
    // 文字列
    StringLiteral: {
      enter(node, parent) {
        parent._context.push({
          type: 'StringLiteral',
          value: node.value,
        });
      },
    },

    CallExpression: {
      enter(node, parent) {
        let expression = {
          type: 'CallExpression',
          callee: {
            type: 'Identifier',
            name: node.name
          },
          arguments: []
        }

        node._context = expression.arguments;

        if (parent.type !== "CallExpression") {
          expression = {
            type: 'ExpressionStatement',//式文
            expression
          }
        }

        parent._context.push(expression)
      }
    }


  })

  return newAst//newAstを返す
}

わかりますが、newAst のデータの変化は、traverser 関数の実行だけで完了します。この関数は、先ほどの ast を引数にして実行され、さまざまなタイプに基づいて newAst の body にコピーする動作を行います。

この関数の内部は次のようになります。

function traverser(ast, visitor) {

  function traverseArray(array, parent) {
    
  }

  function traverseNode(node, parent) {
    // 渡されたnodeに対応するプロパティがあるかどうかを判断します
    let methods = visitor[node.type]

    // 存在する場合、親ノードのbodyに値を追加します
    if (methods && methods.enter) {
      methods.enter(node, parent)
    }

    // その後、visiterに含まれないプロパティを個別に処理します
    switch (node.type) {
      // 最初に実行されるのは、ノードのbodyをトラバースします
      case "Program":
        traverseArray(node.body, node);
        break;

      case "CallExpression":
        traverseArray(node.params, node)
        break;

      case "NumberLiteral":
      case "StringLiteral":
        break;

      default:
        throw new TypeError(node.type)
    }
  }

  traverseNode(ast, null)

}

実行すると、主要な流れは traverser -> traverseNode -> となります。ast が node パラメータとして渡され、この関数の実行プロセスは再帰呼び出しになります。最初の実行では、methods は undefined であり、その後 switch 文に入ります。最初の ast のタイプは Program であり、ast の body を引数として渡し、その後 break します。ここで、関数の主要な流れは完了します。その後の実行フローは、ast.body に対して疑似的な再帰またはネスト呼び出しを行います。関数は、tree パラメータに基づいて、式、パラメータに基づいて新しい ast を生成するかどうかを判断します。

最後に、生成された新しい ast をターゲット言語に変換し、文字列を再帰的に生成するだけです。

まとめると、見かけは簡単な小さなプロジェクトですが、自分で実装する場合、どれだけの細部を考慮する必要があるかわかりません。したがって、フロントエンドの深い道は重要であり、遠い道のりです!

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。