1. 词法分析器(tokenizer
函数)
java
```
function tokenizer(input) {
// 我们用一个`current`变量作为游标来跟踪我们在代码中的位置。
let current = 0;
// 并且用一个`tokens`数组来存放我们生成的token。
let tokens = [];
// 我们开始创建一个`while`循环,在这个循环中我们可以根据需要
// 在循环的"内部"多次增加`current`的值。
//
// 我们这么做是因为我们的tokens可以有任何长度。
while (current < input.length) {
// 我们还将存储输入中的`current`字符。
let char = input[current];
// 首先我们要检查的是开括号。这将在之后用于`CallExpression`,
// 但现在我们只关心字符本身。
//
// 我们检查是否有一个开括号:
if (char === '(') {
// 如果有,我们推送一个新的token,类型为`paren`,值为开括号。
tokens.push({
type: 'paren',
value: '(',
});
// 然后我们增加`current`
current++;
// 接着我们继续进行循环的下一个周期。
continue;
}
// 接下来我们要检查闭括号。我们做的和之前一样:检查闭括号,添加一个新token,
// 增加`current`,然后继续。
if (char === ')') {
tokens.push({
type: 'paren',
value: ')',
});
current++;
continue;
}
// 继续,我们现在要检查空白。这很有趣,
// 因为我们关心的是空白的存在是为了分隔字符,但实际上我们并不需要
// 将它作为token存储。我们稍后只会丢弃它。
//
// 所以在这里我们只是测试它是否存在,如果存在我们就继续。
let WHITESPACE = /\s/;
if (WHITESPACE.test(char)) {
current++;
continue;
}
// 下一个token类型是数字。这和我们之前看到的不一样,因为一个数字
// 可能由多个字符组成,我们想把整个字符序列作为一个token捕获。
//
// (add 123 456)
// ^^^ ^^^
// 只是两个独立的tokens
//
// 所以当我们遇到序列中的第一个数字时开始。
let NUMBERS = /[0-9]/;
if (NUMBERS.test(char)) {
// 我们将创建一个`value`字符串,我们将字符添加到这个字符串中。
let value = '';
// 然后我们将循环通过序列中的每个字符,直到我们遇到不是数字的字符,
// 将每个数字字符添加到我们的`value`中,并且随着我们前进将`current`增加。
while (NUMBERS.test(char)) {
value += char;
char = input[++current];
}
// 之后我们将我们的`number` token推送到`tokens`数组中。
tokens.push({ type: 'number', value });
// 然后我们继续。
continue;
}
// 我们还将为我们的语言添加对字符串的支持,任何被双引号(")包围的文本。
//
// (concat "foo" "bar")
// ^^^ ^^^ 字符串tokens
//
// 我们将通过检查开引号来开始:
if (char === '"') {
// 保持一个值`变量用于构建我们的字符串token。
let value = '';`
```
// 我们将跳过token中的开双引号。
char = input[++current];
// 然后我们将遍历每个字符,直到我们遇到另一个双引号。
while (char !== '"') {
value += char;
char = input[++current];
}
// 跳过闭双引号。
char = input[++current];
// 并将我们的`string` token添加到`tokens`数组中。
tokens.push({ type: 'string', value });
continue;
}
// 最后一种token类型将是`name` token。这是字母的序列而不是数字,它们是我们Lisp语法中函数的名称。
//
// (add 2 4)
// ^^^
// 名字token
//
let LETTERS = /[a-z]/i;
if (LETTERS.test(char)) {
let value = '';
// 同样,我们将循环通过所有字母,将它们添加到值中。
while (LETTERS.test(char)) {
value += char;
char = input[++current];
}
// 然后以`name`类型推送该值作为一个token,并继续。
tokens.push({ type: 'name', value });
continue;
}
// 如果我们现在还没有匹配到一个字符,我们将抛出一个错误并完全退出。
throw new TypeError('I dont know what this character is: ' + char);
}
// 在我们的`tokenizer`的末尾,我们简单地返回tokens数组。 return tokens;
}
```
简单点说: 词法分析器将原始代码转换成一个令牌流。令牌是表示代码片段的小对象,比如数字、字符串、标签、符号等。例如,词法分析器会将 var x = 2;
转换成类似 VAR_TOKEN
、IDENTIFIER(x)
、ASSIGNMENT_OPERATOR(=)
、NUMBER_LITERAL(2)
和 SEMICOLON
的令牌。
2.解析器(parser
函数)
此函数 parser
是一个编译器的一部分,它将由前一个函数 tokenizer
生成的词法单元(tokens)数组转换为一种抽象语法树(AST)。这是编译器中的解析步骤,它构建了一个嵌套的对象表示,这个对象表示源代码的结构,这里是一个特殊的 LISP 风格的语法结构。
csharp
// 解析器函数接受一个 tokens 数组作为输入
function parser(tokens) {
// current 变量作为游标
let current = 0;
// walk 函数是一个内部函数,用于递归地遍历 tokens 并创建 AST 的节点
function walk() {
// 从当前 token 开始
let token = tokens[current];
// 如果当前 token 是数字类型
if (token.type === 'number') {
// 增加 current
current++;
// 返回一个表示数字字面量的 AST 节点
return {
type: 'NumberLiteral',
value: token.value,
};
}
// 如果当前 token 是字符串类型
if (token.type === 'string') {
// 增加 current
current++;
// 返回一个表示字符串字面量的 AST 节点
return {
type: 'StringLiteral',
value: token.value,
};
}
// 如果当前 token 是一个开括号,表示一个函数调用的开始
if (token.type === 'paren' && token.value === '(') {
// 跳过开括号
token = tokens[++current];
// 创建一个调用表达式节点
let node = {
type: 'CallExpression',
name: token.value,
params: [],
};
// 跳过函数名
token = tokens[++current];
// 循环遍历所有参数,直到遇到闭括号
while ((token.type !== 'paren') || (token.type === 'paren' && token.value !== ')')) {
// 递归调用 walk,为每个参数创建节点,并添加到 params 数组中
node.params.push(walk());
token = tokens[current];
}
// 跳过闭括号
current++;
// 返回调用表达式节点
return node;
}
// 如果无法识别 token 类型,则抛出错误
throw new TypeError(token.type);
}
// 创建 AST 的根节点,类型为 'Program'
let ast = {
type: 'Program',
body: [],
};
// 循环调用 walk 函数填充 AST 的 body,直到处理完所有 tokens
while (current < tokens.length) {
ast.body.push(walk());
}
// 返回最终生成的 AST
return ast;
}
此函数为解析器的核心部分,它可以递归地处理嵌套结构,如函数调用中的参数列表,这些参数可能本身也是函数调用。它支持基本的数据类型如数字和字符串,并能识别函数调用表达式。它不处理空格或其他非结构化的词法单元,因为这些在语法分析阶段是不需要的。最终的结果是一个可以被进一步处理的抽象语法树。
也可以简单理解为: 解析器接收令牌流,并将其转换成一个抽象语法树(AST)。AST 是一个深层嵌套的对象,它以一种易于后续步骤处理的方式表示代码结构。例如,一个简单的表达式 1 + 2
可能会被转换成一个表示加法操作的AST,其中包含 1
和 2
作为子节点。
3.遍历器(traverser
函数)
这个 traverser
函数的作用是遍历抽象语法树(AST),并且在遍历的过程中调用提供的 visitor
对象的方法。这个函数在编译器的转换阶段非常有用,因为它允许我们对AST进行操作,如添加、修改或删除节点。
php
// traverser函数接收一个AST和一个visitor对象。
function traverser(ast, visitor) {
// traverseArray函数允许我们迭代一个数组,并且调用traverseNode函数。
function traverseArray(array, parent) {
array.forEach(child => {
traverseNode(child, parent);
});
}
// traverseNode函数接收一个节点和它的父节点。这样我们可以把它们传递给visitor中的方法。
function traverseNode(node, parent) {
// 我们首先检查visitor对象中是否有匹配`type`的方法存在。
let methods = visitor[node.type];
// 如果存在这个节点类型的`enter`方法,我们就用节点和它的父节点调用它。
if (methods && methods.enter) {
methods.enter(node, parent);
}
// 接下来我们根据当前节点的类型来分情况处理。
switch (node.type) {
// 我们从最顶层的`Program`开始。因为Program节点有一个名为body的属性,它包含一个节点数组,
// 我们将调用`traverseArray`来遍历它们。
//
// (记住`traverseArray`会反过来调用`traverseNode`,所以我们是在递归地遍历树)
case 'Program':
traverseArray(node.body, node);
break;
// 接下来我们对`CallExpression`做同样的处理,并且遍历它们的`params`。
case 'CallExpression':
traverseArray(node.params, node);
break;
// 在`NumberLiteral`和`StringLiteral`的情况下,我们没有子节点要访问,所以我们只需中断。
case 'NumberLiteral':
case 'StringLiteral':
break;
// 如果我们没能识别出节点类型,那么我们将抛出一个错误。
default:
throw new TypeError(node.type);
}
// 如果存在这个节点类型的`exit`方法,我们也用节点和它的父节点调用它。
if (methods && methods.exit) {
methods.exit(node, parent);
}
}
// 最后我们通过调用`traverseNode`来启动遍历器,传递我们的ast和null作为`parent`,
// 因为AST的顶层没有父节点。
traverseNode(ast, null);
}
traverser
函数能够深入AST中每一个节点,并且通过visitor
参数中定义的方法来对这些节点执行特定的操作。通过enter
和exit
方法,我们可以在进入一个节点之前以及离开一个节点之后执行代码。这种遍历方法允许对AST进行详尽的、细粒度的控制,是实现编译器转换的强大工具。
简单举例说明:构建AST后,遍历器用于"遍历"AST。它会对遇到的每个节点执行操作。这些操作可以根据节点类型而变化,并且通常由访问者模式定义,访问者模式为进入和/或离开每种类型的节点提供函数。例如,你可能希望在进入变量声明节点时将 var
声明转换为 let
声明。
4. 转换器(transformer
函数)
它将一个编程语言的抽象语法树(AST)转换为另一种语言的AST。
javascript
let newAst = {
type: 'Program',
body: [],
};
创建一个新的AST对象newAst
,它有一个类型为'Program'
的节点,这个节点包含一个空的body
数组。这个数组最终将填充新的AST节点。
javascript
ast._context = newAst.body;
在原始的AST(ast
)上设置一个属性_context
,这个属性指向新的AST的body
数组。在转换过程中,新的节点将被添加到这个数组中。
javascript
traverser(ast, {
调用traverser
函数,这个函数负责遍历原始的AST,并根据提供的规则(visitor对象)对每个节点进行处理。
javascript
NumberLiteral: {
enter(node, parent) {
parent._context.push({
type: 'NumberLiteral',
value: node.value,
});
},
},
定义了一个处理NumberLiteral
(数值字面量)的规则。当遍历器进入这种类型的节点时,会在其父节点的_context
数组中添加一个新的数值字面量节点。
javascript
StringLiteral: {
enter(node, parent) {
parent._context.push({
type: 'StringLiteral',
value: node.value,
});
},
},
为StringLiteral
(字符串字面量)定义了处理规则。同样地,在父节点的_context
数组中添加一个新的字符串字面量节点。
javascript
CallExpression: {
enter(node, parent) {
let expression = {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: node.name,
},
arguments: [],
};
node._context = expression.arguments;
处理CallExpression
(调用表达式)时,首先创建一个新的CallExpression
节点,包含callee
(被调用者)和一个空的arguments
(参数)数组。然后在原始节点上设置_context
属性,指向新节点的arguments
数组。
javascript
if (parent.type !== 'CallExpression') {
expression = {
type: 'ExpressionStatement',
expression: expression,
};
}
如果父节点不是CallExpression
类型,那么新创建的CallExpression
节点将被包装在一个ExpressionStatement
中,这反映了JavaScript中顶层调用表达式实际是一个语句的事实。
javascript
parent._context.push(expression);
将新创建的CallExpression
(可能已经包装为ExpressionStatement
)添加到父节点的_context
数组中。
javascript
});
这结束了traverser
函数调用内部的visitor对象定义。
javascript
return newAst;
在转换函数的最后,返回新创建的ASTnewAst
。
这个转换函数展示了如何从一个AST创建一个全新的AST,这通常是在编译器或源代码转换工具中进行的一种操作。
简单来说: 转换器接收原始的AST,并将其转换成表示转换后代码的新AST。这一步是编译或转译实际发生的地方。使用遍历器的功能,它会根据期望的结果对每个节点应用转换,比如改变语法或应用优化。
5.代码生成器(codeGenerator
函数)
javascript
function codeGenerator(node) {
定义了一个名为codeGenerator
的函数,它接收一个名为node
的参数。
javascript
switch (node.type) {
通过node.type
来确定节点类型,并基于这个类型执行不同的代码生成逻辑。
javascript
case 'Program':
return node.body.map(codeGenerator)
.join('\n');
如果节点是一个Program
,则对body
数组中的每一个元素执行codeGenerator
函数,并使用换行符将结果连接起来。
javascript
case 'ExpressionStatement':
return (
codeGenerator(node.expression) +
';' // << (...because we like to code the *correct* way)
);
如果节点是一个ExpressionStatement
表达式语句,将对其中的expression
表达式执行codeGenerator
函数,并在末尾加上分号。
javascript
case 'CallExpression':
return (
codeGenerator(node.callee) +
'(' +
node.arguments.map(codeGenerator)
.join(', ') +
')'
);
如果节点是一个CallExpression
调用表达式,将输出调用的函数名(callee
属性),然后在括号中使用逗号分隔参数(通过对arguments
数组中每个元素执行codeGenerator
函数并连接结果)。
javascript
case 'Identifier':
return node.name;
如果节点是一个Identifier
标识符,直接返回节点的名称。
javascript
case 'NumberLiteral':
return node.value;
如果节点是一个NumberLiteral
数字字面量,直接返回节点的值。
javascript
case 'StringLiteral':
return '"' + node.value + '"';
如果节点是一个StringLiteral
字符串字面量,返回节点的值,并在其两侧加上引号。
javascript
default:
throw new TypeError(node.type);
如果节点类型未被识别,抛出一个TypeError
异常。
javascript
}
函数定义结束。
这个函数的目的是将AST中的每个节点转换成对应的代码字符串,这是编译器生成最终代码的一个步骤。
简单来说:代码生成器接收转换后的AST,并将其重新转换成代码字符串。它递归地调用自身来打印树中的每个节点成一个大的字符串,有效地将已经应用的高级转换转化为低级或不同语言的代码。
6. 编译器(compiler
函数)
ini
function compiler(input){
let tokens = tokenizer(input);
let ast = parser(tokens);
let newAst = transformer(ast);
let output = codeGenerator(newAst); // and simply return the output!
return output;
}
compiler
函数是一个将输入字符串转换成新的代码字符串的编译器的主要入口点。它使用了一系列步骤来处理代码,这些步骤是大多数编译器流程的抽象表示。下面是这个函数的中文释义:
-
tokenizer(input)
:这个函数接受一个字符串输入,然后将其分解成一个令牌数组(tokens)。这些令牌是编程语言语法的最小单位,比如关键字、值、操作符和标点符号等。 -
parser(tokens)
:解析器(parser)函数接受令牌数组,并将它们组织成一个抽象语法树(AST)。AST是一个深层次的对象表示,它描述了代码的结构,不仅包含了元素本身,还包括了它们之间的关系。 -
transformer(ast)
:变换器(transformer)函数接受AST作为输入,并对其进行转换,生成一个新的AST。这一步通常包括修改、添加或移除代码的特定部分,以达到优化代码或转换成另一种语言的目的。 -
codeGenerator(newAst)
:代码生成器(codeGenerator)函数接受转换后的AST,并遍历这个树结构,生成新的代码字符串。这是编译过程的最后一步,将内部的AST转换成可执行的代码或者另一种格式的代码。
整个compiler
函数的执行过程就是编译过程的一个简化模型:它开始于原始代码的字符串形式,经过词法分析、语法分析、AST转换和代码生成,最终产生新的代码字符串。这个函数展示了编译器如何将一种代码形式转换为另一种形式,这在编程语言的转换、代码优化、代码压缩等多种场景中非常有用。