亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專欄INFORMATION COLUMN

如何用 ANTLR 4 實(shí)現(xiàn)自己的腳本語言?

scwang90 / 1850人閱讀

摘要:是一個(gè)實(shí)現(xiàn)的詞法語法分析生成程序,目前最新版本為,支持,,等語言,這里我們用來實(shí)現(xiàn)一個(gè)自己的腳本語言。在實(shí)現(xiàn)時(shí),只要每個(gè)節(jié)點(diǎn)都做好自己的工作就可以了。不過,它是一個(gè)好的開始,可以讓我們?cè)诖嘶A(chǔ)上,設(shè)計(jì)更完善易用的語言。

ANTLR 是一個(gè) Java 實(shí)現(xiàn)的詞法/語法分析生成程序,目前最新版本為 4.5.2,支持 Java,C#,JavaScript 等語言,這里我們用 ANTLR 4.5.2 來實(shí)現(xiàn)一個(gè)自己的腳本語言。

因?yàn)槟承┪粗?,ANTLR 官方的文檔似乎有些地方和 4.5.2 版的實(shí)際情況不太吻合,所以,有些部分,我們必須多方查找和自己實(shí)踐得到,所幸 ANTLR 的文檔比較豐富,其在 Github 上例子程序也很多,足夠我們探索的了。

如果你沒有編譯原理的基礎(chǔ),只要寫過正則表達(dá)式,應(yīng)該也能很快理解其規(guī)則,進(jìn)而編寫自己的規(guī)則文件,事實(shí)上,因?yàn)榻Y(jié)構(gòu)更清晰, ANTLR 的規(guī)則文件,比正則表達(dá)式要簡(jiǎn)單得多。

我使用 C# 版本,所以下載了 antlr-4.5.2-complete.jar 和 C# 的支持庫(kù) Antlr4.Runtime.dll。

ANTLR 官方網(wǎng)址 http://www.antlr.org/
ANTLR 官方 Github https://github.com/antlr/antlr4
大量語法文件例子 https://github.com/antlr/grammars-v4
因?yàn)槲恼轮胁贿m合貼全部的代碼,建議下載了 TinyScript 的代碼后,和此文章對(duì)照閱讀和實(shí)踐。

本文程序的 Github https://github.com/Lifeng-Liang/TinyScri...
好了,進(jìn)入正題,我們要定義一個(gè)解釋型的腳本語言,就起個(gè)名叫 TinyScript 好了,規(guī)則文件名 TinyScript.g4 ,簡(jiǎn)單起見,暫不實(shí)現(xiàn)函數(shù),具體實(shí)現(xiàn)的功能如下:

變量,支持的數(shù)據(jù)類型為 decimal,bool,string,不支持 null
變量賦值支持自動(dòng)類型推斷,用 var 標(biāo)識(shí)
四則運(yùn)算,支持字符串通過 + 進(jìn)行連接
支持比較運(yùn)算符,支持與或非運(yùn)算符
if 語句,語句塊必須用大括號(hào)包裹
while,do/while,for 循環(huán),同樣語句塊必須用大括號(hào)包裹
一個(gè)內(nèi)置的輸出函數(shù) print,可以輸出表達(dá)式的值到控制臺(tái)
先說四則運(yùn)算。四則運(yùn)算里,除了括號(hào)外,需要先乘除,后加減,這個(gè)規(guī)則在 ANTLR 里怎么實(shí)現(xiàn)呢?

在 ANTLR 里,我們寫的規(guī)則,會(huì)生成解析器的代碼,這個(gè)解析器,會(huì)把目標(biāo)腳本,解析成一個(gè)抽象語法樹。這顆抽象語法樹上,越是靠近葉子節(jié)點(diǎn)的地方,結(jié)合優(yōu)先級(jí)越高,越是靠近根的地方,結(jié)合優(yōu)先級(jí)越低,根據(jù)這個(gè)特點(diǎn),我們就可以讓 ANTLR 幫我們完成以上的規(guī)則:

addExpression
: mulExpression (("+" | "-") mulExpression)*
;
mulExpression
: primaryExpression (("*" | "/") primaryExpression)*
;
primaryExpression
: Decimal
| "(" addExpression ")"
;

上面展示的 ANTLR 規(guī)則,在 primaryExpression 中,包括兩個(gè)可選項(xiàng),要么是數(shù)字,要么是括號(hào)表達(dá)式,是最高優(yōu)先級(jí),然后是 mulExpression,優(yōu)先級(jí)最低的是 addExpression 。括號(hào)表達(dá)式內(nèi),是一個(gè) addExpression ,所以,這是一個(gè)循環(huán)結(jié)構(gòu),可以處理無限長(zhǎng)的四則運(yùn)算式,比如 1+2*3-(4+5)/6+7+8,會(huì)被解析為如下的語法樹:

