|
| 1 | +--- |
| 2 | + |
| 3 | +--- |
| 4 | + |
| 5 | +# ANTLR:从语法规则到代码生成 |
| 6 | + |
| 7 | +> 今天周五,最近在忙其他事情,有一段时间没写博客了,打开编辑器,想记录一下关于antlr这个功能强大的 语法分析器生成器。 |
| 8 | +
|
| 9 | +## ANTLR是什么 |
| 10 | + |
| 11 | +首先介绍一下ANTLR,其全称 Another Tool for Language Recognition,是一个功能强大的 语法分析器生成器。它的作用是根据你定义的一套 语法规则(Grammar),自动生成用于 词法分析(Lexer) 和 语法分析(Parser) 的 Java、Python、C++ 等语言代码。简单来说就是,你告诉ANTLR要识别什么规则的结构 |
| 12 | +,其便会帮你构建出对应的Token(Lexer),语法树(Parse),并通过监听者模式(Listener) or 访问者模式(Visitor)无侵入的对语法树局部进行定制化操作,而不用关心整个词法、语法树的构建与访问等情况。 |
| 13 | + |
| 14 | +**关于词法和语法、语义等知识概念,可以去学习一下编译原理相关知识进行补充了解,笔者虽然大学的时候编译原理这一门课学得还行,也写过词法分析、语法分析、语义分析到最终解释器的代码,但是时间一久,相关内容也忘了不少,而且这门课程确实也很难,值得且需要花费时间与精力去学校** |
| 15 | + |
| 16 | +## 适用场景 |
| 17 | + |
| 18 | +- 🌐 自定义语言或 DSL 的解析器(如 SQL 方言、脚本语言等) |
| 19 | + |
| 20 | +- 🛠️ 静态代码分析工具(比如代码质量检查器) |
| 21 | + |
| 22 | +- 🧠 编译器前端(语法分析器) |
| 23 | + |
| 24 | +- 📚 学习语言构造和编译原理 |
| 25 | + |
| 26 | +- 📝 配置文件解析(如自定义规则语言) |
| 27 | + |
| 28 | +## 背景 |
| 29 | + |
| 30 | +在我们项目中,有许多用到领域特定语言(DSL)以及用户自定义表达式解析计算等需求。但在老项目随着功能需求的不断迭代,业务复杂度不断提高。原有的方案已经不满足现有的场景。例如,需要访问深层嵌套的语法树的局部节点做特定处理,需要在整个分析过程中构建语法树进行处理上下文等等场景。如果在原有的方案上开发成本和心智负担过于沉重。因此,选择了通过ANTLR自定义语法规则进行方案替换优化。 |
| 31 | + |
| 32 | +## ANTLR 简单使用 |
| 33 | + |
| 34 | +### ANTLR 的核心组件 |
| 35 | + |
| 36 | +在使用ANTLR前,先介绍一下ANTLR相关的几个核心概念来组织语法和生成代码: |
| 37 | + |
| 38 | +| 组件 | 说明 | |
| 39 | +| ---------------- | ----------------------- | |
| 40 | +| `.g4` 文件 | ANTLR 的语法规则文件,定义语法结构 | |
| 41 | +| Lexer(词法分析器) | 将字符流拆解成 **Token** 流 | |
| 42 | +| Parser(语法分析器) | 根据语法规则组织 Token,构造语法树 | |
| 43 | +| Visitor/Listener | 对语法树进行遍历,做语义分析、执行、代码生成等 | |
| 44 | + |
| 45 | +### 一个简单的表达式语法例子 |
| 46 | + |
| 47 | +因此,首先我们需要创建一个*.g4的语法规则文件,定义词法和语法规则(实际生产中根据场景,可以将词法与语法.g4文件拆分来进行维护,避免冗余等情况,便于维护)。 |
| 48 | + |
| 49 | +下面便创建一个支持 + 和 * 运算的简单计算器。 |
| 50 | + |
| 51 | +```bash |
| 52 | +grammar Expr; |
| 53 | + |
| 54 | +expr: expr op=('*'|'/') expr # MulDiv |
| 55 | + | expr op=('+'|'-') expr # AddSub |
| 56 | + | INT # Int |
| 57 | + | '(' expr ')' # Parens |
| 58 | + ; |
| 59 | + |
| 60 | +INT: [0-9]+; |
| 61 | +WS: [ \t\r\n]+ -> skip; |
| 62 | +``` |
| 63 | + |
| 64 | +**解释:expr 是递归定义,支持括号嵌套和操作符优先级。每个语法规则后加的 # Name 是“标签名”,用于 Visitor 模式中区分子节点。** |
| 65 | + |
| 66 | +> 具体以及其他用法可以参考官方文档:https://github.com/antlr/antlr4/tree/master |
| 67 | +
|
| 68 | +### 从语法到代码:使用流程 |
| 69 | + |
| 70 | +1. 安装 ANTLR 工具 |
| 71 | + |
| 72 | +```bash |
| 73 | +brew install antlr # macOS |
| 74 | +# or 下载 jar:https://www.antlr.org/download.html |
| 75 | +``` |
| 76 | + |
| 77 | +如果习惯使用idea的朋友可以在idae上安装对应插件也是一样的。 |
| 78 | + |
| 79 | +2. 生成代码 |
| 80 | + |
| 81 | +```bash |
| 82 | +antlr4 Expr.g4 -visitor -package com.example.expr |
| 83 | +``` |
| 84 | + |
| 85 | +输出包括但不局限于下面的文件: |
| 86 | +- ExprLexer.java |
| 87 | +- ExprParser.java |
| 88 | +- ExprBaseVisitor.java |
| 89 | +- ExprVisitor.java |
| 90 | + |
| 91 | +3.编写 Visitor 解析表达式 |
| 92 | + |
| 93 | +```java |
| 94 | +public class EvalVisitor extends ExprBaseVisitor<Integer> { |
| 95 | + @Override |
| 96 | + public Integer visitAddSub(ExprParser.AddSubContext ctx) { |
| 97 | + int left = visit(ctx.expr(0)); |
| 98 | + int right = visit(ctx.expr(1)); |
| 99 | + return ctx.op.getType() == ExprParser.PLUS ? left + right : left - right; |
| 100 | + } |
| 101 | + |
| 102 | + @Override |
| 103 | + public Integer visitMulDiv(ExprParser.MulDivContext ctx) { |
| 104 | + int left = visit(ctx.expr(0)); |
| 105 | + int right = visit(ctx.expr(1)); |
| 106 | + return ctx.op.getType() == ExprParser.MUL ? left * right : left / right; |
| 107 | + } |
| 108 | + |
| 109 | + @Override |
| 110 | + public Integer visitInt(ExprParser.IntContext ctx) { |
| 111 | + return Integer.parseInt(ctx.getText()); |
| 112 | + } |
| 113 | + |
| 114 | + @Override |
| 115 | + public Integer visitParens(ExprParser.ParensContext ctx) { |
| 116 | + return visit(ctx.expr()); |
| 117 | + } |
| 118 | +} |
| 119 | +``` |
| 120 | + |
| 121 | +4. 执行并测试 |
| 122 | + |
| 123 | +```java |
| 124 | +ANTLRInputStream input = new ANTLRInputStream("2 * (3 + 4)"); |
| 125 | +ExprLexer lexer = new ExprLexer(input); |
| 126 | +CommonTokenStream tokens = new CommonTokenStream(lexer); |
| 127 | +ExprParser parser = new ExprParser(tokens); |
| 128 | +ParseTree tree = parser.expr(); |
| 129 | + |
| 130 | +EvalVisitor visitor = new EvalVisitor(); |
| 131 | +System.out.println(visitor.visit(tree)); // 输出 14 |
| 132 | +``` |
| 133 | + |
| 134 | +至此,一个简单的计算表达式便开发完毕了,ANTLR给我们封装和屏蔽了整个词法,语法规则的构建过程。因此我们只需要专注于编写规则文件以及根据需求使用Listener或Visitor去访问语法树实现我们的业务诉求即可。 |
| 135 | + |
| 136 | +## 小结 |
| 137 | + |
| 138 | +上面只是一个简单的介绍和例子,实际的开发场景中会遇到奇奇怪怪、各式各样的诉求:可能需要支持上下文敏感语法,可能需要处理优雅的错误提示,可能希望和已有项目无缝集成,甚至还有人拿它写代码转换、重写器、生成器……但不管是哪一种,用 ANTLR 的过程总会逼着你直面语言本质的混乱与秩序。 |
| 139 | + |
| 140 | +越往深处走,就越发现语法这东西——远不只是“规则”的堆砌。它是抽象、是妥协、是试图给混乱世界套上的结构化“框”。当站在一棵语法树面前凝视它的结构时,可能会突然意识到:语言本身,就是我们人类思维的一种折射。 |
| 141 | + |
| 142 | +而这时,再联想到当下火热的人工智能与自然语言处理,很多问题似乎也不再只是技术性的。是的,我们试图让机器理解人类语言,而 ANTLR 则是在更底层,帮我们定义“什么才算是一门语言”。 |
| 143 | + |
| 144 | +当然,最后我想写的是:ANTLR 给了我们一种可能,但并没有告诉我们哪种方式一定是“对”的,也没告诉我们什么时候该停下,什么时候该继续拆分与组合。它只提供了一个工具,而不是答案。因地制宜,适配自己的诉求,或许才是最佳实践。可以写一个优雅的 DSL,也可以做一个粗糙但可用的语法识别器;可以深入语言结构,也可以浅尝辄止。这没有标准答案,只有当下最适合的方案。 |
| 145 | + |
| 146 | + |
0 commit comments