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的主要相关知识,可能内容也有些杂乱,有些地方也没有说的很清楚,或者是说到一半就戛然而止了,但愿你们多多包涵,也但愿你们有什么想法能够和我讨论。