antlr4
Antlr是指可以根據(jù)輸入自動(dòng)生成語法樹并可視化的顯示出來的開源語法分析器。ANTLR—Another Tool for Language Recognition,其前身是PCCTS,它為包括Java,C++,C#在內(nèi)的語言提供了一個(gè)通過語法描述來自動(dòng)構(gòu)造自定義語言的識(shí)別器(recognizer),編譯器(parser)和解釋器(translator)的框架。
Antlr 是一個(gè)強(qiáng)大的跨語言語法解析器,可以用來讀取、處理、執(zhí)行或翻譯結(jié)構(gòu)化文本或二進(jìn)制文件
antlr運(yùn)行流程
- 詞法分析(接收文本,源代碼,輸出token流,同時(shí)生成符號(hào)表)
- 語法分析(接收token stream并且生成語法樹)
- 語義分析(講語法樹轉(zhuǎn)換成能被cpu執(zhí)行的中間代碼)
- 解釋器解釋(調(diào)?宿主語?,或虛擬機(jī)直接執(zhí)?代碼)
安裝antlr
打開官網(wǎng)anttr4
如果是macOS的話,執(zhí)行以下代碼
$ cd /usr/local/lib
$ sudo curl -O https://www.antlr.org/download/antlr-4.9.2-complete.jar
$ export CLASSPATH=".:/usr/local/lib/antlr-4.9.2-complete.jar:$CLASSPATH"
$ alias antlr4='java -jar /usr/local/lib/antlr-4.9.2-complete.jar'
$ alias grun='java org.antlr.v4.gui.TestRig'
如果是windows用戶的話
- 下載antlr4 這里是下載地址
- 將 antlr-4.9.2-complete.jar 加入環(huán)境變量中
安裝好之后可以通過antlr4 的命令來驗(yàn)證是否安裝完畢
初始化TS項(xiàng)目
這里我們用typescript來編寫antlr4的項(xiàng)目
- 創(chuàng)建一個(gè)新目錄,我們暫且稱呼該語言為A語言,并且初始化npm
mkdir ALang
cd ALang
npm init -y
- 安裝antlr4ts,用于解析g4語法文件,antlr4ts-cli作為包管理器
yarn add antlr4ts
yarn add -D antlr4ts-cli
- 新建語法文件目錄,這里我講語法文件放入一個(gè)新目錄
ALang>src>antlr>ALang.g4
- 設(shè)置package.json啟動(dòng)腳本,腳本的含義是用antlr4ts的訪問者模式來解析ALang.g4文件
"scripts": {
"antlr4ts": "antlr4ts -visitor src/antlr/ALang.g4"
}
- 新建入口文件app.ts
touch app.ts
- 解析g4文件,會(huì)在src/antlr目錄下生成相關(guān)ts文件
npm run antlr4ts