addExpression : 1 + child1_1 - child1_2 + 7 + 8
child1_1 mulExpression : 2 * 3
child1_2 mulExpression : child1_2_1 / 6
child1_2_1 addExpression : 4 + 5

以上的語法樹,其實(shí)是我簡(jiǎn)化了的,比如,其中的數(shù)字 1 其實(shí)應(yīng)該是 ·mulExpression ,而這個(gè) mulExpression 只有一項(xiàng) primaryExpression,而這個(gè) primaryExpression,是 Decimal,其值為 1 。

PS: 在 ANTLR 中,大寫字母開頭的標(biāo)識(shí)符,如上面的 Decimal,是詞法分析器解析的,而小寫字母開頭的標(biāo)識(shí)符,如 addExpression,是語法分析器解析的,它可以通過 override Visitor 的相應(yīng)函數(shù),改成我們自己的處理。因?yàn)槿笔∏闆r下,ANTLR 4 生成的是 listener,而我想要使用 visitor,所以命令行輸入為:

   java -jar C:ProjectsScriptParser	santlr-4.5.2-complete.jar -visitor -no-listener TinyScript.g4

用上面的命令生成代碼后,我們需要知道怎么才能啟動(dòng)它,可惜這里,至少對(duì)于 C#,文檔寫的要么不全,要么不正確,最后,我找到了正確的打開方式:

using (var ais = new AntlrInputStream(new FileStream(fileName,     FileMode.Open)))
{
var lexer = new TinyScriptLexer(ais);
var tokens = new CommonTokenStream(lexer);
var parser = new TinyScriptParser(tokens);
parser.BuildParseTree = true;
var tree = parser.program();
var visitor = new MyVisitor();
visitor.Visit(tree);
}

上面的 MyVisitor,是我們需要實(shí)現(xiàn)的,它從生成的 TinyScriptBaseVisitor 繼承, TinyScriptBaseVisitor 是個(gè)泛型類,研究后,它的泛型參數(shù)是設(shè)計(jì)用來傳遞返回值的,因?yàn)橐С侄喾N數(shù)據(jù)類型,所以我把它定義為 object 。

在實(shí)現(xiàn) MyVisitor 時(shí),只要每個(gè)節(jié)點(diǎn)都做好自己的工作就可以了。下面我們以 VisitMulExpression 函數(shù)來簡(jiǎn)單介紹一下如何實(shí)現(xiàn)乘除運(yùn)算:

public override object VisitMulExpression([NotNull]     TinyScriptParser.MulExpressionContext context)
{
var a = VisitPrimaryExpression(context.primaryExpression(0));
for (int i = 1; i < context.ChildCount; i += 2)
{
var op = context.GetChild(i).GetText();
var b =     (decimal)VisitPrimaryExpression((TinyScriptParser.PrimaryExpressionContext)context.GetChild(i + 1));
switch (op)
{
case "*":
a = (decimal)a * b;
break;
case "/":
a = (decimal)a / b;
break;
}
}
return a;
}

因?yàn)?mulExpression 的定義中,至少有一個(gè) primaryExpression,然后,可以有任意多乘除運(yùn)算符及相應(yīng)的 primaryExpression ,對(duì)應(yīng)在 VisitMulExpression 函數(shù)中,就是第一個(gè)子節(jié)點(diǎn)是 primaryExpression ,(如果有的話)第二個(gè)子節(jié)點(diǎn)是運(yùn)算符,第三個(gè)子節(jié)點(diǎn)是 primaryExpression,第四個(gè)子節(jié)點(diǎn)是運(yùn)算符……所以,上面的代碼,先通過 VisitPrimaryExpression 取出第一個(gè)節(jié)點(diǎn)值,保存在變量 a 中,然后,通過循環(huán)獲取運(yùn)算符和另一個(gè)值,并進(jìn)行相應(yīng)的運(yùn)算,并把結(jié)果保存在 a 中,最后把運(yùn)算結(jié)果 a 返回。因?yàn)樵?VisitMulExpression 中,只會(huì)處理乘除運(yùn)算,它們是同等的優(yōu)先級(jí),我們也就不用考慮這個(gè)問題,直接運(yùn)算下去就可以了。

要注意的是,如果 mulExpression 只有一個(gè) primaryExpression 節(jié)點(diǎn),它就不一定是 decimal ,所以 a 的類型是 object ,而在進(jìn)行運(yùn)算時(shí),才會(huì)把它強(qiáng)制類型轉(zhuǎn)換成 decimal,因?yàn)檫@時(shí)我們已經(jīng)確定它是 decimal 類型了。

