当前位置: 首页 > news >正文

用ANTLR实现表达式词法和语法分析器

需求

易元平台的很多地方都需要表达式,如:字段公式、验证条款、动作的先决条件、视图表的表达式字段、高级查询条件等等。表达式需要支持算术表达式、逻辑表达式、函数表达式以及括号。

旧方案:正则表达式词法分析

在上一代平台采用正则表达式来进行词法分析,词法分析器代码如下:

/// <summary>
/// 文本格式表达式语法分析器
/// </summary>
/// <remarks>
/// @${XXX} - 参变量
/// @XXX - 参数
/// </remarks>
public sealed class LexicalAnalyzer
{private static readonly Regex REGEX =new Regex( @"\(|\)|/|[\+\-\=]{1,2}|\*|[<>!]=?|('([^']|'')*')|(@\$\{([^\}])*\})|(\$\{([^\}])*\})|[^,\u0000-\u0020'!<>=\-\*\+/\(\)]+",RegexOptions.Compiled | RegexOptions.IgnoreCase );private Match match;private Match previousMatch;private string expression;/// <summary>/// 初始化 <see cref="LexicalAnalyzer"/> 对象的新实例。/// </summary>/// <param name="expression">表达式</param>public LexicalAnalyzer( string expression ){this.expression = expression;}/// <summary>/// 当前符号/// </summary>public string Current{get { return match.Value; }}/// <summary>/// 上一个符号/// </summary>public string Previous{get{if ( previousMatch != null ){return previousMatch.Value;}return String.Empty;}}/// <summary>/// 上一个表达式段/// </summary>public Match PreviousMatch{get { return previousMatch; }}/// <summary>/// 移动到下一标识/// </summary>/// <returns></returns>public string MoveNext(){previousMatch = match;if ( match == null ){match = REGEX.Match( expression );}else{match = match.NextMatch();}if ( match.Success ){return Current;}return String.Empty;}
}

然后将词法分析器切分出的表达式片段进行语法分析处理,得到逆波兰表达式。再根据逆波兰表达式构建出表达式对象。

旧方案的不足

  1. 正则表达式本身的解析效率低。
  2. 无法处理一些高级语法(也许只是我写正则表达式的能力问题),如:字符串的转义字符,负号的处理等等。

新方案:采用ANTLR(语法分析器生成工具)

ANTLR是一款强大的语法分析器生成工具,可用于读取、处理、执行和翻译结构化的文本或二进制文件。它被广泛应用于学术领域和工业生产实践,是众多语言、工具和框架的基石。ANTLR使得语法和语言类应用程序的开发更加容易,只要写好语法规则,ANTLR就可以根据语法规则生成C#语言编写的词法分析器和语法分析器。

ANTLR的最新版是4.13.2,易元平台用的是3.1版本,据官方的说法,4比3更易于使用,语法规则编写更简单。

LL(*)文法

LL(*)可以算是形式语言中介绍的上下文无关文法的子集,具体理论笔者也没有深入研究过,这里直接给出已经写好的语法文件,有兴趣读者可以自行研究。因为是3.1版本,支持语法的友好度不如4版,有些表达写起来比较绕,来来回回摸索了好久。

