接下来我们通过计算器示例展示Antlr4的更多用法。
该示例在官方文档上也有,不过我添加了几个功能。

计算器需求

  1. 单行注释,以 // 开头
  2. 支持整数、小数与字符串
  3. 支持赋值
  4. 支持一个打印函数,可以输入任意个参数
  5. 支持加、减、乘、除的运算

就想这样:

  1. // 这是注释
  2. a = 1
  3. b = 4.5
  4. c = a * b
  5. print("结果:", c)

语法规则

让我们看看语法规则文件:
有几个注意的地方:

  1. 如果一个语法规则有多种匹配,则使用 | 代表分支;
  2. #表示分支后的标签,之后会根据标签自动生成对应的函数;
  3. 如果有关键字和通用的标识符,要把关键字放到标识符之前,比如这里的 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);

  1. 这是解析的语法树:<br />![截屏2021-12-06 下午9.21.54.png](https://cdn.nlark.com/yuque/0/2021/png/1123541/1638796947753-add6c4e3-749e-4113-9a99-ce08868594f4.png#clientId=u04acf0ed-530b-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u4f4c4d09&margin=%5Bobject%20Object%5D&name=%E6%88%AA%E5%B1%8F2021-12-06%20%E4%B8%8B%E5%8D%889.21.54.png&originHeight=402&originWidth=1796&originalType=binary&ratio=1&rotation=0&showTitle=false&size=95701&status=done&style=none&taskId=u2d199c9f-ec97-48b0-ab69-caff882af64&title=)
  2. <a name="UvHi9"></a>
  3. # 语法解析
  4. 还是和之前一样,使用访问者模式。<br />更多代码请参考示例工程。
  5. ```go
  6. type MyVisitor struct {
  7. parser.BaseExprVisitor
  8. props map[string]interface{} // 保存变量的值
  9. }
  10. func NewMyVisitor() *MyVisitor {
  11. return &MyVisitor{
  12. props: make(map[string]interface{}),
  13. }
  14. }

注意:使用了键值对保存变量的值

错误处理

默认情况下,ANTLR将所有的错误消息送至标准错误(standard error),不过我们可以通过实现接口ANTLRErrorListener来改变这些消息的目标输出和内容。
继承 DefaultErrorListener,重写 syntaxError 方法,该方法接收各种错误,错误的位置和错误的内容。

  1. package main
  2. import (
  3. "fmt"
  4. "github.com/antlr/antlr4/runtime/Go/antlr"
  5. )
  6. type MyErrorListener struct {
  7. antlr.DefaultErrorListener
  8. }
  9. func NewMyErrorListener() *MyErrorListener {
  10. return &MyErrorListener{}
  11. }
  12. func (m *MyErrorListener) SyntaxError(recognizer antlr.Recognizer,
  13. offendingSymbol interface{},
  14. line,
  15. column int,
  16. msg string,
  17. e antlr.RecognitionException) {
  18. parser := recognizer.(antlr.Parser)
  19. stack := parser.GetRuleInvocationStack(parser.GetParserRuleContext())
  20. fmt.Println("rule stack:", stack)
  21. fmt.Println("line:", line)
  22. fmt.Println("column:", column)
  23. fmt.Println("msg:", msg)
  24. }
  1. func main() {
  2. ...
  3. // 移出原来的错误监听
  4. p.RemoveErrorListeners()
  5. // 添加我们自定义的错误监听
  6. p.AddErrorListener(NewMyErrorListener())
  7. ...
  8. }

隐藏通道

我们之前都是把注释忽略掉,那么就无法获取注释的内容。可是如果我们想通过注释做一些操作,比如生成文档,有没有办法获取呢?答案是肯定的。
可以将注释送入另一个通道,正常的词法符号默认在0通道。
获取另一个通道的内容就能获取注释了。
uTools_1638954496548.png

  1. func printComments(lexer *parser.ExprLexer) {
  2. tokens := lexer.GetAllTokens()
  3. fmt.Println("注释:")
  4. for _, t := range tokens {
  5. if t.GetChannel() == antlr.TokenHiddenChannel {
  6. fmt.Println(t.GetText())
  7. }
  8. }
  9. }

大部分功能就介绍到这里了,更多功能请看文档。
完整代码