#8 规则引擎设计和实现-2 | 青训营笔记

119 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 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中, truefalse就是这样的词汇, 但其被视为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可以从语法上来生成一个可以构建和遍历解析树的解析器。

笔者对该软件知之甚少, 目前收集到的信息如下:

  1. 源代码中大写字母开头的是允许的Token, 之后有该类词素的匹配规则
  2. fragment用于告诉ANTLR, 这个符号不是一个词法符号,它只会被其他的词法规则使用, 如示例源代码中的IdentifierStart
  3. 小写字母开头的一系列语句就是定义的语法规则, 简单做了一点注释, 不一定对. 如果之后对Antlr有更深入的理解, 再来修改
  4. 可以用IDEA/GolandIDEAntlr插件可视化某一Token序列的语法树

image.png

// 语法规则, 以小写字母开头, 这应该是一个表达式语法
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, 该方法的流程大致如下.

  1. 忽略字符串开头的空白字符, 找到第一个非空白字符(skipWhitespace()), 注意对字符串的读取会导致Scanner结构体对象中的position等成员变化;

  2. 读取结构体的下一个字符, 根据读取到的字符的具体值执行以下操作之一

    1. 如果是EOF,则返回token为EOF
    2. 如果读取到的是一个字母, 那么要返回的token可能是一个标识符、关键字, 调用scanIdentifier()方法读取之后的字符, 直到遇到的字符不再是字母或数字, 将读取到的字符封装成字符串, 判定读取到的字符串是否为关键字, 如果不是关键字, 则该token为标识符
    3. 如果读取到的是一个数字或者., 则向后读取字符, 直到读取到的字符不再是数字或., 封装成浮点数或整数写入Token(这里会查找字符串中是否有多个点, 如果出现多个点, 则褚翠);
    4. 以上是标识符、关键字和数值字面量的处理。
      1. 对于"+-*/%()'等, 这些字符是确定的单一运算符, 直接获取对应的类型写入Token即可;
      2. 而对于<>!=等, 这些符号之后还可以有"=", 因此需要多读一个字符确定具体的类型;
      3. 最后是&, |等, 目前是不支持位运算的, 因此必须等到&&,||才能确定类型, 否则就输出错误
  3. 返回Token, 如果出现错误, 还需要封装错误和Token一起返回

Parser结构体

看样子就是对Scanner.Lexer()方法的输出结构的封装(Token序列), 其核心方法是ParseSyntax(), 通过对Token序列的分析, 判断其是否符合语法规则

type Parser struct {
   tokens      []token.Token
   index       int
   tokenLength int
}

ParseSyntax()的流程如下:

  1. 排除由于括号不匹配导致的错误(checkBalance()), 如果括号不匹配, 直接返回错误;
  2. 迭代, 分析后一个字符是否被前一个字符兼容(通过Kind.GetLexerState()获取前一字符的兼容符号列表LexerState, 然后通过CanTransitionTo方法判定)
  3. 判断最终状态, 如果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包的内容还需要一些时间整理.