摘要:是一個實現的詞法語法分析生成程序,目前最新版本為,支持,,等語言,這里我們用來實現一個自己的腳本語言。在實現時,只要每個節點都做好自己的工作就可以了。不過,它是一個好的開始,可以讓我們在此基礎上,設計更完善易用的語言。
ANTLR 是一個 Java 實現的詞法/語法分析生成程序,目前最新版本為 4.5.2,支持 Java,C#,JavaScript 等語言,這里我們用 ANTLR 4.5.2 來實現一個自己的腳本語言。
因為某些未知原因,ANTLR 官方的文檔似乎有些地方和 4.5.2 版的實際情況不太吻合,所以,有些部分,我們必須多方查找和自己實踐得到,所幸 ANTLR 的文檔比較豐富,其在 Github 上例子程序也很多,足夠我們探索的了。
如果你沒有編譯原理的基礎,只要寫過正則表達式,應該也能很快理解其規則,進而編寫自己的規則文件,事實上,因為結構更清晰, ANTLR 的規則文件,比正則表達式要簡單得多。
我使用 C# 版本,所以下載了 antlr-4.5.2-complete.jar 和 C# 的支持庫 Antlr4.Runtime.dll。
ANTLR 官方網址 http://www.antlr.org/
ANTLR 官方 Github https://github.com/antlr/antlr4
大量語法文件例子 https://github.com/antlr/grammars-v4
因為文章中不適合貼全部的代碼,建議下載了 TinyScript 的代碼后,和此文章對照閱讀和實踐。
本文程序的 Github https://github.com/Lifeng-Liang/TinyScri...
好了,進入正題,我們要定義一個解釋型的腳本語言,就起個名叫 TinyScript 好了,規則文件名 TinyScript.g4 ,簡單起見,暫不實現函數,具體實現的功能如下:
變量,支持的數據類型為 decimal,bool,string,不支持 null
變量賦值支持自動類型推斷,用 var 標識
四則運算,支持字符串通過 + 進行連接
支持比較運算符,支持與或非運算符
if 語句,語句塊必須用大括號包裹
while,do/while,for 循環,同樣語句塊必須用大括號包裹
一個內置的輸出函數 print,可以輸出表達式的值到控制臺
先說四則運算。四則運算里,除了括號外,需要先乘除,后加減,這個規則在 ANTLR 里怎么實現呢?
在 ANTLR 里,我們寫的規則,會生成解析器的代碼,這個解析器,會把目標腳本,解析成一個抽象語法樹。這顆抽象語法樹上,越是靠近葉子節點的地方,結合優先級越高,越是靠近根的地方,結合優先級越低,根據這個特點,我們就可以讓 ANTLR 幫我們完成以上的規則:
addExpression : mulExpression (("+" | "-") mulExpression)* ; mulExpression : primaryExpression (("*" | "/") primaryExpression)* ; primaryExpression : Decimal | "(" addExpression ")" ;
上面展示的 ANTLR 規則,在 primaryExpression 中,包括兩個可選項,要么是數字,要么是括號表達式,是最高優先級,然后是 mulExpression,優先級最低的是 addExpression 。括號表達式內,是一個 addExpression ,所以,這是一個循環結構,可以處理無限長的四則運算式,比如 1+2*3-(4+5)/6+7+8,會被解析為如下的語法樹:
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
以上的語法樹,其實是我簡化了的,比如,其中的數字 1 其實應該是 ·mulExpression ,而這個 mulExpression 只有一項 primaryExpression,而這個 primaryExpression,是 Decimal,其值為 1 。
PS: 在 ANTLR 中,大寫字母開頭的標識符,如上面的 Decimal,是詞法分析器解析的,而小寫字母開頭的標識符,如 addExpression,是語法分析器解析的,它可以通過 override Visitor 的相應函數,改成我們自己的處理。因為缺省情況下,ANTLR 4 生成的是 listener,而我想要使用 visitor,所以命令行輸入為:
java -jar C:ProjectsScriptParser santlr-4.5.2-complete.jar -visitor -no-listener TinyScript.g4
用上面的命令生成代碼后,我們需要知道怎么才能啟動它,可惜這里,至少對于 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,是我們需要實現的,它從生成的 TinyScriptBaseVisitor 繼承, TinyScriptBaseVisitor 是個泛型類,研究后,它的泛型參數是設計用來傳遞返回值的,因為要支持多種數據類型,所以我把它定義為 object 。
在實現 MyVisitor 時,只要每個節點都做好自己的工作就可以了。下面我們以 VisitMulExpression 函數來簡單介紹一下如何實現乘除運算:
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; }
因為 mulExpression 的定義中,至少有一個 primaryExpression,然后,可以有任意多乘除運算符及相應的 primaryExpression ,對應在 VisitMulExpression 函數中,就是第一個子節點是 primaryExpression ,(如果有的話)第二個子節點是運算符,第三個子節點是 primaryExpression,第四個子節點是運算符……所以,上面的代碼,先通過 VisitPrimaryExpression 取出第一個節點值,保存在變量 a 中,然后,通過循環獲取運算符和另一個值,并進行相應的運算,并把結果保存在 a 中,最后把運算結果 a 返回。因為在 VisitMulExpression 中,只會處理乘除運算,它們是同等的優先級,我們也就不用考慮這個問題,直接運算下去就可以了。
要注意的是,如果 mulExpression 只有一個 primaryExpression 節點,它就不一定是 decimal ,所以 a 的類型是 object ,而在進行運算時,才會把它強制類型轉換成 decimal,因為這時我們已經確定它是 decimal 類型了。
PS:在這里,我們有兩種方式取得子節點的值,如果定義中用了標識符,就可以直接使用這個標識符名作為函數調用,如上面的 context.primaryExpression(0) ,表示取第一個 primaryExpression ;另一種方法是調用 GetChild 函數,GetChild 函數因為是通用函數,所以經常需要強制類型轉換為我們需要的類型。
下面,我們來說說變量定義及自動類型推斷。
為了實現變量,我們在我們的 Visitor 中定義一個 Dictionary 類型的變量 Variables ,用來保存變量和它的值,在 VisitDeclareExpression 函數中,根據變量類型,在 Variables 中插入相應的鍵值對,然后,在賦值時,檢查要被賦值的表達式的值的類型,是否和 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; }
當然,我們也可以選擇不在乎賦值語句兩邊是否類型相同,這樣,它的行為方式就和很多腳本語言如 JavaScript 比較類似,變量在使用中可以改變類型。
不知道你是否注意到了,在上面的描述中,我們說到,我們其實知道表達式的結果的類型,并能在類型不匹配的時候拋出異常,那么,如果我們選擇在定義類型時,如果變量類型是 var 的話,我們就不處理類型不匹配的問題,就是實現了自動類型推斷!有點小顛覆吧?似乎很高級的這個語言特性,其實是順理成章就可以得到的,不需要什么高大上的技術。在我們的腳本里,要做到這一點,只要在 VisitDeclareExpression 函數中,遇到 var 時,在插入變量時,變量值是 null 就可以了。
下面,我們再來看看 if 語句的處理,我們頂一個一個必須用大括號包裹的語句組類型 blockStatement , if 語句定義如下:
ifStatement : "if" quoteExpr blockStatement | "if" quoteExpr blockStatement "else" blockStatement ;
當然,其實,上面的定義和下面這種寫法是等價的:
ifStatement : "if" quoteExpr blockStatement ("else" blockStatement)? ;
然后,我們在 VisitIfStatement 函數中,真的寫一個 if 語句,用來執行不同的 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; }
最后那個 return null 是表明,我們的 if 語句不產生任何值。加上對 Visitor 內取值遍歷等的理解,這個 if 語句的處理是否看起來非常清晰明了?
最后,來看看循環語句,我們以 for 循環為例,先看定義:
forStatement : "for" "(" commonExpression ";" expression ";" assignAbleStatement ")" blockStatement ;
再看實現:
public override object VisitForStatement([NotNull] TinyScriptParser.ForStatementContext context) { for (VisitCommonExpression(context.commonExpression()); (bool)VisitExpression(context.expression()); VisitAssignAbleStatement(context.assignAbleStatement())) { VisitBlockStatement(context.blockStatement()); } return null; }
嗯,你沒看錯,我們真的用了一個 for 循環來實現 for 循環 :slight_smile:
好了,如果你下載了整個程序,并編譯成功,我們現在可以編寫一些腳本來做測試了,比如下面這個計算 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);
運行 ts sum.ts ,控制臺輸出:
sum 1 to 100 is : 5050
當然,這個腳本語言功能還比較弱,比如不支持函數,比如字符串不支持轉義符等;也有一些實現的不太嚴格地方,比如強制類型轉換如果出錯,出錯信息不準確等。不過,它是一個好的開始,可以讓我們在此基礎上,設計更完善、易用的語言。
OneAPM 為您提供端到端的 Java 應用性能解決方案,我們支持所有常見的 Java 框架及應用服務器,助您快速發現系統瓶頸,定位異常根本原因。分鐘級部署,即刻體驗,Java 監控從來沒有如此簡單。想閱讀更多技術文章,請訪問 OneAPM 官方技術博客。
本文轉自 OneAPM 官方博客
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/65690.html
摘要:如何用定制你自己的環境前言學習幾個月了,一直在論壇和群里潛水,一直都想寫點什么回報大家積極的知識分享。關于如何使用,可以參考上面的文章和官方文檔二小試牛刀,用構建一個的環境并運行程序首先來貼上我的先附上這個項目地址。 如何用Docker定制你自己的Beego環境 前言: 學習golang幾個月了,一直在論壇和qq群里潛水,一直都想寫點什么回報大家積極的知識分享。 前幾日在CSDN上...
摘要:編程基礎要學習如何用進行數據分析,數據分析師建議第一步是要了解一些的編程基礎,知道的數據結構,什么是向量列表數組字典等等了解的各種函數及模塊。數據分析師認為數據分析有的工作都在處理數據。 showImg(https://segmentfault.com/img/bVbnbZo?w=1024&h=653); 本文為CDA數據分析研究院原創作品,轉載需授權 1.為什么選擇Python進行數...
摘要:具體操作如下在下,在文本框中搜索選擇第二種可能性解決主要是里面的和中的與沖突刪除即可,具體方法在下,在文本框中搜索選擇在該應用的目錄刪除目前貌似就這么兩種解決方法吧親測第一種可用 showImg(https://segmentfault.com/img/bVOIUy?w=590&h=49); showImg(https://segmentfault.com/img/bVOIXc?w=5...
摘要:經過連續幾期的介紹,手寫編譯器系列進入了智能提示模塊,前幾期從詞法到文法語法,再到構造語法樹,錯誤提示等等,都是為智能提示做準備。 1 引言 詞法、語法、語義分析概念都屬于編譯原理的前端領域,而這次的目的是做 具備完善語法提示的 SQL 編輯器,只需用到編譯原理的前端部分。 經過連續幾期的介紹,《手寫 SQL 編譯器》系列進入了 智能提示 模塊,前幾期從 詞法到文法、語法,再到構造語法...
閱讀 3072·2021-10-11 10:58
閱讀 1989·2021-09-24 09:47
閱讀 502·2019-08-30 14:19
閱讀 1684·2019-08-30 13:58
閱讀 1444·2019-08-29 15:26
閱讀 640·2019-08-26 13:45
閱讀 2139·2019-08-26 11:53
閱讀 1772·2019-08-26 11:30