这是我参与「第五届青训营 」伴学笔记创作活动的第 12 天,今天讲解如何简单实现一个规则引擎.
一. 设计一个规则引擎
1.前置准备
- go语言环境
- Windows Docker 安装
- 其他linux版本可以在左面教程选择
- Arch Linux Docker安装
- 安装Docker
如果安装了yay也可以sudo pacman -S dockeryay docker - 启动Docker
- 启动Docker
sudo systemctl start docker.service - 设置开机启动
sudo systemctl enable docker.service - 关闭开机启动
sudo systemctl disable docker.service - 工作用户加入 docker 组
一般情况下,docker会自动创建docker,直接将当前工作用户加入即可
重启docker(restart)并重新登录该用户,或者重启即可可生效sudo gpasswd -a $USER docker
如果没有创建,需要手动创建,再加入工作组sudo groupadd docker
- 启动Docker
- 安装Docker
- 安装docker-compose工具
- 项目
脚本执行成功,则环境可以支持项目的执行git clone https://github.com/qimengxingyuan/young_engine.git chmod a+x ./setup.sh ./setup.sh
2. 设计目标
设计一个规则引擎,支持特定的词法,运算符,数据类型,数据类型和优先级.并且支持基于以上预定义语法的规则表达式的编译和执行.
-
词法(合法Token)
- 参数 : 由字母数字下划线组成eg: _ab2,user_name
- 布尔值 : true , false
- 字符串 : "abcd" , 'abcd' , `abcd`
- 十进制int : 1234
- 十进制float : 123.5
- 预定义运算符 : + -
-
运算符
- 一元运算符 : + -
- 二元运算符 : + - * / % < > <= >= != ==
- 逻辑运算符 : && || !
- 括号 : ( )
-
数据类型
- 字符串
"abc"'def' - 十进制int
123 - 十进制float
123.4 - bool
true - 变量
id
- 字符串
-
优先级
3.词法分析
- 设计语法分析的状态机:
同心圆会向后再看一位
4.语法分析
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 ;
add : add '+' mul | add '-' mul | mul;
mul : mul '+' pri | mul '/' pri | mul '%' pri | pri;
pri : BooleanLiteral | IntengerLiteral | FloatLiteral | StringLiteral | Identifier | '('expr')' '
- 优先级的表达
type precedence struct {
validSymbols []Symbol // 当前优先级支持的运算符类型
nextPrecedence *precedence // 更高优先级的
planner planner // 当前优先级的处理函数
}
- 语法树结构
- 一元运算符 : 左子树为空,右子树为右操作符.
- 二元运算符 : 左子树为左操作符,右子树为右操作符.
- 括号 : 左子树为空,右子树为内部表达式的AST(抽象语法树).
5. 语法树执行与类型检查
-
语法树的执行
预先定义好每种操作符的执行逻辑.
对抽象语法树进行后续遍历执行,即:- 先执行左子树,得到左节点的值
- 再执行右子树,得到右节点的值
- 最后根据根节点的操作符执行得到根节点的值
-
类型检查
检查时机: 执行时检查.
检查方法: 在一个节点的左右子节点执行完成后,分别校验左右子节点的类型是否符合对应操作符的类型检查预设规则.
例如:- '>' 符号要求左右子节点的值都存在且为 int 或者 float .
- '|' 符号要求左节点为空且右节点的值为 bool .
二. 规则引擎的实现
-
项目结构
-
先定义词法(token):
...
-
再进行词法分析(compiler/scanner.go):
type Scanner struct { source []rune // 规则表达式字符串 position int // 遍历规则表达式过程中的位置 length int // 规则表达式字符串, 用于判断是否扫描结束 ch rune // position 位置对应的字符 } ... func (scanner *Scanner) Lexer() ([]token.Token, error) { tokens := make([]token.Token, 0) var err error var tok token.Token for { tok, err = scanner.Scan() tokens = append(tokens, tok) if err != nil || tok.Kind == token.Eof { break } } return tokens, err } ...Scan()为读取一个类型为Scanner的当前位置的字符,并写入tok
-
然后就是语法分析(compiler/parser.go)
type Parser struct { tokens []token.Token index int tokenLength int } ... func NewParser(tokens []token.Token) *Parser { return &Parser{ tokens: tokens, tokenLength: len(tokens), } } ...然后进行语法检查(checkBalance(),ParseSyntax() ),避免后续进行不必要的操作.
例如类型'(a + (b > c)' ,括号不对称,是不合法的;'param1 + 100 param2' is illegal;'a + b +' is illegal.
保证构建语法树不会受到干扰 -
构建语法树(compiler/builder.go)
type Builder struct { rootPlanner *precedence parser *Parser } ... func (b *Builder) Build() (*executor.Node, error) { if b.parser == nil { return nil, errors.New("parse is nil") } if b.rootPlanner != nil { // TODO 树优化 return b.rootPlanner.plan(b) } return nil, errors.New("build failed") } ...