初始化的任務(wù)就結(jié)束了,接下來開始寫相關(guān)代碼了
語法文件
grammar Alang;
prog: stat+ ;
// -------------給每個(gè)備選分支打標(biāo)簽
stat: expr NEWLINE # printExpr
| ID '=' expr NEWLINE # assign
| NEWLINE # blank
;
expr: expr MUL expr # Multiplication
| expr ADD expr # Addition
| expr DIV expr # Division
| expr SUB expr # Subtraction
| INT # int
| ID # id
| BooleanLiteral # BooleanExpr
| '(' expr ')' # parens
;
// -------------給運(yùn)算符號(hào)設(shè)置名字,也形成詞法符號(hào)
MUL : '*' ;
DIV : '/' ;
ADD : '+' ;
SUB : '-' ;
BooleanLiteral: 'true'
| 'false';
// -------------剩下的是和之前一樣的詞法符號(hào)
ID : [\u4e00-\u9fa5_a-zA-Z]+ ; // 標(biāo)識(shí)符:一個(gè)到多個(gè)英文字母
INT : [0-9]+ ; // 整形值:一個(gè)到多個(gè)數(shù)字
NEWLINE:'\r'? '\n' ; // 換行符
WS : [ \t]+ -> skip ; // 跳過空格和tab
- grammar Alang(是用來聲明當(dāng)前的語言名叫Alang)
- prog: stat+ (程序入口是prog,所以app.ts中會(huì)執(zhí)行prog方法)
- stat和expr都是用來聲明語法,規(guī)定了該語言是如何編寫的
- 文件底部的是詞法文件,是對(duì)詞匯的描述,比如MUL代表的是“*”乘號(hào),在anglr解析之后會(huì)生成對(duì)應(yīng)的符號(hào)表
入口文件
這里需要按照antlr的執(zhí)行順序,依次對(duì)代碼進(jìn)行操作:
分別是:
- 詞法分析(接收文本,源代碼,輸出token流,同時(shí)生成符號(hào)表)
- 語法分析(接收token stream并且生成語法樹)
- 語義分析(講語法樹轉(zhuǎn)換成能被cpu執(zhí)行的中間代碼)
- 解釋器解釋(調(diào)?宿主語?,或虛擬機(jī)直接執(zhí)?代碼)
完整代碼如下
import {
ANTLRInputStream, BufferedTokenStream, CharStream, CommonTokenStream
} from "antlr4ts";
import { ALangLexer } from "./antlr/ALangLexer";
import { ALangParser } from "./antlr/ALangParser";
//將文本轉(zhuǎn)換為token,并生成符號(hào)表
let inputStream: CharStream = new ANTLRInputStream("a=1+2\nb=a*2+1\nc=a*3+2*b\n");
// 詞法分析
let lexer: ALangLexer = new ALangLexer(inputStream);
// 生成token流
let tokenStream: BufferedTokenStream = new CommonTokenStream(lexer);
//接收token并且生成語法樹
let parser = new ALangParser(tokenStream);
//執(zhí)行解析器
let tree = parser.prog();
實(shí)現(xiàn)訪問者具體方法
antlr工具生成的可以使用的代碼中,我們已經(jīng)使用了兩個(gè)文件,第一個(gè)是ALangLexer,第二個(gè)是ALangParser,還有兩個(gè)我們沒有使用到,分別是ALangListener和ALangVisitor
這里我們使用ALangVisitor,采用的訪問者模式,更適合當(dāng)前對(duì)樹形結(jié)構(gòu)的遍歷
打開ALangVisitor文件我們可以看到它的源代碼
// Generated from src/antlr/ALang.g4 by ANTLR 4.9.0-SNAPSHOT
import { ParseTreeVisitor } from "antlr4ts/tree/ParseTreeVisitor";
import { PrintExprContext } from "./ALangParser";
import { AssignContext } from "./ALangParser";
import { BlankContext } from "./ALangParser";
import { MultiplicationContext } from "./ALangParser";
import { AdditionContext } from "./ALangParser";
import { DivisionContext } from "./ALangParser";
import { SubtractionContext } from "./ALangParser";
import { IntContext } from "./ALangParser";
import { IdContext } from "./ALangParser";
import { BooleanExprContext } from "./ALangParser";
import { ParensContext } from "./ALangParser";
import { ProgContext } from "./ALangParser";
import { StatContext } from "./ALangParser";
import { ExprContext } from "./ALangParser";
export interface ALangVisitor<Result> extends ParseTreeVisitor<Result> {
visitPrintExpr?: (ctx: PrintExprContext) => Result;
visitAssign?: (ctx: AssignContext) => Result;
visitBlank?: (ctx: BlankContext) => Result;
visitMultiplication?: (ctx: MultiplicationContext) => Result;
visitAddition?: (ctx: AdditionContext) => Result;
visitDivision?: (ctx: DivisionContext) => Result;
visitSubtraction?: (ctx: SubtractionContext) => Result;
visitInt?: (ctx: IntContext) => Result;
visitId?: (ctx: IdContext) => Result;
visitBooleanExpr?: (ctx: BooleanExprContext) => Result;
visitParens?: (ctx: ParensContext) => Result;
visitProg?: (ctx: ProgContext) => Result;
visitStat?: (ctx: StatContext) => Result;
visitExpr?: (ctx: ExprContext) => Result;
}
源代碼是一堆需要實(shí)現(xiàn)的接口類,所以我們需要對(duì)這些方法進(jìn)行實(shí)現(xiàn)。新建一個(gè)ALangBaseVisitor.ts文件,實(shí)現(xiàn)上述接口類
import { AbstractParseTreeVisitor } from "antlr4ts/tree";
import { ALangVisitor } from "./antlr/ALangVisitor";
export default class ALangBaseVisitor
extends AbstractParseTreeVisitor<number>
implements ALangVisitor<number>{
protected defaultResult(): number {
throw new Error("Method not implemented.");
}
}
- visitPrintExpr方法,這里需要講表達(dá)式進(jìn)行遞歸調(diào)用,拿到最終的值,并且打印出文本
visitPrintExpr(ctx: PrintExprContext) {
const value: number = this.visit(ctx.expr());
const exprString: string = ctx.expr().text;
console.log(exprString+":"+value.toString());
return value;
}
2.visitAssign 賦值語句
- 需要拿到待賦值的變量名出
- 拿到藥賦值的值
- 將計(jì)算之后的值存儲(chǔ)在內(nèi)存中,以便后續(xù)計(jì)算使用
- 計(jì)算結(jié)束之后需要清空內(nèi)存
visitAssign(ctx: AssignContext) {
const id: string = ctx.ID().text;
const value: number = this.visit(ctx.expr());
this.memory[id]=value;
return value;
}
- visitMultiplication 乘法表達(dá)式,需要注意的就是乘法表達(dá)式兩側(cè)都是表達(dá)式,需要對(duì)兩側(cè)的表達(dá)式進(jìn)行遞歸執(zhí)行visitAddition,visitDivision,visitSubtraction都是同理
visitMultiplication(ctx: MultiplicationContext){
const left: number = this.visit(ctx.expr(0));
const right: number = this.visit(ctx.expr(1));
return left*right;
};
- visitInt 在語法聲明文件中,單獨(dú)寫一個(gè)數(shù)字也會(huì)默認(rèn)為表達(dá)式語句,我將它原封不動(dòng)返回
visitInt (ctx: IntContext){
return parseInt(ctx.INT().text);
};
- visitId 訪問到Id的時(shí)候,實(shí)際上是對(duì)Id的取值,這里就需要從緩存中讀取Id的值,并且返回,如果沒有讀取到,則返回0
visitId (ctx: IdContext){
const id: string = ctx.ID().text;
if(this.memory[id]!=null){
return this.memory[id]
}
return 0;
};
- visitParens 括號(hào)表達(dá)式,只需要對(duì)括號(hào)內(nèi)對(duì)語句進(jìn)行遞歸之行即可
visitParens(ctx: ParensContext){
return this.visit(ctx.expr());
};
到此為止,訪問者已經(jīng)創(chuàng)建完畢,接下來我們?cè)谌肟谖募惺褂迷撛L問者,即可訪問到之前生成的語法樹的內(nèi)容,并且執(zhí)行訪問者中的代碼,就可以得到結(jié)果了
app.ts
import {
ANTLRInputStream, BufferedTokenStream, CharStream, CommonTokenStream
} from "antlr4ts";
import ALangBaseVisitor from "./ALangBaseVisitor";
import { ALangLexer } from "./antlr/ALangLexer";
import { ALangParser } from "./antlr/ALangParser";
let inputStream: CharStream = new ANTLRInputStream("a=1+2\nb=a*2+1\nc=a*3+2*b\n");
let lexer: ALangLexer = new ALangLexer(inputStream);
let tokenStream: BufferedTokenStream = new CommonTokenStream(lexer);
let parser = new ALangParser(tokenStream);
let tree = parser.prog();
const exprBaseVisitor: ALangBaseVisitor = new ALangBaseVisitor();
const result: number = exprBaseVisitor.visit(tree);
console.log("計(jì)算結(jié)果是:",result);

和預(yù)期結(jié)果一致!
該demo是一個(gè)很小的antlr語言解析例子,采用了訪問者模式對(duì)生成的語法樹進(jìn)行遞歸訪問,做到了對(duì)語法樹很小的入侵性。
源代碼:
java版本: https://github.com/chesongsong/h-lang
typescript版本: https://github.com/chesongsong/h-lang-ts
關(guān)于我
微信:cjs764901388
公眾號(hào):xstxoo
我的公眾號(hào):小松同學(xué)哦
可以關(guān)注我,一起學(xué)習(xí)前端知識(shí),喜歡把生活中用到技術(shù)的地方記錄下來