PS:在這里,我們有兩種方式取得子節(jié)點(diǎn)的值,如果定義中用了標(biāo)識(shí)符,就可以直接使用這個(gè)標(biāo)識(shí)符名作為函數(shù)調(diào)用,如上面的 context.primaryExpression(0) ,表示取第一個(gè) primaryExpression ;另一種方法是調(diào)用 GetChild 函數(shù),GetChild 函數(shù)因?yàn)槭峭ㄓ煤瘮?shù),所以經(jīng)常需要強(qiáng)制類型轉(zhuǎn)換為我們需要的類型。

下面,我們來說說變量定義及自動(dòng)類型推斷。

為了實(shí)現(xiàn)變量,我們?cè)谖覀兊?Visitor 中定義一個(gè) Dictionary 類型的變量 Variables ,用來保存變量和它的值,在 VisitDeclareExpression 函數(shù)中,根據(jù)變量類型,在 Variables 中插入相應(yīng)的鍵值對(duì),然后,在賦值時(shí),檢查要被賦值的表達(dá)式的值的類型,是否和 Variables 中的一致,如果不一致,則拋出異常。

public override object VisitAssign([NotNull] TinyScriptParser.AssignContext     context)
{
var name = context.Identifier().GetText();
object obj;
if (!Variables.TryGetValue(name, out obj))
{
throw context.Exception("Variable [{0}] should be definded first.", name);
}
var r = base.VisitAssign(context);
if (obj != null)
{
if (obj.GetType() != r.GetType())
{
throw context.Exception("Cannot assign [{1}] type value to a variable with  type [{0}].", obj.GetType().Name, r.GetType().Name);
}
}
Variables[name] = r;
return null;
}

當(dāng)然,我們也可以選擇不在乎賦值語句兩邊是否類型相同,這樣,它的行為方式就和很多腳本語言如 JavaScript 比較類似,變量在使用中可以改變類型。

不知道你是否注意到了,在上面的描述中,我們說到,我們其實(shí)知道表達(dá)式的結(jié)果的類型,并能在類型不匹配的時(shí)候拋出異常,那么,如果我們選擇在定義類型時(shí),如果變量類型是 var 的話,我們就不處理類型不匹配的問題,就是實(shí)現(xiàn)了自動(dòng)類型推斷!有點(diǎn)小顛覆吧?似乎很高級(jí)的這個(gè)語言特性,其實(shí)是順理成章就可以得到的,不需要什么高大上的技術(shù)。在我們的腳本里,要做到這一點(diǎn),只要在 VisitDeclareExpression 函數(shù)中,遇到 var 時(shí),在插入變量時(shí),變量值是 null 就可以了。

下面,我們?cè)賮砜纯?if 語句的處理,我們頂一個(gè)一個(gè)必須用大括號(hào)包裹的語句組類型 blockStatement , if 語句定義如下:

ifStatement
: "if" quoteExpr blockStatement
| "if" quoteExpr blockStatement "else" blockStatement
;

當(dāng)然,其實(shí),上面的定義和下面這種寫法是等價(jià)的:

ifStatement
: "if" quoteExpr blockStatement ("else" blockStatement)?
;

然后,我們?cè)?VisitIfStatement 函數(shù)中,真的寫一個(gè) if 語句,用來執(zhí)行不同的 blockStatement 就可以了:

public override object VisitIfStatement([NotNull]     TinyScriptParser.IfStatementContext context)
{
var condition = (bool)VisitQuoteExpr(context.quoteExpr());
if (condition)
{
VisitBlockStatement(context.blockStatement(0));
}
else if (context.ChildCount == 5)
{
VisitBlockStatement(context.blockStatement(1));
}
return null;
}

最后那個(gè) return null 是表明,我們的 if 語句不產(chǎn)生任何值。加上對(duì) Visitor 內(nèi)取值遍歷等的理解,這個(gè) if 語句的處理是否看起來非常清晰明了?

最后,來看看循環(huán)語句,我們以 for 循環(huán)為例,先看定義:

forStatement
: "for" "(" commonExpression ";" expression ";" assignAbleStatement ")"   blockStatement
;

再看實(shí)現(xiàn):

public override object VisitForStatement([NotNull] TinyScriptParser.ForStatementContext context)
{
for (VisitCommonExpression(context.commonExpression());
(bool)VisitExpression(context.expression());
VisitAssignAbleStatement(context.assignAbleStatement()))
{
VisitBlockStatement(context.blockStatement());
}
return null;
}

嗯,你沒看錯(cuò),我們真的用了一個(gè) for 循環(huán)來實(shí)現(xiàn) for 循環(huán) :slight_smile:

