语法树构造
基本概念
语法解析过程
语法设计
TimeConvert(get_order_info.create_time + time_hour(24), "yyyy/mm/dd", "UTC") TimeConvert(get_order_info.create_time + time_hour(24), "yyyy/mm/dd", "UTC") + time_day(2)
这是时间戳转换函数的例子, 这里面包含了字面量, 常量, 运算符以及函数格式.
这样做是为了让引擎在运行时知道这个变量的具体值, 当有新的转换需求时, 只需要新增函数, 而不需要修改底层基本数据结构和DB表
类似这样
定义语法的首要任务是确定基本的元素, 包括基础数据类型、关键字和操作符。
get_order_info.create_time 中, “.” 作为一个selector表达式, 从一个父变量中推导出一个新变量, 函数本身也是推导出一个变量。
+ , - , * , / 这些操作符在此之前已经支持, 针对Float 和 Int 类型. 而现在可以推广到日期类型和字符串. 参考Scala 中的语法, 对数组或者集合类型来说, + - * / 也是可以有具体含义的.
字符串和数字等常量, 和所有编程语言一样, 没什么特别.
有了以上信息, 我们可以开始设计一下语法, 比如有哪些Token, 在字节流中抽取出Token实际上执行的是一系列的匹配的规则. 因此在Token的定义上往往能用到类似正则表达式的语法.
// function.g4
grammar function;
// Tokens
MUL: '*';
DIV: '/';
ADD: '+';
SUB: '-';
IDENTIFIER : ([a-zA-Z_] [a-zA-Z_0-9]*)('.'([a-zA-Z_] [a-zA-Z_0-9]*))* ;
DECIMAL : [0-9]+ ( '.' [0-9]+ )? ;
NUMBER : [0-9]+;
WHITESPACE : [ \r\n\t]+ -> skip;
// Rules
start : expression EOF;
expression
: IDENTIFIER '(' expression (',' expression)* ')' # Function
| expression op=('*'|'/') expression # MulDiv
| expression op=('+'|'-') expression # AddSub
| DECIMAL # Decimal
| IDENTIFIER # Identifier
| STRING # String
;
STRING
: '"' (ESC | SAFECODEPOINT)* '"'
;
fragment ESC
: '\\' (["\\/bfnrt] | UNICODE)
;
fragment UNICODE
: 'u' HEX HEX HEX HEX
;
fragment SAFECODEPOINT
: ~ ["\\\u0000-\u001F]
;
fragment HEX
: [0-9a-fA-F]
;
以 TimeConvert(get_order_info.create_time + time_hour(24), "yyyy/mm/dd", "UTC") 语句为例, 得到的语法树如下
语义重构
变量推导
现在的能支持自定义转换函数,更多变量类型,字段选择。
grammar variate;
// Tokens
MUL: '*';
DIV: '/';
ADD: '+';
SUB: '-';
DOT: '.';
LP: '(';
RP: ')';
COMMA:',';
WHITESPACE : [ \r\n\t]+ -> skip;
// Rules
start: expression EOF;
expression
: IDENTIFIER LP expression (COMMA expression)* RP # Function
| expression op=(MUL|DIV) expression # MulDiv
| expression op=(ADD|SUB) expression # AddSub
| expression (DOT PROPERTY)+ # Selector
| IDENTIFIER # Identifier
| STRING # String
| NUMBER # Number
;
PROPERTY: FUNCTION | IDENTIFIER;
FUNCTION : IDENTIFIER LP PARAM (COMMA PARAM)* RP;
PARAM
: IDENTIFIER
| STRING
| NUMBER
;
IDENTIFIER : ([a-zA-Z_] [a-zA-Z_0-9]*) ;
NUMBER : [0-9]+ ( DOT [0-9]+ )? ;
STRING
: '"' (ESC | SAFECODEPOINT)* '"'
;
fragment BINARY: MUL|DIV|ADD|SUB;
fragment ESC
: '\\' (["\\/bfnrt] | UNICODE)
;
fragment UNICODE
: 'u' HEX HEX HEX HEX
;
fragment SAFECODEPOINT
: ~ ["\\\u0000-\u001F]
;
fragment HEX
: [0-9a-fA-F]
;
List推导
可以参考Scala 或者python 语法
UDF(用户自定义函数)
明显的收益是,简化配置,代码热部署,api编排,让业务方自定义函数,而不是把业务方的代码逻辑全部搬到api-proxy,减少重复开发。
比如:
比较纠结的点在于,应该自行设计脚本语法,还是直接提供python 或者 golang 语法给到用户。
ANTLR 在github上已经提供了各种语言的解析器产生式(然而一看issue 370 个,调试了一顿,终于搞定了Golang 的解析器)。然后简单写了一个执行逻辑,Golang 极限套娃完成。
剩下的问题,就是把已有的自定义函数搬进来,然后实现for 循环和 条件语句的逻辑(这很简单)。一些常用的包比如strings, slice 包里的方法也得搬进来(都是苦力活,也许可以找Chat-GPT , cursor 帮忙写)。不考虑性能的话,直接用反射吧。
较难的问题,函数之间的依赖分析,计算图的生成,执行优化,函数过多的时候语法树的存储和加载,缓存淘汰策略等。