用最快的速度設(shè)計(jì)一種新的編程語(yǔ)言
最近打算寫(xiě)一些列有趣、而且有一定難度的文章。這個(gè)系列的名字就叫《瘋狂極客》,這一系列的文章大多數(shù)與計(jì)算機(jī)有密切的關(guān)系。包括制作編譯器、制作OS、Android控制電路板、機(jī)器人的制作(通過(guò)Android、IOS等設(shè)備控制)等等。
在正式開(kāi)始《瘋狂極客》系列文章之前,先來(lái)熱熱身。用最短的時(shí)間設(shè)計(jì)一種簡(jiǎn)單,但好玩的編程語(yǔ)言CShell(不過(guò)不用擔(dān)心,實(shí)現(xiàn)CShell解析器基本上用不著編譯原理的知識(shí),但以后的文章就會(huì)涉及到很多編譯原理的內(nèi)容了)。從CShell的名稱可以猜到,是一種C風(fēng)格的語(yǔ)言,并且可以像Shell一樣解釋執(zhí)行(動(dòng)態(tài)語(yǔ)言)。當(dāng)然,這種語(yǔ)言不可能像C語(yǔ)言或Shell一樣強(qiáng)大,因?yàn)镃語(yǔ)言的編譯器實(shí)現(xiàn)起來(lái)盡管不復(fù)雜(因?yàn)槭墙Y(jié)構(gòu)化編程語(yǔ)言,沒(méi)有類、接口這些東西,實(shí)現(xiàn)起來(lái)要比Java編譯器簡(jiǎn)單得多),但仍然不太可能在很短的時(shí)間內(nèi)完成(一至兩天)。不過(guò)本文實(shí)現(xiàn)的CShell盡管簡(jiǎn)單,但仍然可以實(shí)現(xiàn)一些算法。CShell語(yǔ)言支持輸出值和變量、條件語(yǔ)句(if),for循環(huán),自加、自減、+、-、*、/操作,函數(shù)(支持遞歸)。由于CShell是動(dòng)態(tài)語(yǔ)言,所以不需要聲明變量,不過(guò)支持全局和局部變量,當(dāng)然,還支持?jǐn)?shù)組(整數(shù)、字符串類型數(shù)組),所以使用CShell可以很容易實(shí)現(xiàn)像冒泡排序、階乘等算法。
在討論CShell的設(shè)計(jì)原理和實(shí)現(xiàn)過(guò)程之前,先看一些用CShell編寫(xiě)的程序。單從這些程序所完成的工作來(lái)看都太太太簡(jiǎn)單了,不過(guò)這回完全不同,這回是用我們自己發(fā)明的新語(yǔ)言來(lái)實(shí)現(xiàn)這些算法,例如,遞歸階乘計(jì)算、冒泡排序,是不是很酷呢?。et’s go!
- // 簡(jiǎn)單的變量輸出
- xx= 45;
- _ok = 64;
- print (xx);
- a1 = 65;
- print (a1);
- // 數(shù)組演示
- $arr = [1,2,3,4,5,"aa"]; // 數(shù)值與字符串換搭的數(shù)組,$表示全局變量
- print($arr); // 輸出數(shù)組的所有元素
- print($arr[2]); // 輸出數(shù)組的第3個(gè)元素
- // 三重for循環(huán)
- $x = 0; // 全局變量
- // i、j、z都為局部變量,for循環(huán)外不可訪問(wèn)
- for(i=0;i<10;i=i+1)
- {
- for(j = 0; j < 10; j=j+1)
- {
- for(z = 0; z < 10; z = z + 1)
- {
- $x = $x + 1;
- }
- }
- }
- print($x); // 輸出1000
- // 計(jì)算10的階乘,涉及到函數(shù)的遞歸操作和if語(yǔ)句
- def jc(n)
- {
- if(n == 0)
- {
- return 1;
- }
- else if(n == 1)
- {
- return 1;
- }
- else
- {
- return jc(n - 1) *n;
- }
- }
- print("10!");
- print(jc(10)); // 計(jì)算10的階乘(3628800)
- // 冒泡排序(降序)
- $arr = [5,3,1,7,5,4,-56,12];
- $len = length($arr);
- // 雙循環(huán)冒泡排序
- for(i = 0; i < $len; i++)
- {
- for(j = 0; j < $len - 1; j++)
- {
- if($arr[j] < $arr[j+1])
- {
- x = $arr[j+1];
- $arr[j+1] = $arr[j];
- $arr[j] = x;
- }
- }
- }
- print($arr); // 輸出[12, 7, 5, 5, 4, 3, 1, -56]
如何設(shè)計(jì)和實(shí)現(xiàn)編程語(yǔ)言
設(shè)計(jì)一種編程語(yǔ)言的方法很多,當(dāng)然,通常的做法是要學(xué)好編譯原理,然后按部就班地從詞法分析器做起,然后是詞法分析器、語(yǔ)義分析、中間代碼生成、中間代碼優(yōu)化,目標(biāo)代碼生成,如果語(yǔ)言需要使用runtime運(yùn)行,還需要編寫(xiě)可以運(yùn)行目標(biāo)代碼的虛擬機(jī)(解釋目標(biāo)代碼的程序,例如jvm就是解析Java字節(jié)碼文件的虛擬機(jī))??粗陀悬c(diǎn)暈。而且估計(jì)現(xiàn)在很多科班出身的程序員編譯原理學(xué)的一塌糊涂。就算編譯原理學(xué)的很好,光憑編譯原理的理論,如果要想編寫(xiě)一個(gè)比較復(fù)雜的編譯器或解析器也是很難辦到的(尤其是加入面向?qū)ο蠊δ埽_@是因?yàn)橐粋€(gè)復(fù)雜的編譯器有很多代碼幾乎不太可能完全通過(guò)手工編寫(xiě),例如,語(yǔ)法分析如果使用LL(*)分析方式,計(jì)算大量的first和follow集合就非??植?,就算把代碼編寫(xiě)完了,如果要為語(yǔ)言增加或修改新的語(yǔ)法,修改這些代碼將又是一場(chǎng)惡夢(mèng)。所以大多數(shù)復(fù)雜的工業(yè)級(jí)編程語(yǔ)言都是通過(guò)半自動(dòng)化的方式完成的。
所謂半自動(dòng)化,就是指不可能完全通過(guò)自動(dòng)的方式生成編譯器,而只能通過(guò)自動(dòng)的方式生成編譯器最核心的部分:詞法分析器和語(yǔ)法分析器。基本的做法是通過(guò)DSL(domain-specific language )指定詞法和語(yǔ)法的結(jié)構(gòu)和必要的信息,然后編譯器的編譯器(生成編譯器的程序)會(huì)根據(jù)DSL自動(dòng)生成詞法和語(yǔ)法解析器,當(dāng)然,通過(guò)DSL可以增加語(yǔ)義部分的代碼,這樣生成的程序就直接擁有語(yǔ)義解析功能了。
對(duì)于很多***的企業(yè),如google、微軟、intel、IBM,都會(huì)有自己的CC(編譯器的編譯器),不過(guò)對(duì)于個(gè)人或小企業(yè),完全開(kāi)發(fā)一套CC難度會(huì)很大(這東西比開(kāi)發(fā)一套編譯器的難度更大)。所以我們可以使用開(kāi)源免費(fèi)的CC。例如JavaCC、lex、yacc、antlr等。其中JavaCC只支持Java語(yǔ)言,lex是詞法分析器的生成器、yacc是語(yǔ)法分析器的生成器,這兩個(gè)支持從C語(yǔ)言,而antlr支持多種語(yǔ)言,如Java、C#、ruby、C/C++、JavaScript等等。所以本文使用Antlr來(lái)設(shè)計(jì)和實(shí)現(xiàn)CShell語(yǔ)言。
CShell語(yǔ)言是如何練成的
盡管CShell依靠Antlr來(lái)實(shí)現(xiàn),需要自己編寫(xiě)的代碼仍然非常多,因此本文只介紹核心的代碼和實(shí)現(xiàn)原理。更詳細(xì)的代碼請(qǐng)參考本文提供的源代碼。
學(xué)過(guò)編譯原理的讀者首先就會(huì)想到,設(shè)計(jì)語(yǔ)言首先就是進(jìn)行詞法分析,然后根據(jù)詞法分析的結(jié)果進(jìn)行語(yǔ)法分析。幸運(yùn)的是,這兩樣都可以利用Antlr自動(dòng)完成。
所謂詞法分析,就是將語(yǔ)言文本分成最小但愿的詞素(稱為Token)。例如,下面的是一段CShell語(yǔ)言的代碼。
- for(i = 0; i < $len; i++)
- {
- }
如果要對(duì)這段代碼進(jìn)行詞法分析,就會(huì)分解成如下的一系列Token
- “for”、“(”、“i”、“=”、“0”、“;”、“i”、“<”、“$len”、“;”、“i”、“++”、“{”、“}”
當(dāng)然,要想自己編程實(shí)現(xiàn)這個(gè)分析,就需要使用到有限自動(dòng)機(jī)(DFA)進(jìn)行處理,盡管程序不復(fù)雜,但還是比較麻煩的。有了Antlr,就容易得多了。通常只要定義這些Token的規(guī)則即可。有些Token是與語(yǔ)法規(guī)則放到一起的,有些是單獨(dú)的詞法規(guī)則。例如,上面代碼中有兩個(gè)變量(i和$len),其中i局部變量、$len為全局變量,這兩個(gè)變量都屬于標(biāo)識(shí)符范疇,所以可以定義一個(gè)專門識(shí)別標(biāo)識(shí)符號(hào)的詞法規(guī)則。
- ID : '$'?(LETTER|'_') (LETTER | '0'..'9')* ;
其中ID是詞法規(guī)則名稱,詞法規(guī)則名稱的***個(gè)字母必須大寫(xiě)。LETTER表示26個(gè)小寫(xiě)和26個(gè)大寫(xiě)字母。“?”表示可以有,也可能沒(méi)有,“*”為星閉包,表示重復(fù)0次到N次。
- LETTER: ('a'..'z' | 'A'..'Z')
從ID的詞法規(guī)則可以看出,ID就是可能以“$”開(kāi)頭,也可能沒(méi)有“$”。不管有沒(méi)有“$”,下一個(gè)字符必須是字母或下劃線,接下來(lái)的字母或者是字母、或者是數(shù)字的任意字符串。例如abc、_xyz123、$_23都認(rèn)為是ID。Antlr會(huì)自動(dòng)根據(jù)這個(gè)規(guī)則生成Java代碼。
其他的Token分析也采用類似的方法,例如,識(shí)別字符串可以使用下面的規(guī)則。
- STRING: '\"' .* '\"' ;
其中“.*”表示任意字符序列。也就是在CShell里一個(gè)字符串就是在兩個(gè)雙引號(hào)中的任意字符序列。
詞法處理完,就是相應(yīng)的語(yǔ)法了,詞法的分析結(jié)果是Token序列,而這個(gè)序列正式語(yǔ)法分析的輸入。也就是說(shuō)語(yǔ)法分析和詞法分析的方式很像,只是詞法分析的輸入是單個(gè)字符序列,輸出是Token序列。而語(yǔ)法分析的輸入是Token序列,輸出可能有多種,也可能沒(méi)有輸出,在分析的過(guò)程中就執(zhí)行相應(yīng)的動(dòng)作(語(yǔ)義處理),也可能生成AST(抽象語(yǔ)法樹(shù)),然后進(jìn)一步對(duì)其進(jìn)行優(yōu)化。本例使用的是AST方式,也就是說(shuō)將CShell源代碼經(jīng)過(guò)語(yǔ)法分析后轉(zhuǎn)換為一顆AST,目的是去掉一些雜質(zhì),例如,for循環(huán)中只有i、$len、++等標(biāo)識(shí)符和運(yùn)算符號(hào)是有用的,但左右括號(hào)就沒(méi)有任何用處,這些輔助符號(hào)是為了區(qū)分for語(yǔ)句和其他語(yǔ)句的。
這里只看一個(gè)稍微簡(jiǎn)單的if語(yǔ)句的語(yǔ)法規(guī)則。
- statement : 'if' '(' expr ')' slist elseif_statement_all else_statement?
其中slist是另外一個(gè)產(chǎn)生式,表示if和else if之間的部分。
- slist // 原內(nèi)容: ':' NL (statement)+ '.' NL
- : NL*'{' NL* (statement)* NL* '}'NL* -> ^(BLOCK statement*)
- ;
其中NL表示空行。而^(BLOCK statement*)部分表示AST,其中BLOCK為AST的根節(jié)點(diǎn),從這一點(diǎn)可以看出,AST已經(jīng)將slist中的左右大括號(hào)都過(guò)濾出去了,只剩下有實(shí)際意義的statement。
從statement和slist的定義可以看出,if語(yǔ)句必須以“if”開(kāi)頭,Antlr會(huì)將if作為一個(gè)Token返回給語(yǔ)法分析器。然后緊跟著if的是左括號(hào),接觸是表達(dá)式(expr,另外一個(gè)產(chǎn)生式),然后就是if語(yǔ)句的執(zhí)行體(slist),接著就是elseif部分,剩下的部分就與if部分的定義類似了,請(qǐng)讀者參看源代碼中的antlr/CShell.g文件。
那么編寫(xiě)完Antlr需要的DSL,接下來(lái)做什么呢?接下來(lái)就要自己來(lái)做語(yǔ)義部分,這部分內(nèi)容非常復(fù)雜,基本的思想就是通過(guò)語(yǔ)法分析將變量、關(guān)鍵字(for、if等)返回,然后由語(yǔ)義部分決定如何做。例如,對(duì)于變量,通常做法是定義一個(gè)符號(hào)表(使用Map對(duì)象即可),變量名就是Map的key,先將該變量存儲(chǔ)在Map對(duì)象中。如果遇到某個(gè)變量,會(huì)首先到Map對(duì)象中查找,如果未找到,就定義該變量(將變量和變量值存入Map對(duì)象),如果找到,就直接去除變量值使用。至于for、if語(yǔ)句如何處理,就要利用語(yǔ)法分析生成的AST了。
其中Interpreter類是分析的核心類,給類有一個(gè)exec方法,需要將AST的根節(jié)點(diǎn)傳入該方法,也就是說(shuō)執(zhí)行CShell代碼的過(guò)程就是遍歷AST的過(guò)程,AST是多叉樹(shù),遍歷需要使用廣度優(yōu)先方式遍歷。exec方法的代碼如下:
- // CShellAST表示AST節(jié)點(diǎn)的類型,一個(gè)普通Java類
- public Object exec(CShellAST ast)
- {
- try
- {
- switch (ast.getType())
- {
- case CShellParser.BLOCK: // 處理塊操作
- block(ast);
- break;
- case CShellParser.ASSIGN: // 處理賦值操作
- assign(ast);
- break;
- case CShellParser.LENGTH: // 處理返回長(zhǎng)度操作
- return length(ast);
- case CShellParser.ARRAY: // 處理數(shù)組操作
- arrayStat(ast);
- break;
- case CShellParser.RETURN:
- ret(ast);
- break;
- case CShellParser.PRINT:
- print(ast);
- break;
- case CShellParser.IF: // 處理if語(yǔ)句
- ifstat(ast);
- break;
- case CShellParser.FOR:
- forloop(ast);
- break;
- case CShellParser.CALL:
- return call(ast);
- case CShellParser.ADD:
- return add(ast);
- case CShellParser.PREV:
- case CShellParser.SUFFIX:
- return incAndDec(ast);
- case CShellParser.SUB:
- return op(ast);
- case CShellParser.MUL:
- case CShellParser.DIV:
- return op(ast);
- case CShellParser.EQ:
- return eq(ast);
- case CShellParser.LT:
- return lt(ast);
- case CShellParser.GT:
- return gt(ast);
- case CShellParser.INT:
- return Integer.parseInt(ast.getText());
- case CShellParser.CHAR:
- return new Character(ast.getText().charAt(1));
- case CShellParser.FLOAT:
- return Float.parseFloat(ast.getText());
- case CShellParser.STRING:
- String s = ast.getText();
- return s.substring(1, s.length() - 1);
- case CShellParser.ID:
- case CShellParser.ARRAY_ELEMENT:
- return load(ast);
- default: // catch unhandled node types
- throw new UnsupportedOperationException("無(wú)法處理"
- + ast.getText() + "<" + ast.getType() + ">");
- }
- }
- catch (Exception e)
- {
- listener.error("異常原因: " + ast.toStringTree(), e);
- }
- return null;
- }
下面只看一個(gè)如何處理if語(yǔ)句的ifstat方法的實(shí)現(xiàn)代碼
- private void ifstat(CShellAST ast)
- {
- // 下面的代碼需要從當(dāng)前AST節(jié)點(diǎn)(表示if語(yǔ)句根節(jié)點(diǎn))的子節(jié)點(diǎn)獲取
- // if語(yǔ)句的各個(gè)組成部分
- // 獲取if語(yǔ)句的兩個(gè)圓括號(hào)直接的表達(dá)式部分
- CShellAST expr = (CShellAST) ast.getChild(0);
- // 獲取if條件如果為true要執(zhí)行的代碼塊
- CShellAST ifBlock = (CShellAST) ast.getChild(1);
- // 獲取elseif的部分(包括條件表達(dá)式和要執(zhí)行的塊)
- CShellAST elseifAll = (CShellAST) ast.getChild(2);
- // 獲取else部分要執(zhí)行的代碼塊
- CShellAST elseBlock = (CShellAST) ast.getChild(3);
- // 利用遞歸方式再次調(diào)用exec方法執(zhí)行表達(dá)式,并返回值
- Boolean c = (Boolean) exec(expr);
- // 如果為true,執(zhí)行if block
- if (c.booleanValue())
- {
- exec(ifBlock); // 遞歸執(zhí)行if block
- }
- else
- {
- // 判斷有多少個(gè)elseif部分,CShell支持有無(wú)限多個(gè)else if語(yǔ)句
- if (elseifAll.getChildCount() > 0)
- {
- List<CShellAST> children = elseifAll.getChildren();
- // 挨個(gè)判斷else if后面的表達(dá)式是否為true
- for (CShellAST child : children)
- {
- expr = (CShellAST) child.getChild(0);
- ifBlock = (CShellAST) child.getChild(1);
- c = (Boolean) exec(expr);
- // 如果某個(gè)else if條件為true,直接執(zhí)行else if后面的代碼塊,
- // ***返回,剩下的都不執(zhí)行了
- if (c.booleanValue())
- {
- exec(ifBlock);
- return;
- }
- }
- }
- // ***會(huì)執(zhí)行else語(yǔ)句(因?yàn)榍懊娴臈l件都為false)
- // 判斷是否有else語(yǔ)句(最多只能有1個(gè)else子句)
- if (elseBlock.getChildCount() == 1)
- {
- exec((CShellAST) elseBlock.getChild(0)); // 執(zhí)行else block
- }
- }
- }
CShell代碼分析器的入口類是CShell,在該類中調(diào)用了Interprefer.process方法讀者CShell語(yǔ)言源代碼。其中bubble.cs就是CShell語(yǔ)言的源代碼文件,可以換成其他的源代碼文件。調(diào)用process方法后,就會(huì)根據(jù)具體的CShell代碼執(zhí)行相應(yīng)的操作。例如,print(…)語(yǔ)句會(huì)輸出相應(yīng)的字符串。
- public class CShell
- {
- public static void main(String[] args) throws Exception
- {
- InputStream input = null;
- input = new FileInputStream("source/bubble.cs");
- Interpreter interp = new Interpreter();
- interp.process(input);
- }
- }
如果讀者對(duì)Antlr還不太理解也沒(méi)關(guān)系,本文只是拋磚引玉,目的并不是講解Antlr。只是希望讀者對(duì)Antlr以及設(shè)計(jì)一種語(yǔ)言的過(guò)程有所了解。在后面的一系列文章中將會(huì)深度探討編譯原理以及Antlr的使用方法。通過(guò)設(shè)計(jì)自己的專有語(yǔ)言***的作用是可以顯著提高工作效率,例如,可以將常用的工作抽象成某些語(yǔ)句,到時(shí)只要一執(zhí)行腳本就可完成需要數(shù)小時(shí),甚至數(shù)天才能完成的工作。
原文鏈接:http://www.cnblogs.com/nokiaguy/archive/2013/03/12/2955128.html