grammar AntlrExpression;options{ output=AST; language=CSharp2; ASTLabelType=CommonTree; }tokens{MINUS;
}expression: or_expr;	or_expr: and_expr ( OROR^ and_expr )*;and_expr: not_expr ( ANDAND^ not_expr )*;not_expr: NOT^ not_expr | NOT^? logical_expr ;logical_expr: NOT^? compare_expr;compare_expr: like_expr ( ( LT | GT | EQEQ | LE | GE | NE )^ like_expr )?;like_expr: in_expr ( LIKE^ in_expr )?;in_expr: add_expr ( IN^ ( LPAREN! add_expr ( COMMA! add_expr )* RPAREN! | LPAREN! RPAREN!) )?;add_expr  : mul_expr ( ( ADD | SUB )^ mul_expr )*;  mul_expr  : minus_expr ( ( MUL | DIV | MOD )^ minus_expr )*  ;minus_expr: atom| '-' atom -> ^(MINUS atom);func_expr: ID^ LPAREN! or_expr (COMMA! or_expr)* RPAREN!| ID^ LPAREN! RPAREN!;atom  : BOOL_TRUE| BOOL_FALSE| NULLNULL| EMPTY| func_expr| INT| UINT| LONG| ULONG | FLOAT| DOUBLE | DECIMAL| P| STRING| VAR| LPAREN! expression RPAREN!   ;LPAREN  :   '('  ;  RPAREN  :   ')'  ; COMMA   :   ',';ADD :   '+'  ;  SUB :   '-'  ;  MUL :   '*'  ;  DIV :   '/';MOD :   '%';EQEQ    :   '=='  ;  NE  :   '!='  ;  LT  :   '<'  ;  LE  :   '<='  ;  GT  :   '>'  ;  GE  :   '>='  ;  NOT :   '!'  ;  ANDAND  :   '&&'  ;  OROR    :   '||'  ;  	P	:	'@' (ID':')? NID;VAR: '${' ('[' JOIN_SEQUENCE ']' | ID'[' JOIN_SEQUENCE ']' | ID':')? ID '}';fragment
JOIN_SEQUENCE : ( '+' (('f'|'i'|'l'|'r')'!')? ID ('#' ID?)? | '-' (('f'|'i'|'l'|'r')'!')? ID ('#' ID?)? '(' ID ')')+;BOOL_TRUE	: 'true';BOOL_FALSE	: 'false';NULLNULL	: 'null';EMPTY	: 'empty';IN	:	('i'|'I')('n'|'N');LIKE	:	('l'|'L')('i'|'I')('k'|'K')('e'|'E');ID  :	('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'0'..'9'|'_')*;INT :	('0'..'9')+;UINT :	'0'..'9'+('U'|'u');LONG :	'0'..'9'+('L'|'l');ULONG :	'0'..'9'+('U'|'u')('L'|'l');DECIMAL:   (('0'..'9')+ '.' ('0'..'9')* EXPONENT? |   '.' ('0'..'9')+ EXPONENT? |   ('0'..'9')+ EXPONENT) ('M'|'m')?|   ('0' | '1'..'9' '0'..'9'*)('M'|'m');FLOAT:   (('0'..'9')+ '.' ('0'..'9')* EXPONENT? |   '.' ('0'..'9')+ EXPONENT? |   ('0'..'9')+ EXPONENT|   ('0' | '1'..'9' '0'..'9'*)) ('F'|'f');DOUBLE:   (('0'..'9')+ '.' ('0'..'9')* EXPONENT? |   '.' ('0'..'9')+ EXPONENT? |   ('0'..'9')+ EXPONENT|   ('0' | '1'..'9' '0'..'9'*)) ('D'|'d');NID  :	('a'..'z'|'A'..'Z'|'0'..'9'|'_')+;STRING:  '"' ( ESC_SEQ | ~('\\'|'"') )* '"';fragment
EXPONENT : ('e'|'E') ('+'|'-')? ('0'..'9')+ ;fragment
HEX_DIGIT : ('0'..'9'|'a'..'f'|'A'..'F') ;fragment
ESC_SEQ:   '\\' ('b'|'t'|'n'|'f'|'r'|'\"'|'\\')|   UNICODE_ESC|   OCTAL_ESC;fragment
OCTAL_ESC:   '\\' ('0'..'3') ('0'..'7') ('0'..'7')|   '\\' ('0'..'7') ('0'..'7')|   '\\' ('0'..'7');fragment
UNICODE_ESC:   '\\' 'u' HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT;WS  :   (' '|'\t'|'\r'|'\n')+ { Skip(); }     ;ERROR_CHAR : . ; 

生成C#的解析器

ANTLR是用Java编写的,所以要运行ANTLR需要Java运行时。上面的语法文件中已经指明language=CSharp2语言为C#,所以生成时会自动生成AntlrExpressionLexer.cs(词法分析器)和AntlrExpressionParser.cs(语法分析器)两个类。

使用解析器

利用解析器可以把表达式文本分析称表达式Token树,再遍历Token树生成易元平台中的表达式,代码如下图所示:

// 分析表达式文本
private static Expression InnerParse(Kernel kernel, string expressionString)
{try{ICharStream charStream = new ANTLRStringStream(expressionString);AntlrExpressionLexer lexer = new AntlrExpressionLexer(charStream);CommonTokenStream tokens = new CommonTokenStream(lexer);AntlrExpressionParser parser = new AntlrExpressionParser(tokens);parser.TreeAdaptor = new CommonTreeAdaptor();AstParserRuleReturnScope<CommonTree, IToken> expressionReturn = parser.expression();CommonTree tree = expressionReturn.Tree;return Create(kernel, null, tree);}catch(Expression.ExpressionParsePositionException ex){// 分析详细错误throw new ExpressiveParseException(ex.Message, ex) { Markdown = BuildExceptionMessage(expressionString, ex) };}catch(Exception ex){throw new ExpressiveException(Formatter.String(Resources.Ex_Ea_Expressive_ExpressionParser_ParseError, expressionString), ex);}
}// 创建易元平台的表达式
private static Expression Create(Kernel kernel, Expression parent, CommonTree tree)
{try{Expression expression = TokenMapping.Create(kernel, tree.Token, tree);expression.ParseChildren(kernel, parent, tree, Create);return expression;}catch (ExpressiveParseException ex){Expression.ExpressionParsePositionException eppe;if (TrySyntaxError(tree, Resources.Ex_Ea_Expressive_ExpressionParser_SyntaxError, ex, out eppe)){// 可以分析细节信息,然后封装后抛出throw eppe;}throw new ExpressiveParseException(Resources.Ex_Ea_Expressive_ExpressionParser_SyntaxError);}
}

上面仅列出部分关键代码,代码已在Gitee上开源,查看完整的代码点击这里:ExpressionParser.cs

常规函数和中缀函数

常规函数的形式是:

FunctionName(arg1, arg2, ..., argN)

常规函数可以任意增加,只要函数名符合语法要求即可。

中缀函数是把首个操作数放前面,函数名在中间,形式如下:

arg1 FunctionName(arg2, ..., argN)
// 比如
arg1 In (1, 2, 3)

中缀函数目前只有In和Like,是在语法文件中限定的,不改语法规则文件的话,不能增加中缀函数。后期考虑调整语法规则,增加扩展中缀函数支持。

效果展示

任意表达式计算

表达式计算1

表达式计算2

条件表达式友好配置

条件配置

http://www.jsqmd.com/news/814847/

相关文章:

  • 船载AIS的Class A、Class B和接收器到底怎么选?一篇讲清休闲帆船、渔船和小货船的设备配置指南
  • 杭州AI智能体开发技术解析与靠谱服务商盘点 - 奔跑123
  • Python通达信数据获取终极指南:如何免费获取A股市场数据
  • 2026济南婚纱摄影拍摄全流程体验测评 - charlieruizvin
  • 开源阅读鸿蒙版:打破限制,打造属于你的终极数字阅读世界
  • 新手采购优选!2026塑胶跑道材料排行榜 翻新专用、高性价比、验收无忧 - 极欧测评
  • Halo 专业版功能介绍
  • 2026衢州黄金回收门店测评|奢响佳领衔六大机构优选 - 天天生活分享日志
  • 别再纠结了!RTL8367系列五款千兆交换机芯片怎么选?从封装到功能一次讲清
  • 2026公考培训怎么选?从这三点入手不踩坑 - 品牌排行榜
  • 2026最新|论文AIGC率降重攻略:实测从59%降到6%的5款降AI工具与6大手动技巧 - 降AI实验室
  • Claude推理接口低延迟优化秘技:FastAPI异步中间件+缓存穿透防护+请求批处理(仅限内部团队泄露版)
  • 终极指南:如何利用boardgame.io事件驱动架构实现游戏逻辑完美解耦
  • 如何助力安防设备线上快速打开市场?智能制造网带来低成本获客全攻略 - 品牌推荐大师
  • 跨越设计鸿沟:从Altium Designer到Cadence的封装与符号迁移实战
  • 杭州软件开发服务商技术能力拆解与企业选型指南 - 奔跑123
  • 2026年5月邯郸靠谱美术集训画室推荐榜|世骅学本:十二年办学底蕴 + 名师授课 + 全链路升学保障 - damaigeo
  • 如何配置Aura:完整设置指南与最佳实践
  • 瑞祥商联卡回收全攻略 - 购物卡回收找京尔回收
  • 别再只用默认配置了!手把手教你用nohup后台启动Minio并自定义账号密码(附日志查看技巧)
  • RFID借阅柜-学校RFID借阅柜源头生产厂家推荐 - 聚澜智能
  • 告别手机热点!实测特斯拉Model 3用USB无线网卡搭建‘永久’车载WiFi,看视频、OTA升级全搞定
  • 暗黑破坏神2存档编辑器终极指南:免费在线工具轻松定制你的游戏角色
  • 和信通购物卡如何高效1分钟回收,“懒人”攻略分享 - 可可收
  • ClawRouter:基于x402协议的LLM智能路由与微支付系统解析
  • 终极指南:如何用pinyinjs轻松实现汉字拼音互转
  • 从SORT到DeepSORT:多目标跟踪中卡尔曼滤波与匈牙利算法的演进与实战
  • 工业设备好物推荐 配电柜顶部排风扇,低噪高能效,安装超便捷
  • 3步掌握微信聊天记录导出:永久保存你的数字记忆
  • 软件厂商突然要审计,你们公司 IT 资产管理能扛得住吗