本文所用到的工具:Go、Goland、ANTLR4插件和一个包含Go Mod的空工程。
为什么不用VSCode? 因为它的Antlr4插件不够好用。
为什么使用Golang作为示例程序 因为之前没有找到合适的 Golang 示例
简介
ANTLR4 是一款强大的语法分析器生成工具,可用于读取、处理、执行和翻译结构化的文本或二进制文件。
简单的说,它可以识别结构化语言,比如SQL、JSON、Python或者你自己定义的编程语言。
它可以作为数据读取器、语言解释器和翻译器。
它本身使用Java写的,但是可以生成各种语言的接口。
学习途径
- 官网
- 《ANTLR4权威指南》
- 官方文档 (英文不好的推荐看书,即上一条)
- 官方示例:grammars-v4
安装
官网上有安装方法,不过使用Goland的插件更方便。
在GolandPreference
->Plugins
中搜索 antlr 然后安装工作流程
- 编写
.g4
文件描述要解析的语言规则 - 根据
.g4
生成特定语言的解析文件(可以是Java、C++、Golang等等) - 创建入口程序,依赖生成的解析文件,进行语法分析
开始
ANTLR元语言
为了实现一门编程语言,我们需要识别出一门特定语言的所有的有意义的语句、词组和子词组。
例如,我们能够将输入的 sp=100;
识别为一个赋值语句,这意味着我们需要知道sp是被赋值的目标,100是要被赋予的值。
我们使用ANTLR元语言描述其他语言的语法,保存在 .g4
文件中。
分析语言的过程分为:
- 词法分析
-
词法分析
将相关的词法符号归类,例如INT(整数)、ID(标识符)、FLOAT(浮点数)等。
语法分析
建造一种名为语法分析树(parse tree)或者句法树(syntax tree)的数据结构,该数据结构记录了语法分析器识别出输入语句结构的过程,以及该结构的各组成部分。
编写g4文件
我们编写一个
g4
文件来描述一些规则。
比如我想识别下面格式的文本:{1, 2, 3}
需要的规则:
一对花括号
- 花括号中有数字
- 每个数字以逗号分割
- 数字与逗号之间可以有任意个空格
创建一个名为 ArrayInit.g4
的文件
// 以 grammar 开头,后接文件名
// 表示这是语法文件
grammar ArrayInit;
// 一条名为init的语法规则,匹配花括号,逗号分开的value
init : '{' value (',' value)* '}' ;
// value也是一条语法规则,它是数字
value : INT ;
// INT 词法规则,数字是0到9之间,由一个以上组成
INT : [0-9]+ ;
// 忽略空格、制表符和换行
WS : [ \t\r\n]+ -> skip ;
这里的规则匹配和正则表达式很类似,最好先去了解正则表达式。
测试语法
创建文件 demo.txt
,内容如下:
{1, 2, 3}
- �在 Goland 中打开
ArrayInit.g4
文件 - 打开左下角的
Antlr preview
视图,选择刚创建的demo.txt
文件 - 在
init
规则那里右键,选择Test Rule init
生成解析文件
配置
打开 g4
文件,右键,选择 Configure ANTLR
如图,一般只要配置生成的语言,这里是Go,默认是Java
其他的依次是输出目录、导入的词法文件、语法文件的编码和输出文件的包名
生成
打开 g4
文件,右键,选择 Generate ANTLR Recognize
就会在 gen
目录生成解析文件,如图
语法分析
新建 main.go
,输入以下内容:
package main
import (
parser "antlrDemo/gen"
"fmt"
"github.com/antlr/antlr4/runtime/Go/antlr"
)
func main() {
// 获取文件输入流
input, err := antlr.NewFileStream("demo.txt")
if err != nil {
panic(err)
}
// 新建一个词法分析器
lexer := parser.NewArrayInitLexer(input)
// 新建词法符号缓冲区
tokens := antlr.NewCommonTokenStream(lexer, 0)
// 新建语法分析器
p := parser.NewArrayInitParser(tokens)
// 针对 init 规则,开始语法分析
tree := p.Init()
// 打印结果
result := make([]string, 10)
fmt.Println(tree.ToStringTree(result, p))
//fmt.Println(tree.GetText())
}
运行结果:
(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 }
- 在 `main.go` 中添加代码
```go
func main() {
...
// 新建遍历器
walker := antlr.NewParseTreeWalker()
// 新建监听器
myListener := NewMyListener()
// 开始遍历
walker.Walk(myListener, tree)
// 获取结果
fmt.Println("sum:", myListener.sum)
}
执行结果:
(init { (value 1) , (value 2) , (value 3) })
EnterValue 1
EnterValue 2
EnterValue 3
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 }
- 修改 `main.go`
```go
package main
import (
parser "antlrDemo/gen"
"fmt"
"github.com/antlr/antlr4/runtime/Go/antlr"
)
func main() {
// 获取文件输入流
input, err := antlr.NewFileStream("demo.txt")
if err != nil {
panic(err)
}
// 新建一个词法分析器
lexer := parser.NewArrayInitLexer(input)
// 新建词法符号缓冲区
tokens := antlr.NewCommonTokenStream(lexer, 0)
// 新建语法分析器
p := parser.NewArrayInitParser(tokens)
// 针对 init 规则,开始语法分析
tree := p.Init()
// 打印结果
fmt.Println(tree.GetText())
//listener(input)
visitor(tree)
}
func visitor(tree parser.IInitContext) {
// 新建访问者
myVisitor := NewMyVisitor()
// 访问语法树
myVisitor.Visit(tree)
// 获取结果
fmt.Println("sum:", myVisitor.sum)
}
func listener(tree parser.IInitContext) {
// 新建遍历器
walker := antlr.NewParseTreeWalker()
// 新建监听器
myListener := NewMyListener()
// 开始遍历
walker.Walk(myListener, tree)
// 获取结果
fmt.Println("sum:", myListener.sum)
}
运行结果:
{1,2,3}
VisitValue 1
VisitValue 2
VisitValue 3
sum: 6
区别
访问器机制和监听器机制的最大的区别在于,监听器的方法会被ANTLR提供的遍历器对象自动调用,而在访问器的方法中,必须显式调用visit方法来访问子节点。
访问器的函数有返回值,有时候我们非常需要,比如计算表达式。
未完待续
后续将继续介绍Antlr的更多用法和原理。
本文示例代码