接下来我们通过计算器示例展示Antlr4的更多用法。
该示例在官方文档上也有,不过我添加了几个功能。
计算器需求
- 单行注释,以
//
开头 - 支持整数、小数与字符串
- 支持赋值
- 支持一个打印函数,可以输入任意个参数
- 支持加、减、乘、除的运算
就想这样:
// 这是注释
a = 1
b = 4.5
c = a * b
print("结果:", c)
语法规则
让我们看看语法规则文件:
有几个注意的地方:
- 如果一个语法规则有多种匹配,则使用
|
代表分支; #
表示分支后的标签,之后会根据标签自动生成对应的函数;- 如果有关键字和通用的标识符,要把关键字放到标识符之前,比如这里的
print
```shell grammar Expr;
// 起始规则,语法分析的起点 // EOF 表示 end of file, 文件的结尾 prog: (stat NEWLINE | NEWLINE) EOF ;
// 语句由表达式、赋值表达式和打印语句组成 stat: expr # expression | ID ‘=’ expr # assign | printStat # print ;
// 表达式有多种情况 // 用分支表示 // 每个分支后添加标签,用于区分不同的分支 expr: ‘( ‘expr ‘)’ # child | expr op=(MUL|DIV) expr # mulDiv | expr op=(ADD|MINUS) expr # addMin | INT # int | FLOAT # float | ID # id ;
// 打印函数 printStat : PRINT ‘(‘ printParam (‘,’ printParam)* ‘)’ ; // 打印函数的参数 printParam : expr|STRING ;
// 打印函数 // 关键字一定要放在ID之前 PRINT : ‘print’ ;
// 乘法符号 MUL : ‘*’ ; // 除法符号 DIV : ‘/‘ ; // 加法符号 ADD : ‘+’ ; // 减法符号 MINUS : ‘-‘ ;
// 匹配标识符 ID: [a-zA-Z]+ ; // 匹配整数 INT : DIGIT+ ; // 匹配小数 FLOAT : DIGIT+ ‘.’ DIGIT+ ; // 换行 NEWLINE: ‘\r’ ? ‘\n’ ; // 字符串 STRING : ‘“‘ .*? ‘“‘ ;
// 数字 // fragment 表示只能被词法分析引用 fragment DIGIT: [0-9] ;
// 丢弃空白字符 WS: [ \t]+ -> skip ; // 单行注释,将注释送入隐藏通道,用于后续处理 LINECOMMENT : ‘//‘ ~[\r\n]* -> channel(HIDDEN);
这是解析的语法树:<br />
<a name="UvHi9"></a>
# 语法解析
还是和之前一样,使用访问者模式。<br />更多代码请参考示例工程。
```go
type MyVisitor struct {
parser.BaseExprVisitor
props map[string]interface{} // 保存变量的值
}
func NewMyVisitor() *MyVisitor {
return &MyVisitor{
props: make(map[string]interface{}),
}
}
注意:使用了键值对保存变量的值
错误处理
默认情况下,ANTLR将所有的错误消息送至标准错误(standard error),不过我们可以通过实现接口ANTLRErrorListener来改变这些消息的目标输出和内容。
继承 DefaultErrorListener
,重写 syntaxError
方法,该方法接收各种错误,错误的位置和错误的内容。
package main
import (
"fmt"
"github.com/antlr/antlr4/runtime/Go/antlr"
)
type MyErrorListener struct {
antlr.DefaultErrorListener
}
func NewMyErrorListener() *MyErrorListener {
return &MyErrorListener{}
}
func (m *MyErrorListener) SyntaxError(recognizer antlr.Recognizer,
offendingSymbol interface{},
line,
column int,
msg string,
e antlr.RecognitionException) {
parser := recognizer.(antlr.Parser)
stack := parser.GetRuleInvocationStack(parser.GetParserRuleContext())
fmt.Println("rule stack:", stack)
fmt.Println("line:", line)
fmt.Println("column:", column)
fmt.Println("msg:", msg)
}
func main() {
...
// 移出原来的错误监听
p.RemoveErrorListeners()
// 添加我们自定义的错误监听
p.AddErrorListener(NewMyErrorListener())
...
}
隐藏通道
我们之前都是把注释忽略掉,那么就无法获取注释的内容。可是如果我们想通过注释做一些操作,比如生成文档,有没有办法获取呢?答案是肯定的。
可以将注释送入另一个通道,正常的词法符号默认在0通道。
获取另一个通道的内容就能获取注释了。
func printComments(lexer *parser.ExprLexer) {
tokens := lexer.GetAllTokens()
fmt.Println("注释:")
for _, t := range tokens {
if t.GetChannel() == antlr.TokenHiddenChannel {
fmt.Println(t.GetText())
}
}
}
大部分功能就介绍到这里了,更多功能请看文档。
完整代码