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 をターゲット言語に変換し、文字列を再帰的に生成するだけです。
まとめると、見かけは簡単な小さなプロジェクトですが、自分で実装する場合、どれだけの細部を考慮する必要があるかわかりません。したがって、フロントエンドの深い道は重要であり、遠い道のりです!