本文所用到的工具:Go、Goland、ANTLR4插件和一个包含Go Mod的空工程。

为什么不用VSCode? 因为它的Antlr4插件不够好用。

为什么使用Golang作为示例程序 因为之前没有找到合适的 Golang 示例

简介

ANTLR4 是一款强大的语法分析器生成工具,可用于读取、处理、执行和翻译结构化的文本或二进制文件。
简单的说,它可以识别结构化语言,比如SQL、JSON、Python或者你自己定义的编程语言。
它可以作为数据读取器、语言解释器和翻译器。
它本身使用Java写的,但是可以生成各种语言的接口。

学习途径

  • 官网
  • 《ANTLR4权威指南》
  • 官方文档 (英文不好的推荐看书,即上一条)
  • 官方示例:grammars-v4

    安装

    官网上有安装方法,不过使用Goland的插件更方便。
    在Goland Preference -> Plugins中搜索 antlr 然后安装

    工作流程

  1. 编写 .g4 文件描述要解析的语言规则
  2. 根据 .g4 生成特定语言的解析文件(可以是Java、C++、Golang等等)
  3. 创建入口程序,依赖生成的解析文件,进行语法分析

入门 - 图1

开始

ANTLR元语言

为了实现一门编程语言,我们需要识别出一门特定语言的所有的有意义的语句、词组和子词组。
例如,我们能够将输入的 sp=100;识别为一个赋值语句,这意味着我们需要知道sp是被赋值的目标,100是要被赋予的值。
我们使用ANTLR元语言描述其他语言的语法,保存在 .g4 文件中。
分析语言的过程分为:

  • 词法分析
  • 语法分析

    词法分析

    将相关的词法符号归类,例如INT(整数)、ID(标识符)、FLOAT(浮点数)等。

    语法分析

    建造一种名为语法分析树(parse tree)或者句法树(syntax tree)的数据结构,该数据结构记录了语法分析器识别出输入语句结构的过程,以及该结构的各组成部分。
    截屏2021-11-28 上午9.20.28.png

    编写g4文件

    我们编写一个 g4 文件来描述一些规则。
    比如我想识别下面格式的文本:

    1. {1, 2, 3}

    需要的规则:

  • 一对花括号

  • 花括号中有数字
  • 每个数字以逗号分割
  • 数字与逗号之间可以有任意个空格

创建一个名为 ArrayInit.g4 的文件

  1. // 以 grammar 开头,后接文件名
  2. // 表示这是语法文件
  3. grammar ArrayInit;
  4. // 一条名为init的语法规则,匹配花括号,逗号分开的value
  5. init : '{' value (',' value)* '}' ;
  6. // value也是一条语法规则,它是数字
  7. value : INT ;
  8. // INT 词法规则,数字是0到9之间,由一个以上组成
  9. INT : [0-9]+ ;
  10. // 忽略空格、制表符和换行
  11. WS : [ \t\r\n]+ -> skip ;

这里的规则匹配和正则表达式很类似,最好先去了解正则表达式。

测试语法

创建文件 demo.txt ,内容如下:

  1. {1, 2, 3}
  • �在 Goland 中打开 ArrayInit.g4 文件
  • 打开左下角的 Antlr preview 视图,选择刚创建的 demo.txt 文件
  • init 规则那里右键,选择 Test Rule init

结果如图,表示解析正确:
截屏2021-11-28 上午10.49.22.png

生成解析文件

这里有两个步骤:配置和生成。

配置

打开 g4 文件,右键,选择 Configure ANTLR
如图,一般只要配置生成的语言,这里是Go,默认是Java
截屏2021-11-29 下午9.02.22.png
其他的依次是输出目录、导入的词法文件、语法文件的编码输出文件的包名

生成

打开 g4 文件,右键,选择 Generate ANTLR Recognize
就会在 gen 目录生成解析文件,如图
截屏2021-11-29 下午9.11.07.png

语法分析

新建 main.go ,输入以下内容:

  1. package main
  2. import (
  3. parser "antlrDemo/gen"
  4. "fmt"
  5. "github.com/antlr/antlr4/runtime/Go/antlr"
  6. )
  7. func main() {
  8. // 获取文件输入流
  9. input, err := antlr.NewFileStream("demo.txt")
  10. if err != nil {
  11. panic(err)
  12. }
  13. // 新建一个词法分析器
  14. lexer := parser.NewArrayInitLexer(input)
  15. // 新建词法符号缓冲区
  16. tokens := antlr.NewCommonTokenStream(lexer, 0)
  17. // 新建语法分析器
  18. p := parser.NewArrayInitParser(tokens)
  19. // 针对 init 规则,开始语法分析
  20. tree := p.Init()
  21. // 打印结果
  22. result := make([]string, 10)
  23. fmt.Println(tree.ToStringTree(result, p))
  24. //fmt.Println(tree.GetText())
  25. }

运行结果:

  1. (init { (value 1) , (value 2) , (value 3) })

进阶

