4. 语法设计基础

在风控规则DSL的设计中,语法层决定了规则“长什么样”以及“如何被理解”。很多团队在初期只关注“能不能跑”,结果规则文件越写越乱:同一个含义有多种写法、优先级靠猜、错误提示指向不明。要构建可维护、可扩展的风控DSL,必须把词法、语法、语义、表达式、运算符与优先级这五个基础概念分层设计清楚。

一、核心原理:四层分离

DSL的解析执行通常分为四个层次,每一层只解决一类问题:

  • 词法(Lexical):把字符流切分为有意义的词法单元(),如标识符、数字、字符串、运算符、括号。它不关心组合是否合法,只关心“单词”是否拼写正确。
  • 语法(Syntax):把序列组织成抽象语法树(AST),定义“哪些组合是合法的”。例如 amount > 100 AND level = "高" 是合法表达式,而 amount > AND 100 是语法错误。
  • 语义(Semantic):在AST上做类型检查、变量绑定、函数签名校验。例如 amount 必须是数值字段,AND 两侧必须是布尔表达式。
  • 执行(Execution):遍历AST,结合运行时上下文求值,得到规则命中结果。

这四层分离的好处是:错误定位清晰、扩展成本低、测试可分层。词法错误提示“第3行第8列出现非法字符”,语法错误提示“缺少右括号”,语义错误提示“字段类型不匹配”,执行错误提示“除零异常”。

二、方法流程:从表达式到AST

以风控规则中常见的条件表达式为例,设计流程如下:

  1. 定义类型:IDENT、NUMBER、STRING、OP、LPAREN、RPAREN、AND、OR、NOT。
  2. 编写词法分析器,按最长匹配原则识别,跳过空白与注释。
  3. 编写递归下降解析器,按优先级从低到高组织函数:parseOr → parseAnd → parseNot → parseComparison → parsePrimary。
  4. 生成AST节点:BinaryExpr、UnaryExpr、Literal、Identifier、FunctionCall。
  5. 语义分析:遍历AST,检查字段是否存在、类型是否匹配、函数参数个数是否正确。
  6. 执行:用访问者模式或递归求值,传入事件上下文,返回布尔结果。

优先级设计建议:OR < AND < NOT < 比较运算符 < 加减 < 乘除 < 括号。括号优先级最高,用于显式覆盖默认结合顺序。

三、案例/场景:一条风控规则的完整解析

假设规则文本为:

amount > 5000 AND (userLevel = "高" OR riskScore >= 80) AND NOT blacklist

词法分析后得到序列:IDENT(amount)、OP(>)、NUMBER(5000)、AND、LPAREN、IDENT(userLevel)、OP(=)、STRING("高")、OR、IDENT(riskScore)、OP(>=)、NUMBER(80)、RPAREN、AND、NOT、IDENT(blacklist)。

语法分析按优先级构建AST:顶层是AND,左子节点是amount > 5000,中间是括号内的OR表达式,右子节点是NOT blacklist。语义分析检查amount为数值、userLevel为字符串、riskScore为数值、blacklist为布尔。执行时传入事件上下文,逐层求值。

层次 输入 输出 典型错误
词法 字符流 序列 非法字符、未闭合字符串
语法 序列 AST 缺少括号、运算符位置错误
语义 AST 带类型标注的AST 字段不存在、类型不匹配
执行 AST + 上下文 布尔结果 除零、空指针、超时

四、误区/复盘:优先级与结合性陷阱

常见误区一:把AND和OR设计成同一优先级。这会导致a OR b AND c的语义依赖解析器实现细节,不同版本可能结果不同。必须显式规定AND高于OR。

常见误区二:比较运算符链式使用,如a < b < c。在多数语言中这是非法的,DSL应直接报语法错误,而不是隐式转换为(a < b) < c。

常见误区三:忽略短路求值。风控规则中blacklist OR amount > 10000,若blacklist为真,不应再计算右侧,否则可能触发不必要的字段查询或异常。

复盘建议:在语法设计阶段就编写优先级表,并用单元测试覆盖所有运算符组合。对于NOT,明确其结合性为右结合,且优先级高于AND但低于比较运算符。对于括号,始终作为最高优先级处理。最终,DSL的语法设计不是“能解析就行”,而是要让规则编写者在不看文档的情况下也能写出符合直觉的表达式。

实践技巧:在AST节点中保留原始的位置信息(行号、列号),这样语义错误和执行错误都能精确定位到规则文本的具体位置,大幅降低排查成本。