使用golang+antlr4構建一個自己的語言解析器(二)

2023-03-27 18:00:51

Antlr4檔案解析流程


該圖展示了一個語言應用程式中的基本流動過程

  • 輸入一個字元流,首先經過詞法分析,獲取各個Token
  • 然後經過語法分析,組成語法分析樹

Antlr4語法書寫規範

語法關鍵字和使用

符號 作用
表示式可選
* 表示式出現0此或多次
+ 表示式至少一次
EOF 語法結尾
expr expr1 expr2 序列模式,由多個表示式或Token組成一個語法規則
expr|expr1|expr2 選擇模式,指定一個語法規則可以選擇多個匹配模式
expr|expr1|expr* 巢狀模式,自身參照自身

處理優先順序、左遞迴和結合性

Antlr4預設使用自上而下,預設左遞迴的方式識別語法, 使用下面一個例子說明左遞迴的方式
expr:expr '*' expr
|expr '+' expr
|INT
;
輸入1+2*3;識別的樹為

這是因為首先定義了乘法語法規則,然後定義了加法語法規則。

更改左遞迴的方式

expr:<assoc=right> expr '^' expr
|INT
;
指定第一個expr接受expr的結果作為結合一個語法規則,輸入12^3,識別的樹為

Antlr4的基本命令我們就瞭解到這,有興趣研究的小夥伴,可以檢視《Antlr4權威指南》這本書。

第一個指令碼

避坑指南

使用go mode模式參照antlr4預設是1.X版本,這個是低版本antlr,無法識別最新的antlr語法,使用
go get -u github.com/antlr/antlr4/runtime/Go/antlr/v4
獲取antlr4最新版本的golang包

語法檔案

我這裡使用的IDE是goland,這個大家根據自己的愛好自選就行。
新建資料夾:parser
在parser資料夾下新建檔案:Calc.g4
輸入一下內容:

grammar Calc;
//Token
MUL: '*';
DIV: '/';
ADD:+;
SUB-';
NUMBER' | [1-9] ('_'? [0-9])*);
WS_NLSEMI:[ \r\n\t]+ -> skip;
//Rule
expression:
expression op = (MUL | DIV) expression #MulDiv
| expression op =  (ADD | SUB) expression #AddSub
| NUMBER
start:expression EOF
  • g4檔案是Antlr4的檔案型別
  • Token代表客製化的語法中的關鍵字,Token都是全部大寫
  • Rule是語法規則,由駝峰樣式書寫,首字母小寫,後續每個單詞首字母大寫
  • EOF代表結尾

指令碼命令

我們需要使用Antlr4命令生成Go語言的語法樹和Listen方式的go檔案

$ java -jar 'C:\Program Files\Java\antlr\antlr-4.12.0-complete.jar' -Dlanguage=Go -no-visitor -package parser *.g4

上述命令就是指定使用antlr4將檔案目錄下所有的.g4檔案生成目標語言為Go的語言檔案。

執行CMD命令:
cd .\parser\
執行上述命令,我們會在parser資料夾中看到生成了很多檔案:
Calc.interp
Calc.tokens
calc_base_listener.go //監聽模式基礎類別的檔案
calc_lexer.go //文法檔案
calc_listener.go //監聽模式的檔案(定義多少個語法或者自定義型別就會有多少對Enter、Exit方法)
calc_parser.go //識別語法的檔案

驗證語法

func main(){
  is := antlr.NewInputStream("1+1*2")`
  lexer := parser.NewCalcLexer(is)
  // Read all tokens
  for {
	t := lexer.NextToken()
	if t.GetTokenType() == antlr.TokenEOF {
		break
	}
	fmt.Printf("%s (%q)\n",lexer.SymbolicNames[t.GetTokenType()], t.GetText())
      }

}

輸入內容:

NUMBER ("1")
ADD ("+")
NUMBER ("1")
MUL ("*")
NUMBER ("2")

證明我們的每個Token都被識別

建立一個四則運演演算法則

type calcListener struct {
	*parser.BaseCalcListener

	stack []int
}

func (l *calcListener) push(i int) {
	l.stack = append(l.stack, i)
}

func (l *calcListener) pop() int {
	if len(l.stack) < 1 {
		panic("stack is empty unable to pop")
	}

	// Get the last value from the stack.
	result := l.stack[len(l.stack)-1]

	// Remove the last element from the stack.
	l.stack = l.stack[:len(l.stack)-1]

	return result
}

func (l *calcListener) ExitMulDiv(c *parser.MulDivContext) {
	right, left := l.pop(), l.pop()

	switch c.GetOp().GetTokenType() {
	case parser.CalcParserMUL:
		l.push(left * right)
	case parser.CalcParserDIV:
		l.push(left / right)
	default:
		panic(fmt.Sprintf("unexpected op: %s", c.GetOp().GetText()))
	}
}

func (l *calcListener) ExitAddSub(c *parser.AddSubContext) {
	right, left := l.pop(), l.pop()

	switch c.GetOp().GetTokenType() {
	case parser.CalcParserADD:
		l.push(left + right)
	case parser.CalcParserSUB:
		l.push(left - right)
	default:
		panic(fmt.Sprintf("unexpected op: %s", c.GetOp().GetText()))
	}
}

func (l *calcListener) ExitNumber(c *parser.NumberContext) {
	i, err := strconv.Atoi(c.GetText())
	if err != nil {
		panic(err.Error())
	}

	l.push(i)
}

func calc(input string) int {
	// Setup the input
	is := antlr.NewInputStream(input)

	// Create the Lexer
	lexer := parser.NewCalcLexer(is)
	stream := antlr.NewCommonTokenStream(lexer, antlr.TokenDefaultChannel)

	// Create the Parser
	p := parser.NewCalcParser(stream)

	// Finally parse the expression (by walking the tree)
	var listener calcListener
	antlr.ParseTreeWalkerDefault.Walk(&listener, p.Start())

	return listener.pop()
}

func main(){
	fmt.Println(calc(1+1*2))
}

至此,我們已經使用antlr4+golang開始自己第一個語法檔案使用,接下來就是如何實現我們自定的語法了!!!