PrattParser项目概述
前段时间一直想着手恶补 编译原理 的相关知识, 一开始打算直接读大学的 编译原理 , 虽然内容丰富, 但是着实抽象难懂. 无意间看到B站的熊爷关于普拉特解析器相关内容, 感觉是一个非常好的切入点.所以就写了基于Swift版本的 PrattParser.
下面是我整理的项目中各个类以及其中函数的作用.
更加具体的请查看 PrattParser解释器项目类与函数
接下来, 我把整个项目UML图发出来, 大家可以借鉴查看.
更加具体的请查看 PrattParser的Swift项目UML图
接下来, 我就以 词法分析
、语法分析
、 中间代码生成
三部分逐步来说明一下这个 基于Swift的PrattParser项目
词法分析
词法分析的核心类是 Lexer
, 输入的原始代码字符串 code
, 输出的是一组词法单元 Token
.
在词法分析器 Lexer
中, 核心函数就是 nextToken
, nextToken函数
职责一共有两个职责.
-
去除代码格式化的逻辑, 例如, 去除
空格
换行
等等. 这一步主要是通过调用skipWhitespace()
函数实现的.Swiftpublic func skipWhitespace() { while (hasNext()) { if (word == " " || word == "\t" || word == "\n" || word == "\r") { readCodeAction(); } else { break } } }
-
读取数学符号与数字并且生成
词法单元Token
Swiftswitch(word) { case "+" : token = PrattParser.Token(TokenType.PLUS, "+") break case "-" : token = PrattParser.Token(TokenType.MINUS, "-") break case "*" : token = PrattParser.Token(TokenType.ASTERISK, "*") break case "/" : token = PrattParser.Token(TokenType.SLASH, "/") break case "(" : token = PrattParser.Token(TokenType.LPAREN, "(") break case ")" : token = PrattParser.Token(TokenType.RPAREN, ")") break case "^" : token = PrattParser.Token(TokenType.HAT, "^") break case nil : token = PrattParser.Token(TokenType.EOF, "") break default: if (isDigit(word)) { let num: String = readNum(); token = PrattParser.Token(TokenType.NUM, num); return token; } else { throw LexerError.lexerError(message: "Lexer error") } }
生成词法单元函数 nextToken
的整体逻辑流程图如下所示. 基本涉及了词法分析器 Lexer
的所有函数.
这里要补充的一点的就是由于数学符号大部分是单个字符, 例如 +
-
*
/
(
)
, 这个读取直接生成即可. 但是数字可能是有多位的, 所以生成的过程需要通过循环一直查找. 在该项目中的代码实现中读取数字字符的逻辑代码主要存在于 readNum
函数中.
Java
public func readNum() -> String {
var num: String = ""
while (isDigit(word)) {
num += word ?? ""
readCodeAction()
}
return num;
}
生成数字函数 readNum
的整体逻辑流程图如下所示.
在该项目中, 词法分析器Lexer
的外部驱动力是 语法分析器Parser
, 也就是说语法分析器Parser
一直在调用 Lexer
的 nextToken
函数从而不断地生成词法单元 Token
.
语法分析
在 词法分析
模块, 我们了解到了 词法分析器Lexer
会为 语法分析器Parser
提供源源不断生成的词法单元 Token
.
语法分析器Parser
则会这些词法单元 Token
根据 符号的优先级 生成一颗 AST语法树
.
在 语法分析器Parser
生成 AST语法树
的过程中, 其入口函数是 parseMain()
, 核心函数是 parseExpression()
, 具体代码如下所示.
Swift
func parseExpression(_ precedence: Precedence) -> Expression? {
let funcName: String? = prefixParseFnHashMap[cur?.type ?? TokenType.None];
if (funcName == nil) {
print("未找到AST节点构建函数名称")
return nil
}
// 生成前置节点, 获取左节点
var leftExpression: Expression? = getPrefixExpression(funcName);
// 能递归的原因 判断下一个词法单元是否是EOF, 判断下一个词法单元的优先级是否大于当前的优先级
while (!peekTokenIs(TokenType.EOF) && precedence.rawValue < peekPrecedence()?.rawValue ?? 0) {
let infixParseFnName: String? = infixParseFnHashMap[peek?.type ?? TokenType.None];
if (infixParseFnName == nil) {
print("未找到AST节点构建函数名称")
return leftExpression;
}
//读取下一个词法单元
nextToken();
// 生成中置节点, 更新AST语法树
leftExpression = parseInfixExpression(leftExpression);
}
return leftExpression
}
由于递归过程比较复杂, 我整理了一下整体的逻辑流程图.
当我们看到上一个图的时候, 我们会诧异, 说好的递归过程在哪呢? 其实递归过程主要隐藏在生成中置节点函数 parseInfixExpression()
中, 由于 parseExpression()
→ parseInfixExpression()
→ parseExpression()
→ ....
的调用关系会最终产生递归效果.
在中置节点生成函数parseInfixExpression
中, 右节点的生成依然会依赖 parseExpression()
, 这也就递归产生的驱动力.
Swift
// 中置节点生成函数
func parseInfixExpression(_ left: Expression?) -> Expression? {
let infixExpression = InfixExpression();
infixExpression.left = left;
infixExpression.operatorValue = cur?.value;
let precedence: Precedence = Precedence.getPrecedence(cur?.type);
nextToken();
// 右节点的生成是递归产生的驱动力
infixExpression.right = parseExpression(precedence);
return infixExpression
}
中置节点生成函数parseInfixExpression
的逻辑流程图如下所示.
粗略的说了大致的流程, 接下来, 我们就详情的说一下具体的执行流程.
具体的以 1 + 4 - 3
和 1 + 2 * 3
两个数学运算为示例.
1 + 4 - 3
的AST语法树构建过程
强烈建议大家一边项目断点, 一边对照该模块的流程!!!
-
整体还是以
parseMain()
为入口, 初始过程中会传入一个最低的优先级(Precedence.LOWEST
)用于驱动整个AST语法树的构建. 当然了, 这时候词法单元读取模块也已经准备就绪了.Swift// 构建AST树主入口 public func parseMain() -> Expression? { return parseExpression(Precedence.LOWEST); }
-
通过
parseMain
函数进入的parseExpression()
函数中, 首先找的就是前置节点
, 通过词法单元读取模块
获取到第一个词法单元1
. 并且生成根据前置节点的类型
生成数字类型的AST前置节点
.getPrefixExpression
就不过多叙述了, 比较简单.Swiftlet funcName: String? = prefixParseFnHashMap[cur?.type ?? TokenType.None]; if (funcName == nil) { print("未找到AST节点构建函数名称") return nil } // 获取左节点 var leftExpression: Expression? = getPrefixExpression(funcName);
Swiftfunc getPrefixExpression(_ funcName: String?) -> Expression? { switch(funcName) { case "parseInteger" : return parseInteger() case "parsePrefixExpression": return parsePrefixExpression() case "parseGroupExpression": return parseGroupExpression() default: return nil } }
Swiftfunc parseInteger() -> Expression? { let number = Double(cur?.value ?? "0") let integerExpression = IntegerExpression(value: number) return integerExpression }
-
紧接着就是去找到中置节点, 这时候通过
peekPrecedence()
知道下一个词法单元为+
, 优先级较高, 满足优先级条件. 进入递归循环. 然后nextToken()
读取下一个词法单元+
, 然后通过调用parseInfixExpression()
尝试生成AST中的中置节点.Swiftwhile (!peekTokenIs(TokenType.EOF) && precedence.rawValue < peekPrecedence()?.rawValue ?? 0) { let infixParseFnName: String? = infixParseFnHashMap[peek?.type ?? TokenType.None]; if (infixParseFnName == nil) { print("未找到AST节点构建函数名称") return leftExpression; } nextToken(); leftExpression = parseInfixExpression(leftExpression); }
-
在中置节点生成函数
parseInfixExpression()
中, 由于当前的词法单元为+
, 左节点为前置节点1
, 我们可以直接构建出这一部分的AST语法树.Swiftlet infixExpression = InfixExpression(); infixExpression.left = left; infixExpression.operatorValue = cur?.value;
-
构建了中置节点的值和左节点, 我们尝试用
parseExpression()
递归的形式找到+的中置节点
的右节点, 我们需要先读取当前+
的优先级(Precedence.SUM
), 然后读取下一个节点.Swiftlet precedence: Precedence = Precedence.getPrecedence(cur?.type); nextToken(); infixExpression.right = parseExpression(precedence);
-
在
parseExpression()
寻找+的中置节点
的右节点, 首先, 就是获取数字词法单元4
生成前置节点, 然后往后读取, 发现是符号词法单元-
优先级与 当前符号词法单元+
的优先级相同, 所以就不进入while循环, 故+的中置节点
的右节点是前置节点4
.Swift// 参数优先级为 Precedence.SUM func parseExpression(_ precedence: Precedence) -> Expression? { let funcName: String? = prefixParseFnHashMap[cur?.type ?? TokenType.None]; if (funcName == nil) { print("未找到AST节点构建函数名称") return nil } // 获取左节点, 生成 数字前置节点 4 var leftExpression: Expression? = getPrefixExpression(funcName); // - 与 + 的优先级相同不进入while循环 while (!peekTokenIs(TokenType.EOF) && precedence.rawValue < peekPrecedence()?.rawValue ?? 0) { let infixParseFnName: String? = infixParseFnHashMap[peek?.type ?? TokenType.None]; if (infixParseFnName == nil) { print("未找到AST节点构建函数名称") return leftExpression; } nextToken(); leftExpression = parseInfixExpression(leftExpression); } // 返回 数字前置节点 4 return leftExpression }
-
这时候, 对于
中置节点+号
的AST语法树就构建完成了, 如图所示. -
然后外部又一次进行while循环, 这次找到的是
➖ 号
, 然后把中置节点+号 的AST语法树
整体作为➖中置节点的左节点传入.Swift// 这时候再次进入 减号➖ 的循环中 while (!peekTokenIs(TokenType.EOF) && precedence.rawValue < peekPrecedence()?.rawValue ?? 0) { let infixParseFnName: String? = infixParseFnHashMap[peek?.type ?? TokenType.None]; if (infixParseFnName == nil) { print("未找到AST节点构建函数名称") return leftExpression; } // 读取词法单元减号➖ nextToken(); // 这里的leftExpression是 加号➕ 的AST语法树 // + // ╱ ╲ // 1 4 leftExpression = parseInfixExpression(leftExpression); }
-
在
➖ 号的中置节点
构建过程中,中置节点+号 的AST语法树
作为其左节点,-
作为其值, 右节点继续通过parseExpression()
寻找.Swiftlet infixExpression = InfixExpression(); // 这里的left是 加号➕ 的AST语法树 // + // ╱ ╲ // 1 4 infixExpression.left = left; infixExpression.operatorValue = cur?.value;
-
和
中置节点+号
寻找右节点的逻辑是一样. 我们继续尝试用parseExpression()
递归的形式找到-的中置节点
的右节点, 我们需要先读取当前-
的优先级(Precedence.SUM
), 然后读取下一个节点.Swiftlet precedence: Precedence = Precedence.getPrecedence(cur?.type); nextToken(); infixExpression.right = parseExpression(precedence);
-
这次在
parseExpression()
就很简单了, 我们先构建了前置节点3
然后往后查找过程发现是结束词法单元EOF
, 我们直接返回前置节点3
即可.Swiftfunc parseExpression(_ precedence: Precedence) -> Expression? { let funcName: String? = prefixParseFnHashMap[cur?.type ?? TokenType.None]; if (funcName == nil) { print("未找到AST节点构建函数名称") return nil } // 构建 前置节点3 var leftExpression: Expression? = getPrefixExpression(funcName); // peekToken == EOF 不满足while循环条件 while (!peekTokenIs(TokenType.EOF) && precedence.rawValue < peekPrecedence()?.rawValue ?? 0) { let infixParseFnName: String? = infixParseFnHashMap[peek?.type ?? TokenType.None]; if (infixParseFnName == nil) { print("未找到AST节点构建函数名称") return leftExpression; } nextToken(); leftExpression = parseInfixExpression(leftExpression); } // 返回 前置节点3 return leftExpression }
-
返回了右节点之后, 我们就直接构建
➖减号的AST语法树
, 这里看一下整体的构建过程. -
➖减号的AST语法树
然后再次返回整体的循环, 发现当前的词法节点以及全部循环完成了, 所以就跳出了while循环, 返回最终的AST语法树. 这里就把整理的流程贴图如下所示. -
所以
1 + 4 - 3
形成的AST语法树是这样的. 如下图所示.
1 + 2 * 3
的AST语法树构建过程
相比于 1 + 4 - 3
的最终结果来说, 1 + 2 * 3
其中 乘法*
一定要比 加法+
优先级高. 最终应该是这样的 1 + (2 * 3)
. 也就是我们预想的AST语法树应该如下所示.
接下来, 我们就一起看一下 1 + 2 * 3
的AST语法树构建逻辑.
-
对于
1 + 2 * 3
一直到加法的中置节点寻找右节点之前的逻辑都是与先前一样的. 这里直接贴图了, 就不过多叙述代码了. -
接下来, 对于
中置节点加号+
需要通过parseExpression()
去寻找它自身的右节点. 这时候准备工作也要做好, 读取下一个词法单元2
, 获取当前加号的优先级(Precedence.SUM
).Swift// 当前加号的优先级为 Precedence.SUM let precedence: Precedence = Precedence.getPrecedence(cur?.type); // 下一个词法单元为 词法单元2 nextToken(); // 寻找 中置节点加号+ 的 右节点 infixExpression.right = parseExpression(precedence);
-
然后, 在
parseExpression()
就是先构建前置节点2
, 然后查看后一个词法单元, 发现是乘法符号*
, 乘法符号的优先级(Precedence.PRODUCT
) 要比 加法符号的优先级(Precedence.SUM
) 要高, 所以进入while循环中. 继续构建关于中置节点乘法*
的相关AST语法树.Swift// 当前优先级是 Precedence.SUM, 当前Token是 2 func parseExpression(_ precedence: Precedence) -> Expression? { let funcName: String? = prefixParseFnHashMap[cur?.type ?? TokenType.None]; if (funcName == nil) { print("未找到AST节点构建函数名称") return nil } // 构建左节点 前置节点2 var leftExpression: Expression? = getPrefixExpression(funcName); // 乘法符号的优先级比当前加号优先级高, 正常进入while循环 while (!peekTokenIs(TokenType.EOF) && precedence.rawValue < peekPrecedence()?.rawValue ?? 0) { let infixParseFnName: String? = infixParseFnHashMap[peek?.type ?? TokenType.None]; if (infixParseFnName == nil) { print("未找到AST节点构建函数名称") return leftExpression; } // 读取下一个词法单元 乘法符号* nextToken(); // 生成乘法符号的中置节点并且更新leftExpression leftExpression = parseInfixExpression(leftExpression); } return leftExpression }
Swiftlet infixExpression = InfixExpression(); infixExpression.left = left; infixExpression.operatorValue = cur?.value; // 当前的乘法符号 的AST语法树 // * // ╱ ╲ // 2 ?
-
紧接着, 就是寻找乘法AST语法树的右节点, 仍然是通过
parseExpression()
函数, 传入的Token则是词法单元3
, 乘法符号的优先级为Precedence.PRODUCT
,Swift// 乘法符号的优先级为 Precedence.PRODUCT let precedence: Precedence = Precedence.getPrecedence(cur?.type); // 读取词法单元3 nextToken(); infixExpression.right = parseExpression(precedence);
-
在这次乘法符号寻找右节点的
parseExpression()
中, 首先构建了前置节点3
, 由于看到下一个节点是结束词法单元EOF
, 所以不进入循环, 直接返回前置节点3
.Swiftfunc parseExpression(_ precedence: Precedence) -> Expression? { let funcName: String? = prefixParseFnHashMap[cur?.type ?? TokenType.None]; if (funcName == nil) { print("未找到AST节点构建函数名称") return nil } // 构建 前置节点3 var leftExpression: Expression? = getPrefixExpression(funcName); // peekToken == EOF 不满足while循环条件 while (!peekTokenIs(TokenType.EOF) && precedence.rawValue < peekPrecedence()?.rawValue ?? 0) { let infixParseFnName: String? = infixParseFnHashMap[peek?.type ?? TokenType.None]; if (infixParseFnName == nil) { print("未找到AST节点构建函数名称") return leftExpression; } nextToken(); leftExpression = parseInfixExpression(leftExpression); } // 返回 前置节点3 return leftExpression }
-
这时候也就构建完成了乘法的AST语法树部分了. 我们一起看一下整体的乘法符号的AST语法树构建过程.
-
由于已经遍历到了最后(遇到了
EOF
), 紧接着就跳出了 加法符号寻找右节点的parseExpression()
过程中的while循环. 并把 乘法符号的AST语法树作为 加法符号的右节点进行了添加.Swift// 这里是加法符号寻找右节点的递归方法 // 当前优先级是 Precedence.SUM, 当前Token是 2 func parseExpression(_ precedence: Precedence) -> Expression? { let funcName: String? = prefixParseFnHashMap[cur?.type ?? TokenType.None]; if (funcName == nil) { print("未找到AST节点构建函数名称") return nil } // 构建左节点 前置节点2 var leftExpression: Expression? = getPrefixExpression(funcName); // 乘法符号的优先级比当前加号优先级高, 正常进入while循环 while (!peekTokenIs(TokenType.EOF) && precedence.rawValue < peekPrecedence()?.rawValue ?? 0) { let infixParseFnName: String? = infixParseFnHashMap[peek?.type ?? TokenType.None]; if (infixParseFnName == nil) { print("未找到AST节点构建函数名称") return leftExpression; } // 读取下一个词法单元 乘法符号* nextToken(); // 生成乘法符号的中置节点并且更新leftExpression leftExpression = parseInfixExpression(leftExpression); } // 最终 leftExpression 是乘法符号的AST语法树 // * // ╱ ╲ // 2 3 return leftExpression }
上述代码就是下图中 红色的parseExpression()的内部过程.
-
最后返回初始那一层 由
parseMain()
进入的parseExpression()
过程, 也是已经遍历到了最后(遇到了EOF
), 跳出循环, 返回最终的AST语法树.Swift// 这里是由 `parseMain()` 进入的 `parseExpression()` func parseExpression(_ precedence: Precedence) -> Expression? { let funcName: String? = prefixParseFnHashMap[cur?.type ?? TokenType.None]; if (funcName == nil) { print("未找到AST节点构建函数名称") return nil } // 构建左节点 前置节点1 var leftExpression: Expression? = getPrefixExpression(funcName); // 构建了加法的AST语法树之后, 就退出了循环 while (!peekTokenIs(TokenType.EOF) && precedence.rawValue < peekPrecedence()?.rawValue ?? 0) { let infixParseFnName: String? = infixParseFnHashMap[peek?.type ?? TokenType.None]; if (infixParseFnName == nil) { print("未找到AST节点构建函数名称") return leftExpression; } nextToken(); leftExpression = parseInfixExpression(leftExpression); } // 最终 leftExpression 是加法符号的AST语法树 // + // ╱ ╲ // 1 * // ╱ ╲ // 2 3 return leftExpression }
-
这样我们对于数学表达式的
1 + 2 * 3
的 AST语法树构建过程就有整体的了解,最终输出的AST语法树如下所示.
中间代码生成与验证
对于上面的 1 + 4 - 3
和 1 + 2 * 3
的两个示例, 我们对PrattParser
构建AST语法树的过程有了大体的了解.
接下来就是中间代码生成过程(其实不太准确, 大体模拟吧~), 我们会直接输入一个结果对象(MInt
), 模拟中间代码的生成.
中间代码生成是由 Evaluator
来实现的, 其主要作用就是解析AST语法树, 生成中间代码结果对象(MInt
). 这部分也是很简单, 主要是通过 eval()
函数来递归解析AST语法树, 然后通过 op()
函数进行各种数学计算. 整体的计算也是由递归完成的.
-
eval()
函数中 主要有三个逻辑分支, 一个是数字的前置节点
一个是符号的前置节点
, 最后一个是中置节点
.数字的前置节点
和中置节点
没有什么好说的,符号的前置节点
主要应对于第一个前置节点带符号的情况例如-1
和+349
等等.Swiftpublic static func eval(_ node: Node?) -> MObj? { if let nodeValue = node as? IntegerExpression { // 纯数字节点逻辑 return MInt(nodeValue.value) } else if let nodeValue = node as? PrefixExpression { // 带符号的数字节点逻辑 if (nodeValue.operatorValue == "-") { return minus(node); } else if (nodeValue.operatorValue == "+") { return eval(nodeValue.right); } } else if let nodeValue = node as? InfixExpression { // 中置节点逻辑 let left = eval(nodeValue.left); let right = eval(nodeValue.right); return op(left, right, nodeValue.operatorValue); } return nil; }
-
op()
就是根据符号进行不同的数学运算, 整体逻辑比较简单, 这里就不过多叙述了.Swiftpublic static func op(_ left: MObj?, _ right: MObj?, _ operatorValue: String?) -> MObj? { if let leftValue = left as? MInt, let rightValue = right as? MInt { switch(operatorValue) { case "+" : return MInt(leftValue.number + rightValue.number) case "-" : return MInt(leftValue.number - rightValue.number) case "*" : return MInt(leftValue.number * rightValue.number) case "/" : return MInt(leftValue.number / rightValue.number) case "^" : return MInt(pow(leftValue.number, rightValue.number)) default: return nil; } } return nil; }
-
minus()
函数主要是用来处理带符号的前置节点情况. 整体逻辑也比较简单, 这里就不过多叙述了.Swiftpublic static func minus(_ node: Node?) -> MObj? { if let nodeValue = node as? PrefixExpression { let m : MObj? = eval(nodeValue.right); if let mValue = m as? MInt { if (nodeValue.operatorValue == "-") { mValue.number = -mValue.number } return mValue; } } return nil; }
最后, 我们就能看到最终的输出结果.
Swift
var code = "1+2*3"
var lexer: Lexer! = Lexer(code: code)
var parser: Parser! = Parser(lexer)
var expression: Expression? = parser.parseMain()
if let intObj = Evaluator.eval(expression) as? MInt {
print(intObj.toString())
}
总结
通过这篇博客详细大家对 PrattParser
解析器的前端工作有个大体的了解了. 希望看这篇博客是可以一边断点项目, 一边查看, 主要是递归过程比较绕, 希望有耐心看完.