在上一個版本實現的指令碼直譯器 GScript 中實現了基本的四則運算以及 AST
的生成。
當我準備再新增一個 %
取模的運運算元時,會發現工作很繁瑣而且幾乎都是重複的;主要是兩步:
%
符號的支援。%
token 實現具體邏輯。其中的詞法解析和遍歷 AST 完全是重複工作,所以我們可否能夠簡化這兩步呢?
Antlr
就是做幫我們解決這些問題的常用工具,利用它我們只需要編寫詞法檔案,然後就可以自動生成詞法、語法解析器,並且可以生成不同語言的程式碼。
下面以 GScript
的範例來看看 antlr 是如何幫我們生成詞法分析器的。
func TestGScriptVisitor_Visit_Lexer(t *testing.T) {
expression := "(2+3) * 2"
input := antlr.NewInputStream(expression)
lexer := parser.NewGScriptLexer(input)
for {
t := lexer.NextToken()
if t.GetTokenType() == antlr.TokenEOF {
break
}
fmt.Printf("%s (%q) %d\n",
lexer.SymbolicNames[t.GetTokenType()], t.GetText(),t.GetColumn())
}
}
//output:
("(") 0
DECIMAL_LITERAL ("2") 1
PLUS ("+") 2
DECIMAL_LITERAL ("3") 3
(")") 4
MULT ("*") 6
DECIMAL_LITERAL ("2") 8
Antlr
會自動將我們的表示式解析為 token
,遍歷 token
時還能拿到該 token
所在的程式碼行數、位置等資訊,在編譯期間做語法檢查非常有用。
要實現這些我們只需要編寫詞法、語法規則檔案即可。
剛才的範例所對應的詞法、語法規則如下:
expr
: '(' expr ')' #NestedExpr
| liter=literal #Liter
| lhs=expr bop=( MULT | DIV ) rhs=expr #MultDivExpr
| lhs=expr bop=MOD rhs=expr #ModExpr
| lhs=expr bop=( PLUS | SUB ) rhs=expr #PlusSubExpr
| expr bop=(LE | GE | GT | LT ) expr # GLe
| expr bop=(EQUAL | NOTEQUAL) expr # EqualOrNot
;
DECIMAL_LITERAL: ('0' | [1-9] (Digits? | '_'+ Digits)) [lL]?;
完整規則:https://github.com/crossoverJie/gscript/blob/main/GScript.g4
執行:
antlr -Dlanguage=Go -o parser -visitor -no-listener GScript.g4
就可以幫我們生成 Go
的程式碼(預設是 Java
),關於 Antlr
的詞法、文法規則以及安裝步驟請參考官網。
而我們要實現具體的語法邏輯時只需要實現相關的介面,Antlr
會自動遍歷 AST
(當然也可以手動控制),同時在存取不同的 AST
節點時會回撥我們自己實現的介面,這樣我們就能編寫自己的語法規則了。
以這裡的新增的取模運算為例:
func (v *GScriptVisitor) VisitModExpr(ctx *parser.ModExprContext) interface{} {
lhs := v.Visit(ctx.GetLhs())
rhs := v.Visit(ctx.GetRhs())
return lhs.(int) % rhs.(int)
}
當 Antlr
回撥 VisitModExpr
方法時,便能獲取到 % 符號左右兩側的資料,這時只需要做相關運算即可。
基於這個模式這次新增了一個 statement
,具體語法如下:
func TestGScriptVisitor_VisitIfElse8(t *testing.T) {
expression := `
if(3!=(1+2)){
return 1+3
} else {
return false
}`
input := antlr.NewInputStream(expression)
lexer := parser.NewGScriptLexer(input)
stream := antlr.NewCommonTokenStream(lexer, 0)
parser := parser.NewGScriptParser(stream)
parser.BuildParseTrees = true
tree := parser.Prog()
visitor := GScriptVisitor{}
var result = visitor.Visit(tree)
fmt.Println(expression, " result:", result)
assert.Equal(t, result, false)
}
Antlr 還有其他各種優勢,比如可以解決:
等問題。
這裡也推薦在 IDE 中安裝 Antlr 的外掛,這樣就可以直觀的檢視 AST 語法樹,可以幫我們更好的偵錯程式碼。
藉助 GScript
提供的 statement
,xjson
也提供了有些有意思的寫法:
因為 xjson
的四則運算語法沒有使用 Antlr
生成,所以為了能支援 GScript
提供的 statement
需要手寫許多詞法程式碼。
這也體現了 Antlr
這類前端工具的重要性,效率提升是非常明顯的。
藉助於 Antlr
後續 GScript
會繼續支援函數呼叫、更完善的型別系統、物件導向等特性;感興趣的朋友請持續關注。
作者: crossoverJie
歡迎關注博主公眾號與我交流。
本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出, 如有問題, 可郵件(crossoverJie#gmail.com)諮詢。