Antlr4入门学习及实用案例(二)

Antlr4入门学习及实用案例(二)

Antlr4入门学习及实用案例(二)

ANTLR4 (ANother Tool for Language Recognition) 是一个功能强大的解析器生成器,可以用来读取、处理、执行或格式化结构化文本或二进制文件。它被广泛用于构建语言、工具和框架。

常用的一些语法

上一篇文章中我们了解了 ANTLR4 基础使用,ANTLR4 提供了许多非常实用的语法特性,这能帮助开发者写出更简洁、更强大、更易于维护的 .g4 语法文件。

这里作一个简要地总结,以帮助初学者了解一些常用的语法规则及格式。

元素标签 (Element Labels)

在 ANTLR 的语法规则中,可以为匹配的元素(无论是一个expr规则还是一个Token词法单元)起一个别名。

这个语法格式是:labelName(别名)= element ,将匹配到的 element 实例进行命名。

规则标签 (#label)

允许开发者为语法规则中的分支添加标签,可在 Visitor/Listener 中为每个标签生成更精确的独立访问方法。

expr:

left=expr op=('*'|'/') right=expr # MulDivExpr

| INT

;

词法命令 (Lexer Commands)

这些命令写在词法规则的末尾,使用 -> 符号,用来控制词法分析器的行为。

语法: LEXER_RULE : ... -> command,比如:WS : [ \t]+ -> skip; 跳过处理空格和制表符。

EBNF 风格的量词

ANTLR 支持类似扩展巴科斯范式 (EBNF) 的量词,可以方便地表示“零次或多次”、“一次或多次”以及“可选”。

* (星号):零次或多次。例:rule: ID (',' ID)*; 匹配一个 ID,后面跟着零个或多个由逗号分隔的 ID ( a, a,b, a,b,c )

+ (加号):一次或多次。例:rule: INT+; 匹配一个或多个连续的整数 ( 1, 1 2 3)

? (问号):零次或一次 (可选)。例:rule: 'public'? 'class' ID; public 关键字是可选的 ( class A, public class A)

片段规则 (Fragment Rules)

fragment 定义可复用的词法片段,但它本身不生成Token, 适当使用可提高词法规则的复用性和可读性。

语法: fragment NAME: ...;,比如:fragment DIGIT: [0-9]; 定义一个数字片段被其他词法规则引用或组合。

练手的实用案例

支持“代数表达式”的计算器

实现一个支持代数表达式的计算器,使用 ANTLR4 进行语法解析。该计算器将能够处理基本的四则运算、变量赋值、括号优先级以及小数运算。通过构建语法分析树,实现 Visitor 模式来遍历和计算表达式的值。

定义计算器语法

定义该计算器的语法规则,主要流程为处理运算优先级以及执行变量存储等步骤,语法关键流程如下所示:

grammar Calculator;

prog: stat+;

stat: expr NEWLINE # Expression

| ID '=' expr NEWLINE # Assign

| NEWLINE # Empty

;

expr: expr op=('*' | '/') expr # MulDiv

| expr op=('+' | '-') expr # AddSub

| number # num

| ID # id

| '(' expr ')' # parens

;

通过 IDEA 的 ANTLR4 插件,输入如下表达式:

a=1.3

b=3.1

c=7

a+b*(c-5)

查看 ANTLR 创建的解析语法树,可清晰地查看递归的每一步解析流程,解析结果也是准确的:

实现计算逻辑

语法树遍历时需要处理算术表达式的运算、变量的赋值,主要的实现方法如下:

private final Map memory = new HashMap<>();

// # Assign 标签:获取赋值表达式的值,并存入变量

@Override

public Number visitAssign(CalculatorParser.AssignContext ctx) {

String id = ctx.ID().getText();

Number value = visit(ctx.expr());

memory.put(id, value);

return value;

}

// # id 标签:遍历id节点,进行读取变量值

@Override

public Number visitId(CalculatorParser.IdContext ctx) {

String id = ctx.ID().getText();

if (memory.containsKey(id)) {

return memory.get(id);

}

return 0;

}

// # MulDiv 标签:执行乘法或除法的计算逻辑

@Override

public Number visitMulDiv(CalculatorParser.MulDivContext ctx) {

Number left = visit(ctx.expr(0));

Number right = visit(ctx.expr(1));

if (ctx.op.getType() == CalculatorParser.MUL) {

return mathTool.mul(left, right);

}

return mathTool.div(left, right);

}

测试运算结果

编写上述表达式的测试用例,执行该计算器的运算,正确完成计算并返回结果值:

@Test

public void testAlgebraicCalculate(){

final String expr = "a=1.3\n" +

"b=3.1\n" +

"c=7\n" +

"a+b*(c-5)\n";

Number result = calculate(expr);

assertEquals(7.5, result);

}

private Number calculate(String expr) {

CalculatorLexer lexer = new CalculatorLexer(CharStreams.fromString(expr));

CommonTokenStream tokenStream = new CommonTokenStream(lexer);

CalculatorParser parser = new CalculatorParser(tokenStream);

CalculatorParser.ProgContext tree = parser.prog();

CalculatorEvalVisitor eval = new CalculatorEvalVisitor();

return eval.visit(tree);

}

基于Map的内存数据库

解析 SQL 语法是 ANTLR 这类解析器工具的一个重要实现方式,在 Java 生态中,ANTLR4 和 JavaCC 是两种主流的 SQL 解析工具。其中,JavaCC 因被 Apache Calcite 采用而被广泛使用,诸多基于 Calcite 的大数据处理框架(如 Apache Hive、Flink SQL、Spark SQL等)均间接依赖 JavaCC 实现 SQL 解析。

定义 SQL Parser

使用 ANTLR4 定义一个 SimpleSql.g4 语法文件,实现简单的 SQL CRUD 操作,主要语法流程如下:

grammar SimpleSql;

statement: (insertStatement | selectStatement | updateStatement | deleteStatement) ';'? ;

insertStatement:

INSERT INTO tableName=ID '(' columns=idList ')' VALUES '(' values=valueList ')'

;

selectStatement:

SELECT columns=selectList FROM tableName=ID (WHERE whereClause)?

;

updateStatement:

UPDATE tableName=ID SET assignments (WHERE whereClause)?

;

deleteStatement:

DELETE FROM tableName=ID (WHERE whereClause)?

;

说明:

定义 insertStatement, selectStatement 等四个增删改查规则。

whereClause 只作简单条件语句的实现,只支持 列名 >= 值 这种的条件形式。

词法规则定义关键字(如 INSERT)、标识符(ID)、字面量(STRING, NUMBER)等。

通过 IDEA 的 ANTLR4 插件,输入SQL的查询语句 select name, age from users where uid = 1,查看 ANTLR 创建的解析语法树及SQL解析流程:

实现 SQL CRUD 操作

依然使用 Visitor 模式来遍历语法树,实现对 Map 数据结构的内存数据库进行操作。

模拟数据库结构,使用一个嵌套的 Map 结构表达:

// 数据库: Map<表名, 表>

final Map>> database;

实现 Insert 操作,主要逻辑如下:

@Override

public Object visitInsertStatement(SimpleSqlParser.InsertStatementContext ctx) {

// 获取表名,如果表不存在则自动创建

String tableName = ctx.tableName.getText();

database.putIfAbsent(tableName, new ArrayList<>());

List> table = database.get(tableName);

// 获取列名字段

List columns = ctx.columns.ID().stream()

.map(ParseTree::getText)

.collect(Collectors.toList());

// 获取VALUES值

List values = ctx.values.value().stream()

.map(this::visitValue) // 使用 visitValue 方法转换值

.collect(Collectors.toList());

if (columns.size() != values.size()) {

throw new RuntimeException("列的数量和值的数量不匹配!");

}

// 创建新行

Map newRow = new HashMap<>();

for (int i = 0; i < columns.size(); i++) {

newRow.put(columns.get(i), values.get(i));

}

// 插入新行

table.add(newRow);

log.info("表 {} 成功插入1行", tableName);

return 1;

}

同理实现 Select 操作:

@Override

public Object visitSelectStatement(SimpleSqlParser.SelectStatementContext ctx) {

String tableName = ctx.tableName.getText();

List> table = database.get(tableName);

if (table == null) {

log.info("表 {} 不存在", tableName);

return new ArrayList<>();

}

// 构建WHERE条件的过滤器

Predicate> whereFilter = row -> true; // 默认不过滤

if (ctx.whereClause() != null) {

whereFilter = createPredicate(ctx.whereClause().expression());

}

// 构建行数据的结果集

List> results = new ArrayList<>();

for (Map row : table) {

if (whereFilter.test(row)) {

Map resultRow = new HashMap<>();

if (ctx.columns.getText().equals("*")) {

resultRow.putAll(row);

} else {

SimpleSqlParser.IdListContext idCtx = ctx.columns.idList();

for (org.antlr.v4.runtime.tree.TerminalNode idNode : idCtx.ID()) {

String colName = idNode.getText();

resultRow.put(colName, row.get(colName));

}

}

results.add(resultRow);

}

}

return results;

}

其余的 Update、Delete 操作等详细的代码展示在文章末尾的项目仓库链接中,可点击查看具体的实现流程。

测试 SQL 语句

编写 SQL Parser 的测试用例,执行 CRUD 操作,验证 SQL 执行的结果准确性:

private final Map>> database = new HashMap>>();

@BeforeEach

public void setupTest() {

execute("INSERT INTO users (id, name, age) VALUES (1, 'Alice', 30);");

execute("INSERT INTO users (id, name, age) VALUES (2, 'Bob', 25);");

execute("INSERT INTO users (id, name, age) VALUES (3, 'Charlie', 35);");

}

@Test

public void testSimpleSqlInsert() {

for (Map user : database.get("users")) {

Integer id = (Integer) user.get("id");

if (id == 1) {

assertEquals("Alice", user.get("name"));

assertEquals(30, user.get("age"));

} else if (id == 2) {

assertEquals("Bob", user.get("name"));

assertEquals(25, user.get("age"));

} else if (id == 3) {

assertEquals("Charlie", user.get("name"));

assertEquals(35, user.get("age"));

} else {

fail("Invalid id");

}

}

printResults(database.get("users"));

}

@Test

public void testSimpleSqlSelect() {

String expr = "select * from users where id = 1";

List> users = (List>) execute(expr);

assertEquals(1, users.size());

assertEquals(1, users.get(0).get("id"));

assertEquals("Alice", users.get(0).get("name"));

assertEquals(30, users.get(0).get("age"));

printResults(users);

}

@Test

public void testSimpleSqlSelect2() {

String expr = "select name, age from users where id = 1";

List> users = (List>) execute(expr);

assertEquals(1, users.size());

assertNull(users.get(0).get("id"));

assertEquals("Alice", users.get(0).get("name"));

assertEquals(30, users.get(0).get("age"));

printResults(users);

}

@Test

public void testSimpleSqlUpdate() {

String expr = "update users set name = 'Dylan' where id = 1";

Object result = execute(expr);

assertEquals(1, result);

assertEquals("Dylan", database.get("users").get(0).get("name"));

printResults(database.get("users"));

}

@Test

public void testSimpleSqlDelete() {

String expr = "delete from users where id = 1";

Object result = execute(expr);

assertEquals(1, result);

assertEquals(2, database.get("users").size());

assertEquals(2, database.get("users").get(0).get("id"));

printResults(database.get("users"));

}

private Object execute(String expr) {

SimpleSqlLexer lexer = new SimpleSqlLexer(CharStreams.fromString(expr));

CommonTokenStream tokenStream = new CommonTokenStream(lexer);

SimpleSqlParser parser = new SimpleSqlParser(tokenStream);

SimpleSqlParser.StatementContext tree = parser.statement();

SimpleSqlEvalVisitor eval = new SimpleSqlEvalVisitor(database);

return eval.visit(tree);

}

运行测试类,方法成功执行通过,可看到如下输出,清晰地展示了 HashMap 如何根据 SQL 语句被执行增、删、改、查操作:

小总结

在本篇文章中,我们使用 ANTLR4 实现了两个小案例。ANTLR 十分强大,我们可以自定义各种语法规则来实现复杂的业务逻辑,通过学习 ANTLR4 ,可以帮助我们更好理解各类开源组件库的源代码逻辑,也可以帮助我们构建符合业务流程的动态规则引擎的实现。

希望本文能够激发你对 ANTLR4 的兴趣,并在探索技术的旅程中提供有价值的参考。

————————————————

Interpreter 1 - Google 文档

简介 - ANTLR 4 简明教程 - 开发文档 - 文江博客

Antlr4系列(一):语法分析器学习 - 知乎

antlr4/doc/parser-rules.md at master · antlr/antlr4 · GitHub

文章案例项目代码:antlr4-simple-demo

https://github.com/Cyanty/antlr4-simple-demo

相关数据

泉州驾照科目二考试一般要多长时间
空落落什么意思?读音是luo还是lao 心里空落落的很难受想哭
真的有“瓩”这个字,除了它还有多个多音节汉字,现在已经不用

友情链接