这是我参与「第五届青训营 」伴学笔记创作活动的第 8 天
本次编程学习的基本环境配置如下
OS: macOS 13.1
IDE: Goland 2022.3
Go Version: 1.18
今天来读一读课程给的代码实例 github.com/qimengxingy…
文件结构
尽管本节的主题是规则引擎的实现, 但事实上代码描述的是一个表达式语句的解析和运行. 在编译原理课上, 这是一个常用的例子
| 目录或文件 | 说明 |
|---|---|
biz/dal/ | 初始化并连接数据库, 将数据写入MYSQL数据库中 |
biz/handler/ | HTTP服务, 支持通过HTTP输入规则语句并执行 |
complier/ | 编译, 包括词法分析、语法分析生成抽象语法树 |
executor/ | 执行, 抽象语法树的执行 |
image//test/ | 图像,测试文件夹 |
token/ | 定义Token的类型和识别规则 |
main.go | 启动HTTP服务 |
docker* | 主机安装了MySQL服务器, 不使用Docker启动MySQL服务 |
代码中的主要结构体(类型)的定义和相关方法
token包
Kind
Kind用于表示Token的类型, 之后的const块枚举了所有可能出现的类型, 如标识符类型和字面量(常量)、括号、四则运算符号、比较运算符、逻辑运算符等.
如果词法分析器面对的是某种编程语言, 事实上还应该包括关键字(Keyword, 就是符合标识符词法但被系统保留的一部分词汇, 通常享有单独的类型分配), 在这个Demo中, true和false就是这样的词汇, 但其被视为BoolLiteral, 即布尔字面量.
// Kind Represents all valid types of tokens that a token can be.
type Kind int
const (
Illegal Kind = iota // 0
Eof
KindBegin
Identifier // variables
BoolLiteral // true, false
IntegerLiteral // 12345
FloatLiteral // 123.45
StringLiteral // "abc"
/*
* single character operator
* */
OpenParen // (
CloseParen // )
...
)
token/kind.go中给出了类型的字符串表示以及字符串到类型的反查字典, 方便之后的调试.
Token
Token结构体用于表达源程序中的一个具有独立语义的词汇, Kind表示Token的值, Value表示静态推导得到的值, Position表示词汇在源程序中的位置.
type Token struct {
Kind Kind
Value interface{}
Position int
}
LexerState
LexerState结构体用于辅助判定语法是否符合规则, 在lexer.go中还包含了相关的validLexerStates变量用map[Kind]LexerState定义了所有的语法规则, 其实该变量定义的语法规则是不完备的, 因为它只定义了某种词素之后还可以接哪些类型, 而忽略了当前已经掌握的上下文信息.
// LexerState 用于辅助判定语法是否符合规定
type LexerState struct {
isEOF bool // 表示是否可以作为结束标志
validNextKinds []Kind // 该符号后还可以接什么符号
}
compiler包
Engine.g4
这个文件是Antlr所能够操作的源文件。Antlr (ANother Tool for Language Recognition) 是一个强大的跨语言语法解析器,可以用来读取、处理、执行或翻译结构化文本或二进制文件。它被广泛用来构建语言,工具和框架。Antlr可以从语法上来生成一个可以构建和遍历解析树的解析器。
笔者对该软件知之甚少, 目前收集到的信息如下:
- 源代码中大写字母开头的是允许的
Token, 之后有该类词素的匹配规则 fragment用于告诉ANTLR, 这个符号不是一个词法符号,它只会被其他的词法规则使用, 如示例源代码中的IdentifierStart等- 小写字母开头的一系列语句就是定义的语法规则, 简单做了一点注释, 不一定对. 如果之后对
Antlr有更深入的理解, 再来修改 - 可以用
IDEA/Goland等IDE的Antlr插件可视化某一Token序列的语法树
// 语法规则, 以小写字母开头, 这应该是一个表达式语法
expr: logOr EOF;
// 或的优先级最低, 可以按照 逻辑或 将表达式分成多段
logOr: logOr '||' logAnd | logAnd;
logAnd: logAnd '&&' logNot | logNot;
logNot: '!' logNot | cmp;
// 比较是子表达式的比较, 表达式可以按照比较运算符分为多段
cmp: cmp '>' add | cmp '>=' add | cmp '<' add | cmp '<=' add | cmp '==' add | cmp '!=' add | add;
// 乘法优先级高, 可以按乘除符号将表达式分成几个子串, 如 1 + 2 * 3 / 2 + 5 / 2 - 3 可以分为 1, 2 * 3 / 2, 5 / 2, 3 四段
add: add '+' mul | add '-' mul | mul;
mul: mul '*' pri | mul '/' pri | mul '%' pri | pri;
// 初始: 常数(各种字面量)标识符和子表达式
pri: BooleanLiteral|IntegerLiteral|FloatLiteral|StringLiteral|Identifier|'('expr')';
Scanner结构体
Scanner结构体用于保存规则表达式字符串, 可通过NewScanner获取一个特定字符串的Scanner对象, 通过Scan()方法从Scanner对象中读取一个Token, Lexer()方法循环调用Scan()方法, 将获取到的token保存到切片中并返回.
type Scanner struct {
source []rune // 规则表达式字符串
position int // 遍历规则表达式过程中的位置
length int // 规则表达式字符串, 用于判断是否扫描结束
ch rune // position 位置对应的字符
}
Scanner结构体的核心方法就是Scan, 该方法的流程大致如下.
-
忽略字符串开头的空白字符, 找到第一个非空白字符(
skipWhitespace()), 注意对字符串的读取会导致Scanner结构体对象中的position等成员变化; -
读取结构体的下一个字符, 根据读取到的字符的具体值执行以下操作之一
- 如果是
EOF,则返回token为EOF - 如果读取到的是一个字母, 那么要返回的token可能是一个标识符、关键字, 调用
scanIdentifier()方法读取之后的字符, 直到遇到的字符不再是字母或数字, 将读取到的字符封装成字符串, 判定读取到的字符串是否为关键字, 如果不是关键字, 则该token为标识符 - 如果读取到的是一个数字或者
., 则向后读取字符, 直到读取到的字符不再是数字或., 封装成浮点数或整数写入Token(这里会查找字符串中是否有多个点, 如果出现多个点, 则褚翠); - 以上是标识符、关键字和数值字面量的处理。
- 对于
"+-*/%()'等, 这些字符是确定的单一运算符, 直接获取对应的类型写入Token即可; - 而对于
<>!=等, 这些符号之后还可以有"=", 因此需要多读一个字符确定具体的类型; - 最后是
&, |等, 目前是不支持位运算的, 因此必须等到&&,||才能确定类型, 否则就输出错误
- 对于
- 如果是
-
返回Token, 如果出现错误, 还需要封装错误和Token一起返回
Parser结构体
看样子就是对Scanner.Lexer()方法的输出结构的封装(Token序列), 其核心方法是ParseSyntax(), 通过对Token序列的分析, 判断其是否符合语法规则
type Parser struct {
tokens []token.Token
index int
tokenLength int
}
ParseSyntax()的流程如下:
- 排除由于括号不匹配导致的错误(
checkBalance()), 如果括号不匹配, 直接返回错误; - 迭代, 分析后一个字符是否被前一个字符兼容(通过
Kind.GetLexerState()获取前一字符的兼容符号列表LexerState, 然后通过CanTransitionTo方法判定) - 判断最终状态, 如果Token不能作为
EOF, 语法错误(排除q + b +这样的语法错误
Builder
type precedence struct {
validKindsToSymbols map[token.Kind]executor.Symbol // 当前优先级的token类型
nextPrecedence *precedence // 更高优先级的
planner planner
}
// planner
type planner func(builder *Builder, curPre *precedence) (*executor.Node, error)
// ... builder
type Builder struct {
rootPlanner *precedence
parser *Parser
}
写在最后
本文主要分析了token包和compiler包的内容, executor包的内容还需要一些时间整理.