好了,如果你下載了整個(gè)程序,并編譯成功,我們現(xiàn)在可以編寫一些腳本來做測(cè)試了,比如下面這個(gè)計(jì)算 1 到 100 的和的程序 sum.ts :

var sum = 0;
for(var i=1; i<=100; i=i+1) {
sum = sum + i;
}
print("sum 1 to 100 is : " + sum);

運(yùn)行 ts sum.ts ,控制臺(tái)輸出:

sum 1 to 100 is : 5050

當(dāng)然,這個(gè)腳本語言功能還比較弱,比如不支持函數(shù),比如字符串不支持轉(zhuǎn)義符等;也有一些實(shí)現(xiàn)的不太嚴(yán)格地方,比如強(qiáng)制類型轉(zhuǎn)換如果出錯(cuò),出錯(cuò)信息不準(zhǔn)確等。不過,它是一個(gè)好的開始,可以讓我們?cè)诖嘶A(chǔ)上,設(shè)計(jì)更完善、易用的語言。

OneAPM 為您提供端到端的 Java 應(yīng)用性能解決方案,我們支持所有常見的 Java 框架及應(yīng)用服務(wù)器,助您快速發(fā)現(xiàn)系統(tǒng)瓶頸,定位異常根本原因。分鐘級(jí)部署,即刻體驗(yàn),Java 監(jiān)控從來沒有如此簡(jiǎn)單。想閱讀更多技術(shù)文章,請(qǐng)?jiān)L問 OneAPM 官方技術(shù)博客。

本文轉(zhuǎn)自 OneAPM 官方博客

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/65690.html

相關(guān)文章

  • 何用Docker定制你自己Beego環(huán)境

    摘要:如何用定制你自己的環(huán)境前言學(xué)習(xí)幾個(gè)月了,一直在論壇和群里潛水,一直都想寫點(diǎn)什么回報(bào)大家積極的知識(shí)分享。關(guān)于如何使用,可以參考上面的文章和官方文檔二小試牛刀,用構(gòu)建一個(gè)的環(huán)境并運(yùn)行程序首先來貼上我的先附上這個(gè)項(xiàng)目地址。 如何用Docker定制你自己的Beego環(huán)境 前言: 學(xué)習(xí)golang幾個(gè)月了,一直在論壇和qq群里潛水,一直都想寫點(diǎn)什么回報(bào)大家積極的知識(shí)分享。 前幾日在CSDN上...

    alaege 評(píng)論0 收藏0
  • 何用Python進(jìn)行數(shù)據(jù)分析?

    摘要:編程基礎(chǔ)要學(xué)習(xí)如何用進(jìn)行數(shù)據(jù)分析,數(shù)據(jù)分析師建議第一步是要了解一些的編程基礎(chǔ),知道的數(shù)據(jù)結(jié)構(gòu),什么是向量列表數(shù)組字典等等了解的各種函數(shù)及模塊。數(shù)據(jù)分析師認(rèn)為數(shù)據(jù)分析有的工作都在處理數(shù)據(jù)。 showImg(https://segmentfault.com/img/bVbnbZo?w=1024&h=653); 本文為CDA數(shù)據(jù)分析研究院原創(chuàng)作品,轉(zhuǎn)載需授權(quán) 1.為什么選擇Python進(jìn)行數(shù)...

    lifefriend_007 評(píng)論0 收藏0
  • java.lang.NoSuchMethodError: antlr.collections.AST

    摘要:具體操作如下在下,在文本框中搜索選擇第二種可能性解決主要是里面的和中的與沖突刪除即可,具體方法在下,在文本框中搜索選擇在該應(yīng)用的目錄刪除目前貌似就這么兩種解決方法吧親測(cè)第一種可用 showImg(https://segmentfault.com/img/bVOIUy?w=590&h=49); showImg(https://segmentfault.com/img/bVOIXc?w=5...

    Zachary 評(píng)論0 收藏0
  • 精讀《手寫 SQL 編譯器 - 智能提示》

    摘要:經(jīng)過連續(xù)幾期的介紹,手寫編譯器系列進(jìn)入了智能提示模塊,前幾期從詞法到文法語法,再到構(gòu)造語法樹,錯(cuò)誤提示等等,都是為智能提示做準(zhǔn)備。 1 引言 詞法、語法、語義分析概念都屬于編譯原理的前端領(lǐng)域,而這次的目的是做 具備完善語法提示的 SQL 編輯器,只需用到編譯原理的前端部分。 經(jīng)過連續(xù)幾期的介紹,《手寫 SQL 編譯器》系列進(jìn)入了 智能提示 模塊,前幾期從 詞法到文法、語法,再到構(gòu)造語法...

    ztyzz 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

scwang90

|高級(jí)講師

TA的文章

閱讀更多
最新活動(dòng)
閱讀需要支付1元查看
<