这是我参与「第五届青训营」伴学笔记创作活动的第8天。青训营的第六次课程中展示了一个简易版规则引擎的实现,包含编译的词法分析、语法分析功能及执行功能,并且在课后作业中,要求使用Hertz框架实现一个HTTP服务的规则引擎系统。下面是我对规则引擎功能实现的回顾,以及个人HTTP规则引擎服务器的实现及效果展示。
规则引擎原理
编译过程
在规则引擎中,编译过程可以分为词法分析及语法分析两个子部分。在词法分析中,编译器将代码文件解析成若干个token的顺序组合,这里的token包括标识符、数值常量、字符常量、运算符等符号。在语法分析中,编译器将顺序排好的token数组解析成可用于执行的语法树,在这里使用了递归下降算法完成语法树的构建。
func Compiler(exp string) (*executor.Node, error) {
tokenScanner := compiler.NewScanner(exp)
tokens, err := tokenScanner.Lexer()
if err != nil {
return nil, err
}
parser := compiler.NewParser(tokens)
err = parser.ParseSyntax()
if err != nil {
return nil, err
}
astBuilder := compiler.NewBuilder(parser)
ast, err := astBuilder.Build()
if err != nil {
return nil, err
}
return ast, nil
}
上述代码展示了规则引擎的具体编译过程,其中:
- 通过
Lexer函数进行词法分析,将输入的字符序列解析成token序列; - 通过
ParseSyntax函数对token序列进行大致的正确性检查; - 通过
Build函数将token序列解析成用于执行的语法树,若执行成功则返回语法树根节点;
下面我们将分别介绍词法分析、语法分析的具体实现:
词法分析
func NewScanner(source string) *Scanner {
runes := []rune(source)
if len(runes) == 0 {
runes = append(runes, rune(eofRune))
}
return &Scanner{
source: runes,
length: len(runes),
ch: runes[0],
}
}
在词法分析中,将输入的字符序列初始化为Scanner类型,其中包含了字符序列本身、当前遍历位置及当前遍历字符。
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
}
在Lexer函数中,通过循环调用Scan函数每次解析字符序列中的一个token,并将其存储于tok数组,直到遍历完成Scan函数返回token.Eof。
func (scanner *Scanner) Scan() (token.Token, error) {
var tok token.Token
var err error
scanner.skipWhitespace()
tok.Position = scanner.position
switch ch := scanner.cur(); {
case isEof(ch):
tok.Kind = token.Eof
case isLetter(ch):
// if the first character is letter, this token must be an Identifier or BoolLiteral or otherwise
literal := scanner.scanIdentifier()
tok.Kind = token.Lookup(literal)
tok.Value = literal
// boolean?
if tok.Kind == token.BoolLiteral {
tok.Value = parseBool(literal)
}
...
return tok, err
}
Scan函数的流程即为遍历字符序列,其首先通过skipWhitespace跳过当前位置及之后存在的空白字符,并通过检查当前字符的类型判断:字符串是否遍历完成、是否为字母(标识符、布尔常量)、是否为数字(常量)等判断类型,在这里不一一阐述。
语法分析
func (p *Parser) ParseSyntax() error {
// '(a + (b > c)' is illegal
err := p.checkBalance()
if err != nil {
return err
}
// 'param1 + 100 param2' is illegal
var lastTok token.Token
state, err := lastTok.Kind.GetLexerState()
for p.hasNext() {
tok := p.next()
if !state.CanTransitionTo(tok.Kind) {
return fmt.Errorf("cannot transition token types from %s [%v] to %s [%v]",
lastTok.Kind.String(), lastTok.Value, tok.Kind.String(), tok.Value)
}
state, err = tok.Kind.GetLexerState()
if err != nil {
return err
}
lastTok = tok
}
// 'a + b +' is illegal
if !state.IsEOF() {
return errors.New("unexpected end of expression")
}
p.Reset()
return nil
}
在执行语法分析之前,需要执行PaseSyntax来检查token序列的大致的正确性,在这里包括了:
- 括号是否是封闭的;
- 相邻两个token之间的承接关系是否合理;、
- token序列是否以
Eof作为结尾;
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")
}
对于语法分析而言,该规则引擎使用plan函数开启递归下降算法,并得到完整的语法树。
func (p *precedence) plan(builder *Builder) (*executor.Node, error) {
var err error
var leftNode, rightNode *executor.Node
if p.nextPrecedence != nil {
leftNode, err = p.nextPrecedence.plan(builder)
if err != nil {
return nil, err
}
} else if p.planner != nil {
leftNode, err = p.planner(builder, p)
if err != nil {
return nil, err
}
}
for builder.parser.hasNext() {
tok := builder.parser.next()
if tok.Kind.IsEof() {
break
}
symbol, exist := p.validKindsToSymbols[tok.Kind]
if !exist {
break
}
rightNode, err = p.plan(builder)
if err != nil {
return nil, err
}
node := executor.NewNode(leftNode, rightNode, symbol, nil)
return node, nil
}
builder.parser.rewind()
return leftNode, nil
}
在该函数中,使用递归下降算法解析语法树,其沿着token优先级从低到高,贪心的构造语法树的左节点、运算符和右节点。通过该种方法,使得运算符优先级功能得以实现。具体流程如下图所示
执行过程
func (n *Node) Eval(parameters map[string]interface{}) error {
if n == nil {
return nil
}
if n.leftNode != nil {
err := n.leftNode.Eval(parameters)
if err != nil {
return err
}
}
if n.rightNode != nil {
err := n.rightNode.Eval(parameters)
if err != nil {
return err
}
}
if n.typeChecker != nil {
if !n.typeChecker(n.leftNode, n.rightNode) {
return n.symbol.formatTypeError(n.leftNode, n.rightNode)
}
}
ret, tp, err := n.operator(n, n.leftNode, n.rightNode, MapParameters(parameters))
if err != nil {
return err
}
n.value = ret
n.tp = tp
return nil
}
Eval函数用于将具体的标识符赋值(通过parameters参数),并执行相应的语法树。执行的过程也通过递归实现,其对将其左右子语法树的运算结果通过运算符对应的函数进行计算,并向上传递,直至整颗语法树被求解。
HTTP规则引擎服务器
添加表达式
type AddExpressionRequest struct {
Exp string `json:"exp"`
}
func HandleAddExpression(ctx context.Context, c *app.RequestContext) {
var req AddExpressionRequest
if err := c.Bind(&req); err != nil {
BindResp(c, ParamErrCode, err.Error(), nil)
return
}
res, err := dal.AddExpression(req.Exp)
if err != nil {
BindResp(c, ServiceErrCode, err.Error(), nil)
return
}
BindResp(c, SuccessCode, "Add success, expression id is return", res.ID)
}
/api/engine/exp/new/提供了保存表达式的功能,其将表达式通过dal.AddExpression方法存入数据库,并返回数据库条目所对应的ID。调用结果如下:
运行表达式
type RunExpressionRequest struct {
Id uint `json:"id"`
}
func HandleRunExpression(ctx context.Context, c *app.RequestContext) {
var req RunExpressionRequest
if err := c.Bind(&req); err != nil {
BindResp(c, ParamErrCode, err.Error(), nil)
return
}
exp, err := dal.GetExpressionByID(req.Id)
if err != nil {
BindResp(c, RuleNotExistCode, err.Error(), nil)
return
}
evaluatedExp, err := Compiler(exp.Exp)
if err != nil {
BindResp(c, CompileErrCode, err.Error(), nil)
return
}
params := make(map[string]interface{})
err = evaluatedExp.Eval(params)
if err != nil {
BindResp(c, RuleExecErrCode, err.Error(), nil)
return
}
resp, _ := evaluatedExp.GetVal()
BindResp(c, SuccessCode, SuccessMsg, resp)
}
/api/engine/exp/run/提供了执行对应ID表达式的接口,其由HandleRunExpression处理函数实现,其从数据库中读取对应ID的表达式,并通过词法分析、语法分析进行编译,并最后运行并返回结果。调用结果如下:
表达式列表
func HandleGetAllExpression(ctx context.Context, c *app.RequestContext) {
exps, err := dal.GetAllExpression()
if err != nil {
BindResp(c, ServiceErrCode, err.Error(), nil)
return
}
BindResp(c, SuccessCode, "success", exps)
}
/api/engine/exp/list/提供了列出所有表达式的接口,其由HandleGetAllExpression处理函数实现,其从数据库中获得所有表达式条目,并返回,执行结果如下。
删除表达式
func HandleDeleteExpression(ctx context.Context, c *app.RequestContext) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
BindResp(c, ParamErrCode, err.Error(), nil)
return
}
err = dal.DeleteExpressionByID(uint(id))
if err != nil {
BindResp(c, ServiceErrCode, err.Error(), nil)
return
}
BindResp(c, SuccessCode, SuccessMsg, nil)
}
/api/engine/exp/:id提供了删除对应ID表达式的接口,其从数据库中删除了对应ID的表达式条目。