DSL自己只是一層爲了表達的能力而作的淺淺封裝,在你考慮DSL的時候你應該將絕大部分的精力花在構建語義模型上,但反過來講DSL的設計從某種程度上來講幫你理清了語義模型的需求,這讓咱們看起來開始像一條產品狗了(笑),接下來咱們大體過一下DSL設計和實現主要考慮的問題吧。前端
接下來咱們將分別詳細介紹這些主題。java
這裏的解析DSL顯然指的是外部DSL,那麼咱們接下來討論的你可能在編譯原理前端都已經聽到過了,但我仍是須要在這裏再簡要地講述一遍,請原諒個人囉嗦。node
解析的過程主要能夠分爲詞法解析、語法解析,視你的DSL的複雜程度你可能還須要在中間生成符號表,使用語境變量和維護做用域上下文。詞法解析過程將DSL文本解析成一個Token Stream,token包含詞的原始內容和位置,同時這個過程將會丟棄一些無用的字符好比空格符等。語法解析從Token Stream中生成語法解析樹,在生成的過程當中你能夠採起一系列的動做來組裝你的語義模型,這一點咱們到下一節再講。express
接下來讓咱們考慮一下最簡單的情形,好比讀入一個CSV文件,咱們並不須要定義語法,經過使用','分隔每一行就足夠進行解析了,這就是所謂的分隔符指導翻譯,因爲語法過於簡單,你只是經過分隔符作了一下詞法解析。編程
咱們能夠考慮一個稍微複雜的場景,你須要從一個日誌文件中的日誌記錄快速讀取若干信息,好比錯誤類型、相關用戶,這時候你能夠考慮使用正則詞法分析,你快速得到了你的詞條,而且並無什麼語法解析的過程就能夠組裝出你的語義模型(多是一個鍵值對或者一行記錄)。ruby
上面所列舉的例子都沒有涉及到語法分析,對於簡單的情形,你能夠根據本身的邏輯編寫LL和LR解析器進行解析而不須要一個顯式的文法定義。但每每咱們首先須要定義咱們的文法並經過文法來指導咱們作語義解析。關於BNF文法和EBNF文法的內容這裏就再也不說起了,你能夠自行查閱。當你不採用語法樹來幫助你進行解析的狀況下,上面方法幾乎都是在組合解析器,欲匹配A規則,A由B和C組成,則先匹配B再匹配C,整個解析過程基於這種嵌套遞歸,有人將這種解析過程爲解析器組合子。這裏面有一些有趣的情形,當兩條文法規則擁有相同的Token前綴的時候,咱們怎麼解決?能夠經過向前看K步直到發現不一致的地方爲止,擴展到極端狀況下就是回溯法。咱們還能夠再作一點優化,在回溯的時候記錄下已經匹配上的子規則和其在標記流中的位置,切換到其餘候選規則時就能夠沒必要重複匹配相同前綴了,有些人將這種方法稱爲是記憶解析器。須要咱們注意的是否使用IR如語法樹對於語法解析的過程並沒有本質區別,不一樣點在於使用IR能夠屢次遍歷語法結構更方便構建語義模型,同時AST等能夠經過許多已有工具快速生成。網絡
對於上下文相關文法,對於C++好比T(x)這種,既多是函數調用也多是類型轉換,你必須結合上下文來判斷,看有沒有T的函數定義。這時候你可能須要構建符號表和做用域上下文來幫助你作謂詞解析(語義預測)。閉包
生成語義模型幾乎是咱們今天最重要的問題了,我我的認爲這也幾乎是DSL開發的核心部分,咱們從內部DSL和外部DSL分別介紹這個問題的解決方案。ide
從外部DSL生成語義模型,咱們能夠根據是否採用IR分開來考察,當不使用IR的時候,咱們是當匹配上某一規則執行對應的操做,好比向模型裏填充數據。可是當你有回溯動做的時候,一旦某個規則匹配失敗,你可能不得不清除其子規則作出的更改。因此好的作法並非直接作出對最終模型修改的動做,而是生成中間對象或者藉助生成器,若是想象爲樹的構建,那麼動做必定是在從下至上出子樹時執行。函數
當藉助IR進行構建時,一方面能夠在構建AST時執行相應操做,另一方面能夠在構建AST完畢後再次遍歷構建語義模型,大多數狀況下這兩種方法能夠結合起來使用。下面咱們以ANTLR爲例介紹藉助於AST的種種構建方法。
grammar Rows; @parser::members { // add members to generated RowsParser int col; public RowsParser(TokenStream input, int col) { // custom constructor this(input); this.col = col; } } file: (row NL)+ ; row locals [int i=0] : ( STUFF { $i++; if ( $i == col ) System.out.println($STUFF.text); } )+ ; TAB : '\t' -> skip ; // match but don't pass to the parser NL : '\r'? '\n' ; // match and pass to the parser STUFF: ~[\t\r\n]+ ; // match any chars except tab, newline
a returns [string expr] : b {$expr = $b.expr;} | c {$expr = $c.expr;} ;
scott handles floor_wax in MA when {/^Baker/.test(lead.name)};
grammar VecMath; statList: stat+; stat: VAR '=' expression # assignStat | 'print' expression # printStat ; primary: VAR | INT | list; expression: multiExpr ( op=('+' | '-') multiExpr )*; parExpr: '(' expression ')' ; unaryExpr: '-'? factorExpr; multiExpr: unaryExpr (op=('*' | '.') unaryExpr)*; factorExpr: parExpr | primary; list: '[' expression (',' expression)* ']' ; VAR: ('a'..'z' | 'A'..'Z')+; INT: ('0'..'9')+; WS: ('\r' | '\n' | '\r' | ' ')+ {skip();};
public class ParseTreeVisitor extends VecMathBaseVisitor<Node> { @Override public Node visitStatList(VecMathParser.StatListContext ctx) { List<Node> children = ctx.stat().stream().map(c->visit(c)).collect(Collectors.toList()); RuleNode node = new RuleNode("statList"); node.setChildren(children); return node; } @Override public Node visitAssignStat(VecMathParser.AssignStatContext ctx) { RuleNode node = new RuleNode("stat"); node.setChildren(listChildren(ctx)); return node; } @Override public Node visitPrintStat(VecMathParser.PrintStatContext ctx) { RuleNode node = new RuleNode("stat"); node.setChildren(listChildren(ctx)); return node; } @Override public Node visitList(VecMathParser.ListContext ctx) { RuleNode node = new RuleNode("list"); node.setChildren(listChildren(ctx)); return node; } private List<Node> listChildren(ParserRuleContext ctx){ return ctx.children.stream().map(c->{ if(c instanceof TerminalNode) return new TokenNode(((TerminalNode) c).getSymbol()); else if(c instanceof ParserRuleContext) return visit(c); else return null; }).filter(c->c!=null).collect(Collectors.toList()); } }
public class RewriteListener extends IDataBaseListener { TokenStreamRewriter rewriter; public RewriteListener(TokenStream tokens) { rewriter = new TokenStreamRewriter(tokens); } @Override public void enterGroup(IDataParser.GroupContext ctx) { rewriter.insertAfter(ctx.stop, '\n'); } }
對於內部DSL主題,可能更多的是一些代碼技巧和語言特性的使用,做爲一名程序猿你可能自己已經對此很熟悉了,但我仍是要在此獻醜一番了。
對於通用語言,生成模式的代碼形式主要是鏈式調用(方法級聯)、方法序列、嵌套方法;這些名詞對你來講可能有些陌生,讓我換一些更通俗的說法吧,上面分別對應於建造者模式、大量的連續指令也就是咱們最經常使用的編程形式、將一個函數的返回值做爲另外一個函數的參數使用。
接下來我要提一些在使用這些形式時須要注意的問題和可能會用到的技巧。當你使用方法級聯時你可能最須要注意的就是做用範圍了。一個生成器(Builder)是最簡單的狀況,咱們不如考慮這樣一種情形,A的生成須要B、C、D,B又依賴於E和F,而後他們每個都有大量的構造參數,這種時候你但願經過使用一次鏈式調用來完成A的建立,恐怕就須要限制級聯方法的範圍了,你能夠在生成器中記錄它的父生成器,父生成器的方法能夠調用子生成器的同名方法,子生成器方法結尾再次返回父生成器。這是一種解決思路,但並不夠優美,固然你也能夠選擇方法序列和鏈式調用結合。
讓咱們再考慮一種情形,若是A依賴於兩個B對象進行構建,那鏈式調用多是這樣的ABuilder.B().E(XX).F(XX).B().E(XX).F(XX),在實現過程當中你必須可以分清楚兩次不一樣的E方法分別做用於哪一個B,因此你可能須要一個應用來表示當前正在進行的階段和生成的對象,這就是語境變量。
當你使用嵌套函數的時候,最大的麻煩在於在你調用上層方法時,其嵌套方法都已經執行完畢了,數據也準備好了,你無法作一些更靈活的擴展。解決的方法一方面你能夠傳入生成器而非方法;另一方面你可使用閉包傳入代碼塊。閉包是一種極其優秀的語言特性,也是實現修飾模式的極佳選擇。使用閉包你能夠將對象的new操做的時機掌控在本身手中,並經過執行閉包中的代碼塊來實際初始化對象。更有意思的是閉包不只僅能夠用來直接執行,你還能夠將它當作是輸入的DSL(不過這裏DSL的語法剛好是通用語言)進行解析來生成語義模型,好比當閉包中是一個bool表達式組合,你就能夠將這個表達式解析成語法樹並最終構建出一個組合模式的語義模型(詳見DSL 41章,這一部分頗有意思)。
接下來咱們所提到的可能更多的和語言特性相關,下面一一列舉作個簡要介紹:
在這裏我主要想提三個比較常見和有表明性的語義模型:
依賴網絡本質上是一個有向無環圖,包含了節點和它們之間的依賴關係,根據節點的類型咱們大體能夠分爲以任務爲核心的依賴網絡好比ANT和以輸出爲核心的依賴網絡好比Makefile,這裏面有些微妙的區別;前者可能更關心任務不會被重複執行也就是構建合理的任務執行序列,後者可能更關心當某個中間輸出結果變更時須要從新生成部分的輸出而並不是全盤從新輸出。
構建依賴網絡的主要問題是:
產生式規則系統的核心是一條一條的規則,當規則知足的時候會觸發相應的動做,因此核心在於如何快速找出候選的規則集,畢竟每次遍歷全部規則既不現實也不高效。這裏須要重視的是規則和規則之間的關係,當某個規則知足並觸發動做時可能會形成有一系列新的規則知足,這被稱爲規則的前向式交互。但咱們依然須要謹慎地防止環的出現,一種辦法是在加入規則時進行檢測,另外一種辦法則是在檢查規則的時候維護一個激活集,一個規則只能被激活一次。我認爲提早構建好規則之間的關係是個不錯的方法,這樣有助於咱們在實際判斷規則條件是否知足的時候可以快速檢索到那些被上一條規則激活的規則。
狀態機的使用十分普遍,相信在座的程序猿沒有幾個木有用過狀態機的,因此在這裏就很少言了,可是它又是如此的重要以致於我必須把它做爲一個醒目的標題列出來,忽視它是不可饒恕的。
在這一節裏咱們主要探討代碼翻譯和代碼生成的技術,根據是否須要顯式藉助語義模型、是否須要藉助於外部信息等能夠把它們分爲三大類:
上面講的都是代碼生成的思路,對於如何操縱輸出內容所提甚少,在實際開發中,輸出模板和模板引擎每每是必不可少的。能夠爲每個語義對象建立輸出模板,而後將實際的語義對象傳入模板引擎,在模板中填入動態變化的數據。這種方式對於AJAX時代以前的WEB程序猿簡直是得心應手,大量的模板被用來生成HTML中的動態內容,本質上並沒有任何不一樣。只不過當咱們藉助於語義模型而且有大量的嵌套操做時,咱們可能得作些模板的嵌套和拼接了。
當不借助於語義模型時咱們也能夠在樹的構建中使用模板以生成輸出,不過在ANTLR4中你可能得手動編寫Listener來調用模板了。說到這裏我就不得不吐槽一下ANTLR4了,雖然我認可將文法和各類邏輯操做解耦是一個正確的方向,可是忽然感受一切沒有那麼靈活了,儘管去掉的這些功能幾乎均可以很快地經過Listener和Visitor實現,依然有一種蛋疼的感受。最後放一個StringTemplate的模板定義文件吧。
group Cymbol; // START: file file(defs) ::= << <defs; separator="\n"> >> // END: file // START: class class(name, sup, members) ::= << class <name> <if(sup)>: <sup><endif> { <members> }; >> // END: class // START: method method(name, retType, args, block) ::= << <retType> <name>(<args; separator=", ">) <block> >> // END: method // START: block block(stats) ::= << { <stats; separator="\n"> } >> // END: block // START: if if(cond, stat1, stat2) ::= << if ( <cond> ) <stat1> <if(stat2)>else <stat2><endif> >> // END: if // START: assign assign(a,b) ::= "<a> = <b>;" // END: assign return(v) ::= "return <v>;" // START: callstat callstat(name, args) ::= "<call(...)>;" // call() inherits name,args // END: callstat // START: decl decl(name, type, init, ptr) ::= "<type> <if(ptr)>*<endif><name><if(init)> = <init><endif>" var(name, type, init, ptr) ::= "<decl(...)>;" arg(name, type, init, ptr) ::= "<decl(...)>" // END: decl // START: ops operation(op, x, y) ::= "(<x> <op> <y>)" // END: ops // START: operator operator(o) ::= "<o>" // END: operator unary_minus(v) ::= "-(<v>)" unary_not(v) ::= "!(<v>)" addr(v) ::= "&(<v>)" deref(v) ::= "*(<v>)" index(array, i) ::= "<array>[<i>]" member(obj, name) ::= "(<obj>).<name>" // START: call call(name, args) ::= << <name>(<args; separator=", ">) >> // END: call
這篇文章的主要目的是爲了整理和歸納一下DSL的主要相關知識,可能內容也有些雜亂,有些地方也沒有說的很清楚,或者是說到一半就戛然而止了,但願你們多多包涵,也但願你們有什麼想法能夠和我討論。