学习链接:github.com/jamiebuilds...
词法分析------tokenizer
主要任务:从左向右逐行扫描源程序的字符,识别出各个单词,确定单词的类型。将识别出的单词转换成统一的机内表示------词法单元(token)形式
根据仓库代码的测试用例分析词法分析的作用
go
test("tokenizer", () => {
const code = `(add 2 (subtract 4 2))`;
const tokens = [
{ type:TokenTypes.Paren, value: "(" },
{ type:TokenTypes.Name, value: "add" },
{ type:TokenTypes.Number, value: "2" },
{ type:TokenTypes.Paren, value: "(" },
{ type:TokenTypes.Name, value: "subtract" },
{ type:TokenTypes.Number, value: "4" },
{ type:TokenTypes.Number, value: "2" },
{ type:TokenTypes.Paren, value: ")" },
{ type:TokenTypes.Paren, value: ")" },
];
expect(tokenizer(code)).toEqual(tokens);
});
- 将代码字符串转换成语法单元(token)
- 提取代码中的基础语法组成部分
- 消除无意义字符,保留有意义语法单元
- 为语法分析提供输入
- 标识代码中的语法结构
词法分析实现
(add 2 (subtract 4 2))
主要处理内容:字母,数字,空格,括号
处理方式:
- 使用正则表达式识别语法单元:空格、字母、数字和括号
- 遍历代码字符串,匹配语法单元
- 括号:type: Paren, value: 括号字符串
- 字母: 正则匹配:
/[a-z]/i
,type: Name, value: 字母 - 数字: 正则匹配:
/[0-9]/
,type: Nmuber, value: 数字 - 空格:正则匹配:
/\s/
,跳过
- 正则匹配成功后将生成的token放入tokens数组
语法分析------parse
语法解析的作用是生成AST来表示代码的语法结构,它位于编译过程的语法分析阶段,属于编译中间步骤的核心过程
主要任务: 从令牌(tokens)中创建出抽象语法树(AST)
根据仓库代码的测试用例分析parse的作用
yaml
test("parse", () =>{
const tokens = [
{ type: TokenTypes.Paren, value: "(" },
{ type: TokenTypes.Name, value: "add" },
{ type: TokenTypes.Number, value: "2" },
{ type: TokenTypes.Paren, value: "(" },
{ type: TokenTypes.Name, value: "subtract" },
{ type: TokenTypes.Number, value: "4" },
{ type: TokenTypes.Number, value: "2" },
{ type: TokenTypes.Paren, value: ")" },
{ type: TokenTypes.Paren, value: ")" },
];
const ast = {
type: NodeTypes.Program,
body: [
{
type: NodeTypes.CallExpression,
name: "add",
params: [
{
type: NodeTypes.NumberLiteral,
value: "2",
},
{
type: NodeTypes.CallExpression,
name: "subtract",
params: [
{
type: NodeTypes.NumberLiteral,
value: "4",
},
{
type: NodeTypes.NumberLiteral,
value: "2",
},
],
},
],
},
],
};
expect(parser(tokens)).toEqual(ast);
});
- 输入是令牌(tokens)数组,输出是抽象语法树(AST)。
- AST以嵌套的对象结构表示代码的语法结构
- 语法解析后的AST可用于代码生成、解释执行等后续处理。
- 用于后续代码生成、解释执行
parse实现
ini
export function parser(tokens: Token[]) {
let current = 0;
const rootNode = createRootNode()
function walk() {
let token = tokens[current]
if(token.type === TokenTypes.Number){
current ++
return createNumberNode(token.value)
}
if(token.type === TokenTypes.Paren && token.value === "("){
token = tokens[++current]
let node = createCallExpressionNode(token.value)
token = tokens[++ current];
while(!(token.type === TokenTypes.Paren && token.value === ")")){
node.params.push(walk())
token = tokens[current]
}
current ++
return node
}
throw new Error(`不认识的token: ${token}`)
}
while (current < tokens.length){
rootNode.body.push(walk())
}
return rootNode
};
- 判断token的类型并创建相应的节点:
- 当token是数字类型时,创建数字的节点
- 当token是字符串类型时,创建字符串的节点
- 当token是左括号时,一般接下来就是要 解析到一个函数调用的语法结构:
javascript
调用createCallExpressionNode来创建一个对应这个函数调用的AST节点
循环调用函数判断创建节点,直到token是右括号
如果token解析不了的话则抛出错误:throw new Error(`不认识的token: ${token}`)
- 判断tokens是否遍历完毕,没有则需要往下继续遍历
traverser 遍历 AST
主要任务:traverser 实现一个简单的AST遍历器,【判断一个节点是哪里的enter或者exit,并标识当前节点的类型和父节点的类型】
css
test("traverser", () => {
const ast: RootNode = {
type: NodeTypes.Program,
body: [
{
type: NodeTypes.CallExpression,
name: "add",
params: [
{
type: NodeTypes.NumberLiteral,
value: "2",
},
{
type: NodeTypes.CallExpression,
name: "subtract",
params: [
{
type: NodeTypes.NumberLiteral,
value: "4",
},
{
type: NodeTypes.NumberLiteral,
value: "2",
},
],
},
],
},
],
};
const callCounts: Array<string | NodeTypes >[] = [];
const visitor: Visitor = {
Program: {
enter(node, parent) {
callCounts.push(["program-enter", node.type, ""]);
},
exit(node, parent) {
callCounts.push(["program-exit", node.type, ""]);
},
},
CallExpression: {
enter(node, parent) {
callCounts.push(["callExpression-enter", node.type, parent!.type]);
},
exit(node, parent) {
callCounts.push(["callExpression-exit", node.type, parent!.type]);
},
},
NumberLiteral: {
enter(node, parent) {
callCounts.push(["numberLiteral-enter", node.type, parent!.type]);
},
exit(node, parent) {
callCounts.push(["numberLiteral-exit", node.type, parent!.type]);
},
},
};
traverser(ast, visitor);
expect(callCounts).toEqual([ ["program-enter", NodeTypes.Program, ""],
["callExpression-enter", NodeTypes.CallExpression, NodeTypes.Program],
["numberLiteral-enter", NodeTypes.NumberLiteral, NodeTypes.CallExpression],
["numberLiteral-exit", NodeTypes.NumberLiteral, NodeTypes.CallExpression],
[ "callExpression-enter", NodeTypes.CallExpression, NodeTypes.CallExpression, ],
["numberLiteral-enter", NodeTypes.NumberLiteral, NodeTypes.CallExpression],
["numberLiteral-exit", NodeTypes.NumberLiteral, NodeTypes.CallExpression],
["numberLiteral-enter", NodeTypes.NumberLiteral, NodeTypes.CallExpression],
["numberLiteral-exit", NodeTypes.NumberLiteral, NodeTypes.CallExpression],
["callExpression-exit", NodeTypes.CallExpression, NodeTypes.CallExpression],
["callExpression-exit", NodeTypes.CallExpression, NodeTypes.Program],
["program-exit", NodeTypes.Program, ""],
]);
});
分析测试代码:
- 定义一个AST对象:包含一个 Program 节点和一些 CallExpression、NumberLiteral 子节点。
- 定义一个 callCounts 数组: 用于记录遍历过程中每个节点的进入和退出的调用。
- 定义一个 visitor 对象:包含了对每个节点类型的进入和退出时需要执行的回调函数。
- 最后调用 traverser 函数,传入 AST 对象和 visitor进行遍历。
-
- 在遍历过程中,会根据节点类型调用相应的 visitor.Program、visitor.CallExpression 等回调函数。这些回调函数会将当前访问的节点信息以及父节点信息记录到 callCounts 数组中。
- 这样就可以通过 callCounts 数组看到整个遍历过程中每个节点的进入和退出的顺序。
tarverser的实现
scss
export function traverser(rootNode: RootNode, visitor: Visitor){
// 1. 深度优先搜索
// 2. visitor
function traverseArray(array: ChildNode[],parent?:ParentNode) {
array.forEach((node) => {
traverseNode(node,parent)
})
}
function traverseNode(node: ChildNode | RootNode, parent?:ParentNode) {
const visitorObj = visitor[node.type]
if(visitorObj){
visitorObj.enter(node,parent);
}
switch (node.type) {
case NodeTypes.NumberLiteral:
break;
case NodeTypes.CallExpression:
traverseArray(node.params,node);
break;
case NodeTypes.Program:
traverseArray(node.body,node);
break;
}
if(visitorObj?.exit && visitorObj){
visitorObj.exit(node,parent);
}
}
traverseNode(rootNode)
}
- traverser 函数就是一个遍历器,它接受两个参数:rootNode[ AST的根节点 ] 和visitor[ 一个对象,包含了访问AST节点时所要执行的操作 ] 。基于深度优先搜索算法,对AST进行遍历。
- traverseArray: 用于遍历一个节点的子节点数组,它接受一个数组和一个可选的父节点参数。该函数内部使用forEach 方法遍历数组中的每个节点,并调用traverseNode函数对其进行遍历
- traverseNode: 用于遍历一个节点,并根据节点类型执行相应的操作。
在函数内部,首先从visitor 对象中获取当前节点类型对应的操作,如果存在则执行其enter方法。
然后根据节点类型进行不同的处理,
- 对于CallExpression类型的节点,会递归遍历其参数数组;
- 对于Program类型的节点,会递归遍历其主体数组。
- 在遍历完子节点后,如果当前节点类型对应的操作存在exit 方法,则执行其exit方法。
transformer 转换 AST
主要任务:将一个AST转换为另一种形式的AST[新的 AST 可以更好地表示代码的结构和语义,便于进行代码重构、优化、分析、生成等操作]
首先定义了一个原始的AST,然后定义了一个期望的转换后的AST。接下来调用transformer函数对原始AST进行转换,并将转换后的AST与期望的AST进行比较,判断转换结果是否符合预期。
css
test("transformer", () => {
const originalAST: RootNode = {
type: NodeTypes.Program,
body: [
{
type: NodeTypes.CallExpression,
name: "add",
params: [
{
type: NodeTypes.NumberLiteral,
value: "2",
},
{
type: NodeTypes.CallExpression,
name: "subtract",
params: [
{
type: NodeTypes.NumberLiteral,
value: "4",
},
{
type: NodeTypes.NumberLiteral,
value: "2",
},
],
},
],
},
],
};
const transformedAST = {
type: NodeTypes.Program,
body: [
{
type: "ExpressionStatement",
expression: {
type: "CallExpression",
callee: {
type: "Identifier",
name: "add",
},
arguments: [
{
type: "NumberLiteral",
value: "2",
},
{
type: "CallExpression",
callee: {
type: "Identifier",
name: "subtract",
},
arguments: [
{
type: "NumberLiteral",
value: "4",
},
{
type: "NumberLiteral",
value: "2",
},
],
},
],
},
},
],
};
expect(transformer(originalAST)).toEqual(transformedAST);
});
对于originalAST:
- type: 表示该 AST 节点的类型 [不同的节点类型有不同的属性和语义,用于表示不同的语法结构和语义]
- name: 表示函数的名称
- params: 通常用于表示函数的参数列表
- value: 表示 AST 中的字面量节点的值 [只有特定的节点类型才具有 value 属性]
对于transformedAST:
- ExpressionStatement: 用于表示该对象是一个表达式语句
- callee 属性是一个对象,它表示被调用的函数。[包含一个 type 属性和一个 name 属性]
- type 属性的值是字符串 "Identifier" ,用于表示该对象是一个标识符
- name 属性的值是字符串 "add" ,用于表示被调用的函数的名称
- arguments 属性是一个数组,它表示函数调用时传递的参数列表。[包含两个对象,分别表示传递的两个参数]
这两个对象中都包含一个 type 属性和一个 value 属性
- type 属性的值是字符串 "NumberLiteral" ,用于表示该对象是一个数字字面量
- value 属性的值是字符串 "2" 和 "4" ,分别表示传递的两个参数的值
transformer的实现
ini
export function transformer (ast: RootNode){
const newAst = {
type: NodeTypes.Program,
body: []
};
ast.context = newAst.body
traverser(ast,{
CallExpression: {
enter(node,parent) {
// 如果节点类型是CallExpression,即函数调用表达式
if(node.type === NodeTypes.CallExpression) {
// 构造一个新的CallExpression节点
let expression: any = {
type: "CallExpression",
callee: { // callee是调用的函数标识符
type: "Identifier",
name: node.name
},
arguments: [], // arguments为参数占位
};
node.context = expression.arguments;
// 如果父节点不是CallExpression,则再包一层ExpressionStatement
if(parent?.type !== NodeTypes.CallExpression){
expression = {
type: "ExpressionStatement",
expression,
}
}
// 将ExpressionStatement推入父节点context中
parent?.context?.push(expression)
}
}
},
NumberLiteral: {
enter(node, parent) {
if(node.type === NodeTypes.NumberLiteral) {
const numberNode: any = {
type: "NumberLiteral",
value: node.value
};
parent?.context?.push(numberNode)
}
}
}
})
return newAst
}
- ast.context = newAst.body [将原始 AST 的 context 属性指向新的 AST 的 body 属性,这样在遍历原始 AST 的过程中,可以方便地将新的节点添加到新的 AST 中]
对于 CallExpression,递归求值 callee 和 arguments,然后执行调用
对于 NumberLiteral 直接返回 value 值
codegen
主要任务:将 AST 转换成 JavaScript 代码
css
test("codegen", () => {
const ast = {
type: NodeTypes.Program,
body: [
{
type: "ExpressionStatement",
expression: {
type: "CallExpression",
callee: {
type: "Identifier",
name: "add",
},
arguments: [
{
type: "NumberLiteral",
value: "2",
},
{
type: "CallExpression",
callee: {
type: "Identifier",
name: "subtract",
},
arguments: [
{
type: "NumberLiteral",
value: "4",
},
{
type: "NumberLiteral",
value: "2",
},
],
},
],
},
},
],
};
expect(codegen(ast)).toMatchInlineSnapshot('"add(2, subtract(4, 2));"');
});
codegen实现
matlab
export function codegen (node) {
switch (node.type) {
case "Program":
return node.body.map(codegen).join("");
case "ExpressionStatement":
return codegen(node.expression) + ";"
case "NumberLiteral":
return node.value;
case "CallExpression":
return (
node.callee.name + "(" + node.arguments.map(codegen).join(", ") + ")"
);
}
}
接受一个 AST 对象作为参数,将其转换为 JavaScript 代码字符串并返回。
根据节点类型进行不同的处理:
- 对于 Program 节点,它将遍历 AST 的 body 属性,并对每个子节点递归调用 codegen 函数,最后将它们连接成一个字符串
- 对于 ExpressionStatement 节点,它将递归调用 codegen 函数处理 expression 属性,并在最后添加一个分号。
- 对于 NumberLiteral 节点,它将返回节点的 value 属性。
- 对于 CallExpression 节点,它将使用节点的 callee 属性的 name 属性作为函数名,并将 arguments 属性中的每个子节点递归调用 codegen 函数处理,最后将它们连接成一个字符串,用括号包裹。
整合实现------compiler
最终任务: 将 Lisp 代码编译成 JavaScript 代码
Lisp 是一种基于列表处理的编程语言,它的语法非常简单,只有两种数据类型:原子和列表。
Lisp 代码由原子和列表组成,原子可以是数字、字符串或符号,而列表则是用括号括起来的一组原子或列表。
scss
test("compiler", () => {
const code = `(add 2 (subtract 4 2))`;
expect(compiler(code)).toBe("add(2, subtract(4, 2));");
});
整合
scss
export function compiler(code: string){
const tokens = tokenizer(code)
const ast = parser(tokens)
const transformedAST = transformer(ast)
return codegen(transformedAST)
}