光打印结果没什么用,现在我们的需求是将识别的数字求和。
前文提到语法解析后变成了一棵语法树,那么我们遍历语法树其实就能获取识别的数字。
官方提供了两种方式获取识别的内容:监听器访问器

监听器

顾名思义,就是在遍历树的时候收到回调。

  • 创建 my_listener.go 文件,继承 BaseArrayInitListener
  • 实现 EnterValue 方法,将计算结果保存到 sum ```go package main

import ( parser “antlrDemo/gen” “fmt” “strconv” )

type MyListener struct { parser.BaseArrayInitListener sum int }

func NewMyListener() *MyListener { return &MyListener{} }

// EnterValue 被回调,当遍历进入 value 时 func (s MyListener) EnterValue(ctx parser.ValueContext) { value := ctx.INT().GetText() fmt.Println(“EnterValue”, value) number, err := strconv.Atoi(value) if err != nil { panic(err) } s.sum += number }

  1. - `main.go` 中添加代码
  2. ```go
  3. func main() {
  4. ...
  5. // 新建遍历器
  6. walker := antlr.NewParseTreeWalker()
  7. // 新建监听器
  8. myListener := NewMyListener()
  9. // 开始遍历
  10. walker.Walk(myListener, tree)
  11. // 获取结果
  12. fmt.Println("sum:", myListener.sum)
  13. }

执行结果:

  1. (init { (value 1) , (value 2) , (value 3) })
  2. EnterValue 1
  3. EnterValue 2
  4. EnterValue 3
  5. sum 6

访问器

使用访问者模式进行遍历。

  • 新建 my_visitor.go , 继承 parser.BaseArrayInitVisitor
  • 实现好几个方法(与Java最大区别就是这里) ```go package main

import ( parser “antlrDemo/gen” “fmt” “github.com/antlr/antlr4/runtime/Go/antlr” “strconv” )

type MyVisitor struct { parser.BaseArrayInitVisitor sum int }

func NewMyVisitor() *MyVisitor { return &MyVisitor{} }

// Visit 接受 func (s *MyVisitor) Visit(tree antlr.ParseTree) interface{} { return tree.Accept(s) }

// VisitInit 重写并调用 VisitChildren func (s MyVisitor) VisitInit(ctx parser.InitContext) interface{} { return s.VisitChildren(ctx) }

// VisitChildren 一定要重写 func (s *MyVisitor) VisitChildren(node antlr.RuleNode) interface{} { var result interface{} n := node.GetChildCount() for i := 0; i < n; i++ { c := node.GetChild(i).(antlr.ParseTree) result = c.Accept(s) } return result }

// 当访问到 value 时调用 func (s MyVisitor) VisitValue(ctx parser.ValueContext) interface{} { // 获取数字类型的字符串 value := ctx.INT().GetText() fmt.Println(“VisitValue”, value) number, err := strconv.Atoi(value) if err != nil { panic(err) } s.sum += number return nil }

  1. - 修改 `main.go`
  2. ```go
  3. package main
  4. import (
  5. parser "antlrDemo/gen"
  6. "fmt"
  7. "github.com/antlr/antlr4/runtime/Go/antlr"
  8. )
  9. func main() {
  10. // 获取文件输入流
  11. input, err := antlr.NewFileStream("demo.txt")
  12. if err != nil {
  13. panic(err)
  14. }
  15. // 新建一个词法分析器
  16. lexer := parser.NewArrayInitLexer(input)
  17. // 新建词法符号缓冲区
  18. tokens := antlr.NewCommonTokenStream(lexer, 0)
  19. // 新建语法分析器
  20. p := parser.NewArrayInitParser(tokens)
  21. // 针对 init 规则,开始语法分析
  22. tree := p.Init()
  23. // 打印结果
  24. fmt.Println(tree.GetText())
  25. //listener(input)
  26. visitor(tree)
  27. }
  28. func visitor(tree parser.IInitContext) {
  29. // 新建访问者
  30. myVisitor := NewMyVisitor()
  31. // 访问语法树
  32. myVisitor.Visit(tree)
  33. // 获取结果
  34. fmt.Println("sum:", myVisitor.sum)
  35. }
  36. func listener(tree parser.IInitContext) {
  37. // 新建遍历器
  38. walker := antlr.NewParseTreeWalker()
  39. // 新建监听器
  40. myListener := NewMyListener()
  41. // 开始遍历
  42. walker.Walk(myListener, tree)
  43. // 获取结果
  44. fmt.Println("sum:", myListener.sum)
  45. }

运行结果:

  1. {1,2,3}
  2. VisitValue 1
  3. VisitValue 2
  4. VisitValue 3
  5. sum: 6

区别

访问器机制和监听器机制的最大的区别在于,监听器的方法会被ANTLR提供的遍历器对象自动调用,而在访问器的方法中,必须显式调用visit方法来访问子节点。
访问器的函数有返回值,有时候我们非常需要,比如计算表达式。

未完待续

后续将继续介绍Antlr的更多用法和原理。
本文示例代码