antlr4
安裝
- 安裝Java 1.7及以上
- 下載
$ cd /usr/local/lib $ curl -O https://www.antlr.org/download/antlr-4.9-complete.jar
- 添加 antlr-4.9-complete.jar 到CLASSPATH:
- 創建ANTLR Tool, 和 TestRig的別名
$ alias antlr4='java -Xmx500M -cp "/usr/local/lib/antlr-4.9-complete.jar:$CLASSPATH" org.antlr.v4.Tool' $ alias grun='java -Xmx500M -cp "/usr/local/lib/antlr-4.9-complete.jar:$CLASSPATH" org.antlr.v4.gui.TestRig'
使用
運行一下
$ grun Expr prog -tree -gui (Now enter something like the string below) a = 1; b = a + 1; b; (now,do:) ^D
- 使用 ANTLR 4 生成目標編程語言代碼的詞法分析器(Lexer)和語法分析器(Parser),支持的編程語言有:Java、JavaScript、Python、C 和 C++ 等;
- 遍歷 AST(Abstract Syntax Tree 抽象語法樹),ANTLR 4 支持兩種模式:訪問者模式(Visitor)和監聽器模式(Listener)
遍歷模式
- Listener (觀察者模式,通過結點監聽,觸發處理方法)
1) Listener模式會由ANTLR提供的walker對象自動調用;在遇到不同的節點中,會調用提供的listener的不同方法
2)Listener模式沒有返回值,只能用一些變量來存儲中間值
3)Listener模式是對整棵樹的遍歷
- Visitor (訪問者模式,主動遍歷)
1)visitor需要自己來指定訪問特定類型的節點,在使用過程中,只需要對感興趣的節點實現visit方法即可
2)visitor模式可以自定義返回值
3)visitor模式是對指定節點的訪問
使用antlr4默認生成的是listener模式的解析器,如果要生成visitor類型的,需要加-vistor參數
在js中的使用
import antlr4 from 'antlr4'; import Lexer from './ExprLexer.js'); import Parser from './ExprParser.js';
import Listener from './ExprListener.js';
const input = `
a = 1; b = a + 1; b;
` const chars = new antlr4.InputStream(input); const lexer = new Lexer(chars);
const tokens = new antlr4.CommonTokenStream(lexer); const parser = new Parser(tokens);
使用Visitor來訪問語法樹
為了實現上述的解釋過程,我們需要區遍歷訪問解析器解析出來的語法樹,ANTLR提供了兩種機制來訪問生成的語法樹:Listener和Visitor,使用Listener模式來訪問語法樹時,ANTLR內部的ParserTreeWalker
在遍歷語法樹的節點過程中,在遇到不同的節點中,會調用提供的listener的不同方法;而使用Visitor模式時,visitor需要自己來指定如果訪問特定類型的節點,ANTLR生成的解析器源碼中包含了默認的Visitor基類/接口ExprVisitor.ts
,在使用過程中,只需要對感興趣的節點實現visit方法即可,比如我們需要訪問到exprStat
節點,只需要實現如下接口:
export interface ExprVisitor<Result> extends ParseTreeVisitor<Result> { ... /** * Visit a parse tree produced by `ExprParser.exprStat`. * @param ctx the parse tree * @return the visitor result */ visitExprStat?: (ctx: ExprStatContext) => Result; ... }
介紹完了如果使用Visitor來訪問語法樹中的節點后,我們來實現Expr解釋器需要的Visitor:ExprEvalVisitor
。
上面提到在訪問語法樹過程中,我們需要記錄遇到的變量和其值、和最后的打印結果,我們使用Visitor內部變量來保存這些中間值:
class ExprEvalVisitor extends AbstractParseTreeVisitor<number> implements ExprVisitor<number> { // 保存執行輸出結果 private buffers: string[] = []; // 保存變量 private memory: { [id: string]: number } = {}; }
我們需要訪問語法樹中的哪些節點呢?首先,為了最后的結果,對表達式語句exprState
的訪問是最重要的,我們訪問表達式語句中的表達式得到表達式的值,並將值打印到執行結果中。由於表達式語句是由表達式加分號組成,我們需要繼續訪問表達式得到這條語句的值,而對於分號,則忽略:
class ExprEvalVisitor extends AbstractParseTreeVisitor<number> implements ExprVisitor<number> { // 保存執行輸出結果 private buffers: string[] = []; // 保存變量 private memory: { [id: string]: number } = {}; // 訪問表達式語句 visitExprStat(ctx: ExprStatContext) { const val = this.visit(ctx.expr()); this.buffers.push(`${val}`); return val; } }
上面遞歸的訪問了表達式語句中的表達式節點,那表達式階段的訪問方法是怎樣的?回到我們的語法定義Expr.g4,表達式是由5條分支組成的,對於不同的分支,處理方法不一樣,因此我們對不同的分支使用不同的訪問方法。我們在不同的分支后面添加了不同的注釋,這些注釋生成的解析器中,可以用來區分不同類型的節點,在生成的Visitor中,由可以看到不同的接口:
export interface ExprVisitor<Result> extends ParseTreeVisitor<Result> { ... /** * Visit a parse tree produced by the `MulDivExpr` * labeled alternative in `ExprParser.expr`. * @param ctx the parse tree * @return the visitor result */ visitMulDivExpr?: (ctx: MulDivExprContext) => Result; /** * Visit a parse tree produced by the `IdExpr` * labeled alternative in `ExprParser.expr`. * @param ctx the parse tree * @return the visitor result */ visitIdExpr?: (ctx: IdExprContext) => Result; /** * Visit a parse tree produced by the `IntExpr` * labeled alternative in `ExprParser.expr`. * @param ctx the parse tree * @return the visitor result */ visitIntExpr?: (ctx: IntExprContext) => Result; /** * Visit a parse tree produced by the `ParenExpr` * labeled alternative in `ExprParser.expr`. * @param ctx the parse tree * @return the visitor result */ visitParenExpr?: (ctx: ParenExprContext) => Result; /** * Visit a parse tree produced by the `AddSubExpr` * labeled alternative in `ExprParser.expr`. * @param ctx the parse tree * @return the visitor result */ visitAddSubExpr?: (ctx: AddSubExprContext) => Result; ... }
所以,在我們的ExprEvalVisitor
中,我們通過實現不同的接口來訪問不同的表達式分支,對於AddSubExpr
分支,實現的訪問方法如下:
visitAddSubExpr(ctx: AddSubExprContext) { const left = this.visit(ctx.expr(0)); const right = this.visit(ctx.expr(1)); const op = ctx._op; if (op.type === ExprParser.ADD) { return left + right; } return left - right; }
對於MulDivExpr
,訪問方法相同。對於IntExpr
分支,由於其子節點只有INT
節點,我們只需要解析出其中的整數即可:
visitIntExpr(ctx: IntExprContext) { return parseInt(ctx.INT().text, 10); }
對於IdExpr
分支,其子節點只有變量ID
,這個時候就需要在我們的保存的變量中去查找這個變量,並取出它的值:
visitIdExpr(ctx: IdExprContext) { const id = ctx.ID().text; if (this.memory[id] !== undefined) { return this.memory[id]; } return 0; }
對於最后一個分支ParenExpr
,它的訪問方法很簡單,只需要訪問到括號內的表達式即可:
visitParenExpr(ctx: ParenExprContext) { return this.visit(ctx.expr()); }
到這里,你可以發現了,我們上述的訪問方法加起來,我們只有從memory讀取變量的過程,沒有想memory寫入變量的過程,這就需要我們訪問賦值表達式assignExpr
節點了:對於賦值表達式,需要識別出等號左邊的變量名,和等號右邊的表達式,最后將變量名和右邊表達式的值保存到memory中:
visitAssignStat(ctx: AssignStatContext) { const id = ctx.ID().text; const val = this.visit(ctx.expr()); this.memory[id] = val; return val; }
解釋執行Expr語言
至此,我們的VisitorExprEvalVisitor
已經准備好了,我們只需要在對指定的輸入代碼,使用visitor來訪問解析出來的語法樹,就可以實現Expr代碼的解釋執行了:
// Expr代碼解釋執行函數 // 輸入code // 返回執行結果 function execute(code: string): string { const input = new ANTLRInputStream(code); const lexer = new ExprLexer(input); const tokens = new CommonTokenStream(lexer); const parser = new ExprParser(tokens); const visitor = new ExprEvalVisitor(); const prog = parser.prog(); visitor.visit(prog); return visitor.print(); }
六、Expr代碼前綴表達式翻譯器
通過前面的介紹,我們已經通過通過ANTLR來解釋執行Expr代碼了。結合ANTLR的介紹:ANTLR是用來讀取、處理、執行和翻譯結構化的文本。那我們能不能用ANTLR來翻譯輸入的Expr代碼呢?在Expr語言中,表達式是我們常見的中綴表達式,我們能將它們翻譯成前綴表達式嗎?還記得數據結構課程中如果利用出棧、入棧將中綴表達式轉換成前綴表達式的嗎?不記得么關系,利用ANTLR生成的解析器,我們也可以簡單的換成轉換。
舉例,對如下Expr代碼:
a = 2; b = 3; c = a * (b + 2); c;
我們轉換之后的結果如下,我們支隊表達式做轉換,而對賦值表達式則不做抓換,即代碼中出現的表達式都會轉換成:
a = 2; b = 3; c = * a + b 2; c;
前綴翻譯Visitor
同樣,這里我們使用Visitor模式來訪問語法樹,這次,我們直接visit根節點prog
,並返回翻譯后的代碼:
class ExprTranVisitor extends AbstractParseTreeVisitor<string> implements ExprVisitor<string> { defaultResult() { return ''; } visitProg(ctx: ProgContext) { let val = ''; for (let i = 0; i < ctx.childCount; i++) { val += this.visit(ctx.stat(i)); } return val; } ... }
這里假設我們的visitor在visitor語句stat
的時候,已經返回了翻譯的代碼,所以visitProg
只用簡單的拼接每條語句翻譯后的代碼即可。對於語句,前面提到了,語句我們不做翻譯,所以它們的visit訪問也很簡單:對於表達式語句,直接打印翻譯后的表達式,並加上分號;對於賦值語句,則只需將等號右邊的表達式翻譯即可:
visitExprStat(ctx: ExprStatContext) { const val = this.visit(ctx.expr()); return `${val};\n`; } visitAssignStat(ctx: AssignStatContext) { const id = ctx.ID().text; const val = this.visit(ctx.expr()); return `${id} = ${val};\n`; }
下面看具體如何翻譯各種表達式。對於AddSubExpr
和MulDivExpr
的翻譯,是整個翻譯器的邏輯,即將操作符前置:
visitAddSubExpr(ctx: AddSubExprContext) { const left = this.visit(ctx.expr(0)); const right = this.visit(ctx.expr(1)); const op = ctx._op; if (op.type === ExprParser.ADD) { return `+ ${left} ${right}`; } return `- ${left} ${right}`; } visitMulDivExpr(ctx: MulDivExprContext) { const left = this.visit(ctx.expr(0)); const right = this.visit(ctx.expr(1)); const op = ctx._op; if (op.type === ExprParser.MUL) { return `* ${left} ${right}`; } return `/ ${left} ${right}`; }
由於括號在前綴表達式中是不必須的,所以的ParenExpr
的訪問,只需要去處括號即可:
visitParenExpr(ctx: ParenExprContext) { const val = this.visit(ctx.expr()); return val; }
對於其他的節點,不需要更多的處理,只需要返回節點對應的標記的文本即可:
visitIdExpr(ctx: IdExprContext) { const parent = ctx.parent; const id = ctx.ID().text; return id; } visitIntExpr(ctx: IntExprContext) { const parent = ctx.parent; const val = ctx.INT().text; return val; }
執行代碼的前綴翻譯
至此,我們代碼前綴翻譯的Visitor就准備好了,同樣,執行過程也很簡單,對輸入的代碼,解析生成得到語法樹,使用ExprTranVisitor
反問prog
根節點,即可返回翻譯后的代碼:
function execute(code: string): string { const input = new ANTLRInputStream(code); const lexer = new ExprLexer(input); const tokens = new CommonTokenStream(lexer); const parser = new ExprParser(tokens); const visitor = new ExprTranVisitor(); const prog = parser.prog(); const result = visitor.visit(prog); return result; }
對輸入代碼:
A * B + C / D ; A * (B + C) / D ; A * (B + C / D) ; (5 - 6) * 7 ;
執行輸出為:
+ * A B / C D; / * A + B C D; * A + B / C D; * - 5 6 7;
tree-sitter
- 足以解析任何編程語言
- 速度足以解析文本編輯器中的每一次擊鍵
- 足夠健壯,即使出現語法錯誤也能提供有用的結果
- 無依賴性,這樣運行時庫(用純C編寫)就可以嵌入到任何應用程序中
使用
npm install tree-sitter npm install tree-sitter-javascript const Parser = require('tree-sitter');const JavaScript = require('tree-sitter-javascript');const parser = new Parser();parser.setLanguage(JavaScript); const sourceCode = 'let x = 1; console.log(x);';const tree = parser.parse(sourceCode); console.log(tree.rootNode.toString()); // (program // (lexical_declaration // (variable_declarator (identifier) (number))) // (expression_statement // (call_expression // (member_expression (identifier) (property_identifier)) // (arguments (identifier))))) const callExpression = tree.rootNode.child(1).firstChild; console.log(callExpression); // { type: 'call_expression', // startPosition: {row: 0, column: 16}, // endPosition: {row: 0, column: 30}, // startIndex: 0, // endIndex: 30 }
參考
喜歡這篇文章?歡迎打賞~~