通用的ast解析工具


語法解析器 (Parser) 語法解析器通常作為編譯器或解釋器出現。它的作用是進行語法檢查,並構建由輸入單詞(Token)組成的數據結構(即抽象語法樹)。語法解析器通常使用詞法分析器(Lexer)從輸入字符流中分離出一個個的單詞(Token),並將單詞(Token)流作為其輸入。實際開發中,語法解析器可以手工編寫,也可以使用工具自動生成。
詞法分析器 (Lexer) 詞法分析是指在計算機科學中,將字符序列轉換為單詞(Token)的過程。執行詞法分析的程序便稱為詞法分析

antlr4

ANTLR(另一種語言識別工具)是一種強大的解析器生成器,用於讀取,處理,執行或翻譯結構化文本或二進制文件。它被廣泛用於構建語言,工具和框架。ANTLR 從語法上生成了一個解析器,可以構建和遍歷解析樹。” ANTLR 支持許多語言作為目標,這意味着它可以生成 Java,C#和其他語言的解析器。對於這個項目,可以使用 ANTLR4TS,它是 ANTLR 的 Node.js 版本,可以在 TypeScript 中生成一個詞法分析器和解析器。

安裝

  1. 安裝Java 1.7及以上
  1. 下載
$ cd /usr/local/lib
$ curl -O https://www.antlr.org/download/antlr-4.9-complete.jar

 

或者用鏈接 https://www.antlr.org/download.html 下載到 /usr/local/lib.
  1. 添加 antlr-4.9-complete.jar 到CLASSPATH:
$ export CLASSPATH=".:/usr/local/lib/antlr-4.9-complete.jar:$CLASSPATH"
也可以加到 .bash_profile 或者啟動腳本里。
  1. 創建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'

 

使用

編寫語法規則
Expr.g4
 
           
grammar Expr;
 
           
 
 
           
prog: stat+;
 
           
 
 
           
stat: exprStat | assignStat;
 
           
 
 
           
exprStat: expr SEMI;
 
           
 
 
           
assignStat: ID EQ expr SEMI;
 
           
 
 
           
expr:
 
           
expr op = (MUL | DIV) expr # MulDivExpr
 
           
| expr op = ( ADD | SUB) expr # AddSubExpr
 
           
| INT # IntExpr
 
           
| ID # IdExpr
 
           
| LPAREN expr RPAREN # ParenExpr;
 
           
 
 
           
MUL: '*';
 
           
DIV: '/';
 
           
ADD: '+';
 
           
SUB: '-';
 
           
LPAREN: '(';
 
           
RPAREN: ')';
 
           
 
 
           
ID: LETTER (LETTER | DIGIT)*;
 
           
INT: [0-9]+;
 
           
EQ: '=';
 
           
SEMI: ';';
 
           
COMMENT: '//' ~[\r\n]* '\r'? '\n'? -> channel(HIDDEN);
 
           
WS: [ \r\n\t]+ -> channel(HIDDEN);
 
           
 
 
           
fragment LETTER: [a-zA-Z];
 
           
fragment DIGIT: [0-9];
 

 

ANTLR4 的語法規則分為詞法(Lexer)規則和語法(Parser)規則,詞法規則定義了怎么將代碼字符串序列轉換成標記序列;語法規則定義怎么將標記序列轉換成語法樹。通常,詞法規則的規則名以大寫字母命名,而語法規則的規則名以小寫字母開始。主流語言的 ANTLR4 語法定義可以到 語法倉庫中找到。
生成相關文件
 
           
// Java中使用
 
           
$ antlr4 Expr.g4
 
           
$ javac Expr*.java
 
           
 
 
           
// javascript
 
           
antlr4 -Dlanguage=JavaScript Expr.g4
 

 

運行一下

$ 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)

遍歷模式

  1. Listener (觀察者模式,通過結點監聽,觸發處理方法)

    1) Listener模式會由ANTLR提供的walker對象自動調用;在遇到不同的節點中,會調用提供的listener的不同方法

    2)Listener模式沒有返回值,只能用一些變量來存儲中間值

    3)Listener模式是對整棵樹的遍歷

  1. 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`;
}

 

下面看具體如何翻譯各種表達式。對於AddSubExprMulDivExpr的翻譯,是整個翻譯器的邏輯,即將操作符前置:

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

Tree-sitter是一個解析器生成器工具,也是一個增量解析庫。它可以為源文件構建一個具體的語法樹,並在編輯源文件時有效地更新語法樹。
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 }

 

參考

https://zhuanlan.zhihu.com/p/31748014
http://codeinchinese.com/%E5%9C%883/%E5%9C%883.html
https://tree-sitter.github.io/tree-sitter/
https://github.com/tree-sitter/node-tree-sitter

 

喜歡這篇文章?歡迎打賞~~

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM