├── .gitignore ├── GLOSSARY.md ├── README.md ├── SUMMARY.md ├── arithmetic-expression.md ├── basic-concept.md ├── calculator-listener.md ├── calculator-visitor.md ├── common-language-patterns.md ├── common-lexical-structures.md ├── different-channels.md ├── embedding-actions.md ├── getting-started.md ├── grammar-design.md ├── images ├── basic-data-flow.png └── parse-tree-hello.png ├── installing-antlr.md ├── introduction.md ├── line-between-lexer-and-parser.md ├── multiple-formats.md ├── precedence-left-recursion-associativity.md ├── rewriting-input-stream.md └── semantic-predicate.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Node rules: 2 | ## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 3 | .grunt 4 | 5 | ## Dependency directory 6 | ## Commenting this out is preferred by some people, see 7 | ## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git 8 | node_modules 9 | 10 | # Book build output 11 | _book 12 | 13 | # eBook build output 14 | *.epub 15 | *.mobi 16 | *.pdf 17 | -------------------------------------------------------------------------------- /GLOSSARY.md: -------------------------------------------------------------------------------- 1 | ## grammar 2 | 3 | 文法,一种形式化(formal)的语言描述。 4 | 5 | ## 文法 6 | 7 | grammar 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ANTLR 4简明教程 2 | 3 | 《The Definitive ANTLR 4 Reference》中译版书籍已出(和我无关),所以此项目已被放弃…… 4 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | * [自述](README.md) 2 | * [简介](introduction.md) 3 | * [安装ANTLR](installing-antlr.md) 4 | * [入门](getting-started.md) 5 | * [基本概念](basic-concept.md) 6 | * [在语法中嵌入任意的操作](embedding-actions.md) 7 | * [使用语义谓词改变语法分析](semantic-predicate.md) 8 | * [处理同一文件中的不同格式](multiple-formats.md) 9 | * [重写输入流](rewriting-input-stream.md) 10 | * [发送记号到不同的通道](different-channels.md) 11 | * [算术表达式语言](arithmetic-expression.md) 12 | * [使用Visitor模式计算结果](calculator-visitor.md) 13 | * [使用Listener模式计算结果](calculator-listener.md) 14 | * [语法设计](grammar-design.md) 15 | * [常用语言模式](common-language-patterns.md) 16 | * [优先级,左递归以及相关性](precedence-left-recursion-associativity.md) 17 | * [常用词法结构](common-lexical-structures.md) 18 | * [词法分析器和语法分析器的界线](line-between-lexer-and-parser.md) 19 | -------------------------------------------------------------------------------- /arithmetic-expression.md: -------------------------------------------------------------------------------- 1 | # 算术表达式语言 2 | 3 | 了解ANTLR最好的方法就是实例。构建一个简单的计算器是个不错的主意。为了使它容易理解且保持简单,我们将只允许基本的算术运算符(加、减、乘、除)、括号表达式、整数和变量。 4 | 5 | ``` 6 | grammar Calc; 7 | 8 | prog 9 | : stat+ 10 | ; 11 | 12 | stat 13 | : expr 14 | | ID '=' expr 15 | ; 16 | 17 | expr 18 | : expr ('*'|'/') expr 19 | | expr ('+'|'-') expr 20 | | INT 21 | | ID 22 | | '(' expr ')' 23 | ; 24 | 25 | ID : [a-zA-Z]+ ; 26 | 27 | INT : [0-9]+ ; 28 | 29 | WS : [ \t\r\n]+ -> skip ; // toss out whitespace 30 | ``` 31 | 32 | 在上述的语法中,程序是由空格(换行符也被当作空格)终止的语句序列,语句可以是表达式或者赋值。那些以小写字母开头的像stat和expr是语法规则;由大写字母开头的诸如ID和INT为词法规则,用于识别标志符和整数这样的记号。我们用“|”分隔规则的选项,我们也可以用“()”把符号分组成子规则。例如,子规则`('*'|'/')`匹配乘法符号或者除法符号。 33 | 34 | ANTLR v4最重要的新特性是它有能力处理(大多数类型的)左递归规则。例如,规则expr前两个选项就在左边缘递归地调用了expr自身。这种指定算术表达式表示法的方法比那些典型的自顶向下语法分析器策略更容易。当然,在这种策略下,我们需要定义多个规则,每个运算符优先级一个规则。 35 | 36 | 记号定义的表示法对那些有正则表达式经验的应该很熟悉。唯一不寻常的是在WS规则上的`-> skip`指令,它告诉词法分析器去匹配但丢弃空格,不要把它们放到记号流中,这样在语法分析树上空格就不会有对应的记号。(每个可能的输入字符都必须被至少一个词法规则匹配。)我们通过使用形式化的ANTLR表示法避免捆绑语法到某个特定的目标语言,而不是在语法中插入任意代码片段来告诉词法分析器去忽略。 37 | 38 | 这里是一些用来评估所有语法特性的测试序列: 39 | 40 | ``` 41 | 193 42 | a=5 43 | b=6 44 | a+b*2 45 | (1+2)*3 46 | ``` 47 | 48 | 把它们放入文件calc.txt中,然后执行以下命令: 49 | 50 | ``` 51 | antlr Calc.g 52 | compile *.java 53 | grun Calc prog -gui calc.txt 54 | ``` 55 | 56 | TestRig会弹出一个显示语法分析树的窗口: 57 | 58 | ![](http://codemany.com/uploads/calc-parse-tree.png) 59 | -------------------------------------------------------------------------------- /basic-concept.md: -------------------------------------------------------------------------------- 1 | # 基本概念 2 | 3 | 一门语言由有效的句子组成,一个句子由短语组成,一个短语由子短语和词汇符号组成。要实现一门语言,我们必须构建一个应用,它能读取句子以及对发现的短语和输入符号作出适当的反应。 4 | 5 | 这样的应用必须能识别特定语言的所有有效的句子、短语和子短语。识别一个短语意味着我们能确定短语的各种组件并能指出它与其它短语的区别。例如,我们把输入`nu = 123;`识别为赋值语句,这意味着我们知道nu是赋值目标以及123是要存储的值。识别赋值语句`nu = 123;`也意味着应用认为它是明显不同于,比如说,`a + b`语句的。 6 | 7 | 识别语言的程序被称为语法分析器。语法指代控制语言成员的规则,每条规则都表示一个短语的结构,文法就是一组规则。为了更容易地实现识别语言的程序,通常我们会把语法分析过程拆解成两个相似但不同的任务或阶段。 8 | 9 | 把字符组成单词或符号(记号)的过程被称为词法分析或简单标记化。我们把标记输入的程序称为词法分析器。词法分析器能把相关的记号组成记号类型,例如INT(整数)、ID(标志符)、FLOAT(浮点数)等。当语法分析器只关心类型的时候,词法分析器会把词汇符号组成类型,而不是单独的符号。记号至少包含两部分信息:记号类型(确定词法结构)和匹配记号的文本。 10 | 11 | 第二阶段是真正的语法分析器,它使用这些记号去识别句子结构,在这里是赋值语句。默认情况下,ANTLR生成的语法分析器会构建一个称为语法分析树或语法树的数据结构,它记录语法分析器如何识别输入句子的结构以及它的组件短语。下图说明了语言识别器的基本的数据流: 12 | 13 | ![](images/basic-data-flow.png) 14 | 15 | 语法分析树的内部节点是分组和确认它们子节点的短语名字。根节点是最抽象的短语名字,在本例中是prog(“program”的缩写)。语法分析树的叶子节点永远是输入记号。 16 | 17 | 通过生成语法分析树,语法分析器给应用的其余部分提供了方便的数据结构,它们含有关于语法分析器如何把符号组成短语的完整信息。树是非常容易处理的,并且也能被程序员很好的理解。更好的是,语法分析器能自动地生成语法分析树。 18 | 19 | 通过操作语法分析树,需要识别相同语言的多个应用能重用同一个语法分析器。当然,你也可以选择直接在语法中嵌入特定应用的代码片段,这是语法分析器生成器传统的做法。ANTLR v4仍然允许这样做,但是语法分析树有助于更简洁更解耦的设计。 20 | 21 | 语法分析树对于需要多次树遍历的转换也是非常有用的,因为在计算依赖关系的阶段通常会需要前一个阶段的信息。相比于在每个阶段都要准备输入字符,我们只需要遍历语法分析树多次,更具有效率。 22 | 23 | 因为我们用一套规则指定短语,语法分析树子树根节点对应于语法规则名。这里的语法规则对应于上图中assign子树的第一层: 24 | 25 | ``` 26 | assign : ID '=' expr ; // 匹配赋值语句像"a=5" 27 | ``` 28 | 29 | 明白ANTLR如何把这些规则转换为人类可读的语法分析代码是使用和调试语法的基础,因此让我们深入地挖掘语法分析是如何工作的。 30 | 31 | ## 实现语法分析器 32 | 33 | ANTLR工具根据语法规则,例如我们刚才看到的assign,生成递归下降语法分析器。递归下降语法分析器只是递归方法的一个集合,每个规则一个方法。下降这个术语指的是分析从语法分析树的根开始向着叶子进行(记号)。我们首先调用的规则,即prog符号,成为语法分析树的根。那也就意味着对前面部分的语法分析树来说需要调用方法prog()。这类分析更通用的术语是自顶向下分析:递归下降语法分析器仅仅是自顶向下语法分析器实现的一种。 34 | 35 | 要了解递归下降语法分析器是什么样子,这里是ANTLR为规则assign生成的方法(稍微整理): 36 | 37 | ``` 38 | // assign : ID '=' expr ; 39 | void assign() { // 根据规则assign生成的方法 40 | match(ID); // 比较ID和当前输入符号然后消费 41 | match('='); 42 | expr(); // 通过调用expr()匹配表达式 43 | } 44 | ``` 45 | 46 | 递归下降语法分析器最酷的部分是通过调用方法prog()、assign()和expr()跟踪出的调用关系图反映了内部的语法分析树节点。match()的调用对应语法分析树叶子。为了在一个手工构建的语法分析器中手动构建一颗语法分析树,我们需要在每个规则方法的开始处插入“添加新子树根”操作,以及给match()一个“添加新叶子节点”操作。 47 | 48 | 方法assign()只是检查确保所有必要的记号存在且以正确的顺序。当语法分析器进入assign()时,它不必在多个选项之间进行选择。选项是规则定义右边的选择之一。例如,调用assign的prog规则可能有其它类型的语句。 49 | 50 | ``` 51 | /** 匹配起始于当前输入位置的任何语句 */ 52 | prog 53 | : assign // 第一个选项('|'是选项分隔符) 54 | | ifstat // 第二个选项 55 | | whilestat 56 | ... 57 | ; 58 | ``` 59 | 60 | prog的分析规则看起来像一条switch语句: 61 | 62 | ``` 63 | void prog() { 64 | switch ( «current input token» ) { 65 | CASE ID : assign(); break; 66 | CASE IF : ifstat(); break; // IF是关键字'if'的记号类型 67 | CASE WHILE : whilestat(); break; 68 | ... 69 | default : «raise no viable alternative exception» 70 | } 71 | } 72 | ``` 73 | 74 | 方法prog()必须通过检查下一个输入记号作出分析决定或预测。分析决定预判哪一个选项将会成功。在本例中,当看到WHILE关键字时会预判是规则prog的第三个选项。规则方法prog()然后就会调用whilestat()。你以前可能听说过术语预读记号,那只是下一个输入记号。预读记号可以是语法分析器在匹配和消费它之前嗅探的任何记号。 75 | 76 | 有时候,语法分析器需要一些预读记号去预判哪个选项会成功。它甚至必须考虑从当前位置直到文件结尾的所有的记号!ANTLR默默地为你处理所有的这些事情,但是对决策过程有个基本的了解是有帮助的,可以让调试生成的语法分析器更容易。 77 | 78 | 为更好地理解分析决定,想象有个单一入口和单一出口的迷宫,有单词写在地板上。每个沿着从入口到出口路径的单词序列表示一个句子。迷宫的结构与定义一门语言的语法规则类似。为测试一个句子在一门语言中的成员身份,我们在穿越迷宫时把句子的单词和沿着地板的单词作比较。如果通过句子的单词我们能到达出口,那么句子是有效的。 79 | 80 | 为了通过迷宫,我们必须在每个岔口选择一条有效路径,正如我们必须在语法分析器中选择选项。我们必须决定该走哪条路,通过把我们句子中下一个单词(们)和沿着来自每个岔口的每条路径上可见的单词比较。我们能从岔口看到的单词与预读记号类似。当每条路径以唯一的单词开始时决定是相当容易的。在规则prog中,每个选项从唯一的记号开始,因此prog()可以通过查看第一个预读记号识别选项。 81 | 82 | 当单词从一个岔口重叠部分开始每条路径时,语法分析器需要继续往前看,扫描可以识别选项的单词。ANTLR根据需要为每个决定自动上下调节预读数量。如果预读的结果是多条同样的到出口的路径,即当前的输入短语有多种解释。解决这样的二义性将是我们的下一个主题。 83 | 84 | ## 二义性 85 | 86 | 一个模棱两可的短语或句子通常是指它有不止一种解释。换句话说,短语或句子能适配不止一种语法结构。要解释或转换一个短语,程序必须要能唯一地确认它的含义,这意味着我们必须提供无歧义的语法,以便生成的语法分析器能用明确的一个方法匹配每个输入短语。 87 | 88 | 在这里,让我们展示一些有歧义的语法以便让二义性的概念更具体。如果你以后在构建语法时陷入二义性,你可以参考本节的内容。 89 | 90 | 一些明显有歧义的语法: 91 | 92 | ``` 93 | assign 94 | : ID '=' expr // 匹配一个赋值语句,例如f() 95 | | ID '=' expr // 前面选项的精确复制 96 | ; 97 | 98 | expr 99 | : INT ; 100 | ``` 101 | 102 | 大多数时候二义性是不明显的,如同以下的语法,它能通过规则stat的两个选项匹配函数调用: 103 | 104 | ``` 105 | stat 106 | : expr // 表达式语句 107 | | ID '(' ')' // 函数调用语句 108 | ; 109 | 110 | expr 111 | : ID '(' ')' 112 | | INT 113 | ; 114 | ``` 115 | 116 | 这里是两个输入f()的解释,从规则stat开始: 117 | 118 | ![](http://codemany.com/uploads/fn-parse-tree.png) 119 | 120 | 左边的语法分析树显示f()匹配规则expr。右边的语法分析树显示f()匹配规则stat的第二个选项。 121 | 122 | 因为大部分语言它们的语法都被设计成无歧义的,有歧义的语法类似于编程缺陷。我们需要识别语法以便为每个输入短语提交单一选择给语法分析器。如果语法分析器发现一个有歧义的短语,它必须选一个可行的选项。ANTLR通过选择涉及决定的第一个选项解决二义性。在本例中,语法分析器将选择与左边的语法分析树有关的f()的解释。 123 | 124 | 二义性可以发生在词法分析器中也能发生在语法分析器中,但ANTLR可以自动地解决它们。ANTLR通过使输入字符串和语法中第一个指定的规则匹配来解决词法二义性。为了明白这是如何工作的,让我们看看对大部分编程语言都很普遍的二义性:在关键字和标志符规则中的二义性。关键字begin(后面有个非字母)也是标志符,至少词法上,因此词法分析器可以匹配b-e-g-i-n到两者中的任何一个规则。 125 | 126 | ``` 127 | BEGIN : 'begin' ; // 匹配b-e-g-i-n序列,即把二义性解析为BEGIN 128 | ID : [a-z]+ ; // 匹配一个或多个任意小写字母 129 | ``` 130 | 131 | 注意,词法分析器会试着为每个记号尽可能匹配最长的字符串,这意味着输入beginner将仅匹配规则ID。词法分析器不会把beginner匹配成BEGIN随后ID匹配输入ner。 132 | 133 | 有时候语言的语法就明显有歧义,没有任何的语法重组能改变这个事实。例如,算术表达式的自然语法可以用两种方式解释输入像1+2*3这样,要么执行运算符从左到右,要么像大部分语言那样按优先级顺序。 134 | 135 | C语言展示了另一种二义性,但我们可以使用上下文信息比如标志符如何被定义来解决它。考虑代码片段i*j;。在语法上,它看起来像是一个表达式,但它的含义或者语义依赖i是类型名还是变量。如果i是类型名,那么这个片段不是表达式,而是一个声明为指向类型i的指针变量j。 136 | 137 | ## 语法分析树 138 | 139 | 为制作语言应用,我们必须为每个输入短语或子短语执行一些适当的代码,那样做最简单的方法是操作由语法分析器自动创建的语法分析树。 140 | 141 | 早些时候我们已经学习了词法分析器处理字符和把记号传递给语法分析器,然后语法分析器分析语法和创建语法分析树的相关知识。对应的ANTLR类分别是CharStream、Lexer、Token、Parser和ParseTree。连接词法分析器和语法分析器的管道被称为TokenStream。下图说明了这些类型的对象如何连接到内存中其它的对象。 142 | 143 | ![](http://codemany.com/uploads/basic-data-structure.png) 144 | 145 | 这些ANTLR数据结构分享尽可能多的数据以便节省内存的需要。上图显示在语法分析树中的叶子(记号)节点含有在记号流中记号的点。记号记录开始和结束字符在CharStream中的索引,而不是复制子串。这里没有与空格字符有关的记号,因为我们假设我们的词法分析器扔掉了空格。 146 | 147 | 下图显示的是ParseTree的子类RuleNode和TerminalNode以及它们所对应的子树根节点和叶子节点。RuleNode包含有方法如getChild()和getParent()等,但RuleNode并不专属于特定语法所有。为支持更好地访问在特定节点中的元素,ANTLR为每个规则生成一个RuleNode子类。下图为我们显示了赋值语句例子的子树根节点的特定类,它们是ProgContext,AssignContext和IntContext: 148 | 149 | ![](http://codemany.com/uploads/parse-tree-node.png) 150 | 151 | 因为它们记录了我们知道的通过规则对短语识别的每件事,所以这些被称为上下文对象。每个上下文对象知道被识别短语的开始和结束记号以及提供对所有短语的元素的访问。例如,AssignContext提供方法ID()和INT()去访问标志符节点和表达式子树。 152 | 153 | 给出了具体类型的描述,我们可以手工写代码去执行树的深度优先遍历。当我们发现和完成节点时我们可以执行任何我们想要的动作。典型的操作是诸如计算结果,更新数据结构,或者生成输出。相比每次为每个应用写同样的树遍历样板代码,我们可以使用ANTLR自动生成的树遍历机制。 154 | 155 | ## 监听器和访问者 156 | 157 | ANTLR在它的运行库中为两种树遍历机制提供支持。默认情况下,ANTLR生成一个语法分析树Listener接口,在其中定义了回调方法,用于响应被内建的树遍历器触发的事件。 158 | 159 | 在Listener和Visitor机制之间最大的不同是:Listener方法被ANTLR提供的遍历器对象调用;而Visitor方法必须显式的调用visit方法遍历它们的子节点,在一个节点的子节点上如果忘记调用visit方法就意味着那些子树没有得到访问。 160 | 161 | 让我们首先从Listener开始。在我们了解Listener之后,我们也将看到ANTLR如何生成遵循Visitor设计模式的树遍历器。 162 | 163 | ### 语法分析树Listener 164 | 165 | 在Calc.java中有这样两行代码: 166 | 167 | ``` 168 | ParseTreeWalker walker = new ParseTreeWalker(); 169 | walker.walk(new DirectiveListener(), tree); 170 | ``` 171 | 172 | 类ParseTreeWalker是ANTLR运行时提供的用于遍历语法分析树和触发Listener中回调方法的树遍历器。ANTLR工具根据Calc.g中的语法自动生成ParseTreeListener接口的子接口CalcListener和默认实现CalcBaseListener,其中含有针对语法中每个规则的enter和exit方法。DirectiveListener是我们编写的继承自CalcBaseListener的包含特定应用代码的实现,把它传递给树遍历器后,树遍历器在遍历语法分析树时就会触发DirectiveListener中的回调方法。 173 | 174 | ![](http://codemany.com/uploads/calc-listener-hierachy.png) 175 | 176 | 下图左边的语法分析树显示ParseTreeWalker执行了一次深度优先遍历,由粗虚线表示,箭头方向代表遍历方向。右边显示的是语法分析树的完整调用序列,它们由ParseTreeWalker触发调用。当树遍历器遇到规则assign的节点时,它触发enterAssign()并且给它传递AssignContext语法分析树节点。在树遍历器访问完assign节点的所有子节点后,它触发exitAssign()。 177 | 178 | ![](http://codemany.com/uploads/listener-call-sequence.png) 179 | 180 | Listener机制的强大之处在于所有都是自动的。我们不必要写语法分析树遍历器,而且我们的Listener方法也不必要显式地访问它们的子节点。 181 | 182 | ### 语法分析树Visitor 183 | 184 | 有些情况下,我们实际想要控制的是遍历本身,在那里我们可以显式地调用visit方法去访问子树节点。选项-visitor告诉ANTLR工具从相应语法生成Visitor接口和默认实现,其中含有针对语法中每个规则的visit方法。 185 | 186 | 下图是我们熟悉的Visitor模式操作在语法分析树上。左边部分的粗虚线表示语法分析树的深度优先遍历,箭头方向代表遍历方向。右边部分指明Visitor中的方法调用序列。 187 | 188 | ![](http://codemany.com/uploads/visitor-call-sequence.png) 189 | 190 | 下面是Calc.java中的两行代码: 191 | 192 | ``` 193 | EvalVisitor eval = new EvalVisitor(); 194 | // To start walking the parse tree 195 | eval.visit(tree); 196 | ``` 197 | 198 | 我们首先初始化自制的树遍历器EvalVisitor,然后调用visit()去访问整棵语法分析树。ANTLR运行时提供的Visitor支持代码会在看到根节点时调用visitProg()。在那里,visitProg()会把子树作为参数调用visit方法继续遍历,如此等等。 199 | 200 | ![](http://codemany.com/uploads/calc-visitor-hierachy.png) 201 | 202 | ANTLR自动生成的Visitor接口和默认实现可以让我们为Visitor方法编写自己的实现,让我们避免必须覆写接口中的每个方法,让我们仅仅聚焦在我们感兴趣的方法上。这种方法减少了我们学习ANTLR必须要花费的时间,让我们回到我们所熟悉的编程语言领域。 203 | -------------------------------------------------------------------------------- /calculator-listener.md: -------------------------------------------------------------------------------- 1 | # 使用Listener模式计算结果 2 | 3 | 在上一节中的计算器是以解释的方式执行的,现在我们想要把它转换成以编译的方式执行。编译执行和解释执行相比,需要依赖于特定的目标机器。在这里我们假设有一台这样的机器,它用堆栈进行运算,支持如下表所示的几种指令: 4 | 5 | 指令 | 说明 | 运算元数目 | 用途 6 | ---- | ------------- | ---------- | ---------------------------- 7 | LDV | Load Variable | 1 | 变量入栈 8 | LDC | Load Constant | 1 | 常量入栈 9 | STR | Store Value | 1 | 栈顶一个元素存入指定变量 10 | ADD | Add | 0 | 栈顶两个元素出栈,求和后入栈 11 | SUB | Subtract | 0 | 栈顶两个元素出栈,求差后入栈 12 | MUL | Multiply | 0 | 栈顶两个元素出栈,求积后入栈 13 | DIV | Divide | 0 | 栈顶两个元素出栈,求商后入栈 14 | RET | Return | 0 | 栈顶一个元素出栈,计算结束 15 | 16 | 做这个最简单的方法是使用ANTLR的语法分析树Listener机制实现DirectiveListener类,然后它通过监听来自树遍历器触发的事件,输出对应的机器指令。 17 | 18 | Listener机制的优势是我们不必要自己去做任何树遍历,甚至我们不必要知道遍历语法分析树的运行时如何调用我们的方法,我们只要知道我们的DirectiveListener类得到通知,在与语法规则匹配的短语开始和结束时。这种方法减少了我们学习ANTLR必须要花费的时间,让我们回到我们所熟悉的编程语言领域。 19 | 20 | 这里不需要创建新的语法规则,还是继续沿用前文Calc.g所包含的语法,标签也要保留: 21 | 22 | ``` 23 | grammar Calc; 24 | 25 | prog 26 | : stat+ 27 | ; 28 | 29 | stat 30 | : expr # printExpr 31 | | ID '=' expr # assign 32 | ; 33 | 34 | expr 35 | : expr op=(MUL|DIV) expr # MulDiv 36 | | expr op=(ADD|SUB) expr # AddSub 37 | | INT # int 38 | | ID # id 39 | | '(' expr ')' # parens 40 | ; 41 | 42 | MUL : '*' ; 43 | 44 | DIV : '/' ; 45 | 46 | ADD : '+' ; 47 | 48 | SUB : '-' ; 49 | 50 | ID : [a-zA-Z]+ ; 51 | 52 | INT : [0-9]+ ; 53 | 54 | WS : [ \t\r\n]+ -> skip ; // toss out whitespace 55 | ``` 56 | 57 | 然后,我们可以运行ANTLR工具: 58 | 59 | ``` 60 | antlr Calc.g 61 | ``` 62 | 63 | 它会生成后缀名为tokens和java的六个文件: 64 | 65 | ``` 66 | Calc.tokens CaclLexer.java CalcParser.java 67 | CalcLexer.tokens CalcBaseListener.java CalcListener.java 68 | ``` 69 | 70 | 正如这里我们看到的,ANTLR会为我们自动生成Listener基础设施。其中CalcListener是语法和Listener对象之间的关键接口,描述我们可以实现的回调方法: 71 | 72 | ``` 73 | public interface CalcListener extends ParseTreeListener { 74 | void enterProg(CalcParser.ProgContext ctx); 75 | void exitProg(CalcParser.ProgContext ctx); 76 | void enterPrintExpr(CalcParser.PrintExprContext ctx); 77 | ... 78 | } 79 | ``` 80 | 81 | CalcBaseListener则是ANTLR生成的一组空的默认实现。ANTLR内建的树遍历器会去触发在Listener中像enterProg()和exitProg()这样的一串回调方法,如同它对语法分析树执行了一次深度优先遍历。为响应树遍历器触发的事件,我们的DirectiveListener需要继承CalcBaseListener并实现一些方法。我们不需要实现全部的接口方法,我们也不需要去覆写每个enter和exit方法,我们只需要去覆写那些我们感兴趣的回调方法。 82 | 83 | 在本例中,我们需要通过覆写6个方法对6个事件——当树遍历器exit那些有标签的选项时触发——作出响应。我们的基本策略是当这些事件发生时打印出已转换的指令。以下是完整的实现代码: 84 | 85 | ``` 86 | public class DirectiveListener extends CalcBaseListener { 87 | 88 | @Override 89 | public void exitPrintExpr(CalcParser.PrintExprContext ctx) { 90 | System.out.println("RET\n"); 91 | } 92 | 93 | @Override 94 | public void exitAssign(CalcParser.AssignContext ctx) { 95 | String id = ctx.ID().getText(); 96 | System.out.println("STR " + id); 97 | } 98 | 99 | @Override 100 | public void exitMulDiv(CalcParser.MulDivContext ctx) { 101 | if (ctx.op.getType() == CalcParser.MUL) { 102 | System.out.println("MUL"); 103 | } else { 104 | System.out.println("DIV"); 105 | } 106 | } 107 | 108 | @Override 109 | public void exitAddSub(CalcParser.AddSubContext ctx) { 110 | if (ctx.op.getType() == CalcParser.ADD) { 111 | System.out.println("ADD"); 112 | } else { 113 | System.out.println("SUB"); 114 | } 115 | } 116 | 117 | @Override 118 | public void exitId(CalcParser.IdContext ctx) { 119 | System.out.println("LDV " + ctx.ID().getText()); 120 | } 121 | 122 | @Override 123 | public void exitInt(CalcParser.IntContext ctx) { 124 | System.out.println("LDC " + ctx.INT().getText()); 125 | } 126 | } 127 | ``` 128 | 129 | 为了让它运行起来,余下我们唯一需要做的事是创建一个主程序去调用它: 130 | 131 | ``` 132 | public class Calc { 133 | 134 | public static void main(String[] args) throws Exception { 135 | InputStream is = args.length > 0 ? new FileInputStream(args[0]) : System.in; 136 | 137 | ANTLRInputStream input = new ANTLRInputStream(is); 138 | CalcLexer lexer = new CalcLexer(input); 139 | CommonTokenStream tokens = new CommonTokenStream(lexer); 140 | CalcParser parser = new CalcParser(tokens); 141 | ParseTree tree = parser.prog(); 142 | 143 | ParseTreeWalker walker = new ParseTreeWalker(); 144 | walker.walk(new DirectiveListener(), tree); 145 | 146 | // print LISP-style tree 147 | System.out.println(tree.toStringTree(parser)); 148 | } 149 | } 150 | ``` 151 | 152 | 这个程序和前文Calc.java中的代码极度相似,区别只在12-13行。这两行代码负责创建树遍历器,然后让树遍历器去遍历那颗从语法分析器返回的语法分析树,当树遍历器遍历时,它就会触发调用到我们的DirectiveListener中实现的方法。此外,通过传入一个不同的Listener实现我们能简单地生成完全不同的输出。Listener机制有效地隔离了语法和语言应用,使语法可以被其它应用再次使用。 153 | 154 | 现在一切完备,让我们尝试着去编译和运行它吧!下面是完整的命令序列: 155 | 156 | ``` 157 | compile *.java 158 | run Calc calc.txt 159 | ``` 160 | 161 | 编译的输出结果如下所示: 162 | 163 | ``` 164 | LDC 19 165 | RET 166 | 167 | LDC 5 168 | STR a 169 | LDC 6 170 | STR b 171 | LDV a 172 | LDV b 173 | LDC 2 174 | MUL 175 | ADD 176 | RET 177 | 178 | LDC 1 179 | LDC 2 180 | ADD 181 | LDC 3 182 | MUL 183 | RET 184 | ``` 185 | -------------------------------------------------------------------------------- /calculator-visitor.md: -------------------------------------------------------------------------------- 1 | # 使用Visitor模式计算结果 2 | 3 | 为了给前面的算术表达式语法分析器计算出结果,我们还需要做些其它的事情。 4 | 5 | ANTLR v4鼓励我们保持语法的整洁,使用语法分析树Visitor和其它遍历器来实现语言应用。不过在接触这些之前,我们需要对语法做些修改。 6 | 7 | 首先,我们需要用标签标明规则的选项,标签可以是和规则名没有冲突的任意标志符。如果选项上没有标签,ANTLR只会为每个规则生成一个visit方法。 8 | 9 | 在本例中,我们希望为每个选项生成一个不同的visit方法,以便每种输入短语都能得到不同的事件。在新的语法中,标签出现在选项的右边缘,且以“#”符号开头: 10 | 11 | ``` 12 | stat 13 | : expr # printExpr 14 | | ID '=' expr # assign 15 | ; 16 | 17 | expr 18 | : expr op=(MUL|DIV) expr # MulDiv 19 | | expr op=(ADD|SUB) expr # AddSub 20 | | INT # int 21 | | ID # id 22 | | '(' expr ')' # parens 23 | ; 24 | ``` 25 | 26 | 接下来,让我们为运算符字面量定义一些记号名字,以便以后可以在visit方法中引用作为Java常量的它们: 27 | 28 | ``` 29 | MUL : '*' ; 30 | 31 | DIV : '/' ; 32 | 33 | ADD : '+' ; 34 | 35 | SUB : '-' ; 36 | ``` 37 | 38 | 现在,我们有了一个增强型的语法。接下来要做的事情是实现一个EvalVisitor类,它通过遍历表达式语法分析树计算和返回值。 39 | 40 | 执行下面的命令,让ANTLR生成Visitor接口和它的默认实现,其中-no-listener参数是告诉ANTLR不再生成Listener相关的代码: 41 | 42 | ``` 43 | antlr -no-listener -visitor Calc.g 44 | ``` 45 | 46 | 所有被标签标明的选项在生成的Visitor接口中都定义了一个visit方法: 47 | 48 | ``` 49 | public interface CalcVisitor extends ParseTreeVisitor { 50 | T visitProg(CalcParser.ProgContext ctx); 51 | T visitPrintExpr(CalcParser.PrintExprContext ctx); 52 | T visitAssign(CalcParser.AssignContext ctx); 53 | ... 54 | } 55 | ``` 56 | 57 | 接口定义使用的是Java泛型,visit方法的返回值为参数化类型,这允许我们根据表达式计算返回值的类型去设定实现的泛型参数。因为表达式的计算结果是整型,所以我们的EvalVisitor应该继承`CalcBaseVisitor`类。为计算语法分析树的每个节点,我们需要覆写与语句和表达式选项相关的方法。这里是全部的代码: 58 | 59 | ``` 60 | public class EvalVisitor extends CalcBaseVisitor { 61 | /** "memory" for our calculator; variable/value pairs go here */ 62 | Map memory = new HashMap(); 63 | 64 | /** ID '=' expr */ 65 | @Override 66 | public Integer visitAssign(CalcParser.AssignContext ctx) { 67 | String id = ctx.ID().getText(); // id is left-hand side of '=' 68 | int value = visit(ctx.expr()); // compute value of expression on right 69 | memory.put(id, value); // store it in our memory 70 | return value; 71 | } 72 | 73 | /** expr */ 74 | @Override 75 | public Integer visitPrintExpr(CalcParser.PrintExprContext ctx) { 76 | Integer value = visit(ctx.expr()); // evaluate the expr child 77 | System.out.println(value); // print the result 78 | return 0; // return dummy value 79 | } 80 | 81 | /** INT */ 82 | @Override 83 | public Integer visitInt(CalcParser.IntContext ctx) { 84 | return Integer.valueOf(ctx.INT().getText()); 85 | } 86 | 87 | /** ID */ 88 | @Override 89 | public Integer visitId(CalcParser.IdContext ctx) { 90 | String id = ctx.ID().getText(); 91 | if ( memory.containsKey(id) ) return memory.get(id); 92 | return 0; 93 | } 94 | 95 | /** expr op=('*'|'/') expr */ 96 | @Override 97 | public Integer visitMulDiv(CalcParser.MulDivContext ctx) { 98 | int left = visit(ctx.expr(0)); // get value of left subexpression 99 | int right = visit(ctx.expr(1)); // get value of right subexpression 100 | if ( ctx.op.getType() == CalcParser.MUL ) return left * right; 101 | return left / right; // must be DIV 102 | } 103 | 104 | /** expr op=('+'|'-') expr */ 105 | @Override 106 | public Integer visitAddSub(CalcParser.AddSubContext ctx) { 107 | int left = visit(ctx.expr(0)); // get value of left subexpression 108 | int right = visit(ctx.expr(1)); // get value of right subexpression 109 | if ( ctx.op.getType() == CalcParser.ADD ) return left + right; 110 | return left - right; // must be SUB 111 | } 112 | 113 | /** '(' expr ')' */ 114 | @Override 115 | public Integer visitParens(CalcParser.ParensContext ctx) { 116 | return visit(ctx.expr()); // return child expr's value 117 | } 118 | } 119 | ``` 120 | 121 | 以前开发和测试语法都是使用的TestRig,这次我们试着编写计算器的主程序来启动代码: 122 | 123 | ``` 124 | public class Calc { 125 | 126 | public static void main(String[] args) throws Exception { 127 | InputStream is = args.length > 0 ? new FileInputStream(args[0]) : System.in; 128 | 129 | ANTLRInputStream input = new ANTLRInputStream(is); 130 | CalcLexer lexer = new CalcLexer(input); 131 | CommonTokenStream tokens = new CommonTokenStream(lexer); 132 | CalcParser parser = new CalcParser(tokens); 133 | ParseTree tree = parser.prog(); 134 | 135 | EvalVisitor eval = new EvalVisitor(); 136 | // 开始遍历语法分析树 137 | eval.visit(tree); 138 | 139 | System.out.println(tree.toStringTree(parser)); 140 | } 141 | } 142 | ``` 143 | 144 | 创建一个运行主程序的脚本: 145 | 146 | ``` 147 | #!/bin/sh 148 | java -cp .:./antlr-4.5.1-complete.jar:$CLASSPATH $* 149 | ``` 150 | 151 | 把它保存为run.sh后,执行以下命令: 152 | 153 | ``` 154 | compile *.java 155 | run Calc calc.txt 156 | ``` 157 | 158 | 然后你就会看到文本形式的语法分析树以及计算结果: 159 | 160 | ``` 161 | 193 162 | 17 163 | 9 164 | (prog (stat (expr 193)) (stat a = (expr 5)) (stat b = (expr 6)) 165 | (stat (expr (expr a) + (expr (expr b) * (expr 2)))) (stat (expr 166 | (expr ( (expr (expr 1) + (expr 2)) )) * (expr 3)))) 167 | ``` 168 | -------------------------------------------------------------------------------- /common-language-patterns.md: -------------------------------------------------------------------------------- 1 | # 常用语言模式 2 | 3 | 现在,我们已经有了一个自顶向下的草拟出语法的通用策略,下面我们要专注于一些常用的语言模式。尽管在过去几十年里有大量的语言被发明,但仍然只有较少的基本语言模式需要被处理。这是因为人们趋向于设计遵循自然语言约束的语言,语言也会因为设计者遵循数学上的常用表示法而趋向于相似。甚至在词法级别,语言趋向于重用一些相同的结构,例如标志符、整数、字符串等。这些单词顺序和依赖的约束来源于自然语言,并逐渐演化成为四种抽象的语言模式: 4 | 5 | #### 模式:序列 6 | 7 | 这是像数组初始值设定项中的值那样的一系列元素,也是在计算机语言中最常见的结构。例如,下面是登录到POP服务器时的序列: 8 | 9 | ``` 10 | USER parrt 11 | PASS secret 12 | RETR 1 13 | ``` 14 | 15 | 这些命令本身也是序列。大部分命令是一个关键词(保留标志符,例如USER和RETR)跟随一个运算元再跟随一个换行符。为了在语法中指定此类序列,我们可以按照顺序简单地列出各个元素。以下是检索命令的序列(其中INT表示整数记号类型): 16 | 17 | ``` 18 | retr : 'RETR' INT '\n' ; 19 | ``` 20 | 21 | 我们可以给RETR序列打上retr规则的标签,这样在语法的其它地方,我们就能使用规则名字作为简写来引用RETR序列。 22 | 23 | 对于任意长度的序列像矢量[1 2 3]这样的简单整数列表,虽然它是一个有限序列,但我们不可能通过像INT INT INT ...这样的规则片段来列出所有可能的整数列表。为了编码这样的一个或者多个元素,我们使用“+”子规则运算符。例如,{INT}+表示任意长度的整数序列,或者使用简写INT+也可以。至于可以为空的列表,我们则使用零个或者多个运算符“*”。 24 | 25 | 这种模式的变体有带终结符的序列和带分隔符的序列,CSV文件就很好地示范了这两者。 26 | 27 | ``` 28 | file : (row '\n')* ; // 带一个“\n”终结符的序列 29 | row : field (',' field)* ; // 带一个“,”分隔符的序列 30 | field: INT ; // 假设字段只是整数 31 | ``` 32 | 33 | 规则file使用带终结符模式的列表去匹配零个或者多个row '\n'序列,记号“\n”终结序列的每个元素。规则row使用带分隔符模式的列表去匹配一个field后面有零个或者多个',' field序列,记号“,”分隔各个字段。 34 | 35 | 最后,还有个特殊类型的零个或者一个序列,用“?”指定。可以使用它去表达可选的构造体。 36 | 37 | #### 模式:选择 38 | 39 | 这是一个在多个可供替代的短语之间的选择,比如在编程语言中不同种类的语句。为了在语言中表示选择的这个概念,我们使用“|”作为ANTLR中的“or”运算符去分隔被称为“选项”的语法选择。 40 | 41 | 回到CVS语法,我们可以通过整数或者字符串的选择让规则field变得更灵活。 42 | 43 | ``` 44 | field: INT | STRING ; 45 | ``` 46 | 47 | 任何时候,如果你发现正在说“语言结构x可以是这个或者那个”,那么你就可以确定应该使用选择模式,在规则x中使用“|”。 48 | 49 | #### 模式:记号依赖 50 | 51 | 记号依赖表示一个记号的存在需要在短语的其它地方有它的对等物的存在,比如匹配的左右括号。前面我们曾经使用INT+去表达在矢量[1 2 3]中的整数非空序列。为指定周围有方括号的矢量,我们需要一种方法去表达记号中的依赖。如果我们在句子中看到一个符号,那么我们必须在句子的其它地方找到它的对等物。为表达这种语法,我们必须使用同时指定对等符号的序列,它们通常包围或分组着其它元素。在这个案例中,我们这样指定矢量: 52 | 53 | ``` 54 | vector : '[' INT+ ']' ; // [1], [1 2], [1 2 3], ... 55 | ``` 56 | 57 | 扫视任何有效的代码,你会看到必须成对出现的各种分组符号:(...),[...],{...}。但是要牢记,依赖符号并不是必须配对的,类C语言都有的a ? b : c三元运算符就指定了当看到“?”符号时需要在接下来的短语中看到“:”符号。 58 | 59 | #### 模式:嵌套短语 60 | 61 | 嵌套短语有一个自相似的语言结构,它的子短语也遵循相同的结构。表达式是典型的自相似语言结构,由被运算符分隔的嵌套子表达式组成。类似地,while的代码块是嵌套在外部代码块内的一个代码块。我们在语法中使用递归规则表达自相似的语言结构。因此,如果规则的伪代码引用它自身,我们将需要一个递归的自引用规则。 62 | 63 | 让我们来看下代码块的嵌套是如何工作的。while语句是关键词while跟随一个在括号中的条件表达式再跟随一个语句。我们也可以把多条语句包裹在花括号里当作一个单块语句。表达语法如下所示: 64 | 65 | ``` 66 | stat: 'while' '(' expr ')' stat // 匹配WHILE语句 67 | | '{' stat* '}' // 匹配在括号中的语句块 68 | ; 69 | ``` 70 | 71 | 这里的stat可以是单条语句或者被花括号括起来的一组语句。规则stat是直接递归的,因为它在两个选项中直接引用它自身。如果我们把第二个选项移到它自己的规则中,规则stat和block将是双向间接递归的。语法如下所示: 72 | 73 | ``` 74 | stat: 'while' '(' expr ')' stat // 匹配WHILE语句 75 | | '{' stat* '}' // 匹配语句块 76 | ; 77 | block: '{' stat* '}' ; // 匹配在括号中的语句块 78 | ``` 79 | 80 | 看下面仅有3类表达式(索引数组引用、括号表达式和整数)的简单语言的语法: 81 | 82 | ``` 83 | expr: ID '[' expr ']' // a[1], a[b[1]], a[(2*b[1])] 84 | | '(' expr ')' // (1), (a[1]), (((1))), (2*a[1]) 85 | | INT // 1, 94117 86 | ; 87 | ``` 88 | 89 | 注意递归是如何自然地发生的。数组索引表达式的索引组件是表达式本身,因此我们只需要在选项中引用expr即可。 90 | 91 | 下图是关于两个例子输入的语法分析树: 92 | 93 | ![](http://codemany.com/uploads/expr-parse-tree.png) 94 | 95 | 分析树中的内部树节点是规则引用,叶子是记号引用。从树根到任何节点的路径表示元素的规则调用栈(或者ANTLR生成的递归下降语法分析器调用栈)。路径代表递归嵌套的子树有多个相同规则的引用。规则节点是其下方子树的标签。根节点是expr,所以整棵树是一个表达式。在1之前的那棵expr子树会把整数当作一个表达式。 96 | 97 | 为实现这些模式,我们只需要由选项、记号引用、规则引用组成的语法规则即可。我们还可以把这些元素组成子规则,子规则是裹在括号内的行内规则。我们也可以将子规则标记为“?”或“*”或“+”循环去识别被包围的语法片段多次。 98 | -------------------------------------------------------------------------------- /common-lexical-structures.md: -------------------------------------------------------------------------------- 1 | # 常用词法结构 2 | 3 | 编程语言在词法上看起来惊人地相似,无论是函数式、过程式、声明式还是面向对象语言,看起来几乎都是一样的。这很棒,因为我们只需要学习一次如何描述标志符和整数,没有太大的变化,就可以把它们应用到大多数编程语言上。正如语法分析器以及词法分析器使用规则去描述各种语言构造体一样,我们要使用基本相同的表示法。唯一的区别是语法分析器识别在记号流中的语法结构,而词法分析器识别在字符流中的语法结构。 4 | 5 | 因为词法分析和语法分析有相似的结构,ANTLR允许我们把两者合并在单个语法文件中。但是因为词法分析和语法分析是语言识别的两个不同阶段,我们必须告诉ANTLR每个规则是和哪个阶段相关联的。我们能够通过以大写字母开始的词法规则名字和以小写字母开始的语法规则名字做到这点。例如,ID是一个词法规则名字,expr则是一个语法规则名字。 6 | 7 | 当开始构建一个新的语法时,对于那些常用的词法构造体:标志符、数字、字符串、注释以及空格等,我们可以从已经存在的语法中拷贝粘贴规则。然后,通过一些细微的调整,就可以让它运行起来。几乎所有的语言,甚至像JSON和XML这样的非编程语言,都有这些记号的变体。例如,C语言的词法分析器完全可以标记化以下的JSON代码: 8 | 9 | ``` 10 | { 11 | "title":"Cat wrestling", 12 | "chapters":[ {"Intro":"..."}, ... ] 13 | } 14 | ``` 15 | 16 | 另一个例子就是块注释。在C语言中,它们是被/* ... */括起来的。而在XML里,注释是被括起来。但它们除了开始和结束符号之外,或多或少都有相同的词法构造。 17 | 18 | 对于关键词、运算符和标点符号,我们不需要词法规则,因为我们可以在语法分析器规则中直接引用它们,用单引号括起来,就像`'while'`、`'*'`、`'++'`这样。有些开发者更喜欢使用像MUL而不是字面量`'*'`这样的词法规则引用,这些都没问题,因为它们都有相同的记号类型。 19 | 20 | 为了阐明词法规则看起来像什么,让我们从标志符开始构建一个常用记号的简单版本。 21 | 22 | #### 匹配标志符 23 | 24 | 在语法伪代码中,一个基本的标志符是一个大写和小写字母的非空序列。根据已经学习到的知识,我们知道需要用(...)+表示法来表示这样的序列模式。因为序列元素可以是大写或小写字母,所以在子规则中我们需要使用选择运算符: 25 | 26 | ``` 27 | ID : ('a'..'z'|'A'..'Z')+ ; // 匹配一个或多个大小写字母 28 | ``` 29 | 30 | 唯一的新ANTLR表示法是范围运算符:'a'..'z'代表从a到z的任意字符。或者你也可以使用Unicode代码点字面量'\uXXXX',这里的XXXX是Unicode字符代码点值的十六进制值。 31 | 32 | 作为字符集的一个简写,ANTLR支持我们使用更熟悉的正则表达式集合表示法: 33 | 34 | ``` 35 | ID : [a-zA-Z]+ ; // 匹配一个或多个大小写字母 36 | ``` 37 | 38 | 有时候我们会发现像下面的语法貌似存在冲突的现象: 39 | 40 | ``` 41 | enumDef : 'enum' '{' ... '}' ; 42 | FOR : 'for' ; 43 | ID : [a-zA-Z]+ ; // 不匹配'enum'或者'for' 44 | ``` 45 | 46 | 规则ID也可以同时匹配enum和for这样的关键词,这意味着同样的字符串能被多个规则匹配。但事实上,ANTLR处理这种混合语法时会把字符串字面量以及词法规则与语法规则分隔开,像enum这样的字面量就变成了词法规则并紧随在语法规则之后和在显式的词法规则之前。 47 | 48 | ANTLR词法分析器通过偏爱首先指定的规则来解决词法规则间的二义性,这意味着ID规则应该定义在所有的关键词规则之后。ANTLR把隐式的为字面量生成的词法规则放在显式的词法规则之前,因此它们总是有更高的优先级。在这里,'enum'被自动赋予比ID高的优先级。 49 | 50 | 因为ANTLR会重新排序词法规则并让它在语法规则之后发生。所以上面的语法与下面的变体是相同的: 51 | 52 | ``` 53 | FOR : 'for' ; 54 | ID : [a-zA-Z]+ ; // 不匹配'enum'或者'for' 55 | enumDef : 'enum' '{' ... '}' ; 56 | ``` 57 | 58 | #### 匹配数字 59 | 60 | 描述像10这样的整型数字非常容易,因为它只是一个数字序列。 61 | 62 | ``` 63 | INT : '0'..'9'+ ; // 匹配一个或多个数字 64 | ``` 65 | 66 | 或者 67 | 68 | ``` 69 | INT : [0..9]+ ; // 匹配一个或多个数字 70 | ``` 71 | 72 | 浮点数要复杂的多,但如果我们忽略指数的话,可以很容易地制作一个简化版本。浮点数是数字序列后面跟着一个句点和一个可选的小数部分;或者以一个句点开始,然后是数字序列。单独一个句点是不合法的。因此我们的浮点规则使用一个选择和一些序列模式: 73 | 74 | ``` 75 | FLOAT: DIGIT+ '.' DIGIT* // 匹配1. 39. 3.14159等等 76 | | '.' DIGIT+ // 匹配.1 .14159 77 | ; 78 | 79 | fragment 80 | DIGIT: [0-9] ; // 匹配单个数字 81 | ``` 82 | 83 | 这里我们使用了一个帮助规则DIGIT,因此我们不必到处去写[0-9]。通过在规则前面加上fragment前缀,我们让ANTLR知道该规则仅被其它词法规则使用。它本身不是一个记号,这意味着我们不能在语法规则中引用它。 84 | 85 | #### 匹配字符串字面量 86 | 87 | 计算机语言中共同具有的下一个常用记号是字符串字面量,例如"hello"。大部分使用双引号作分隔符,有些使用单引号或两者都使用。以双引号为分隔符而言,在语法伪代码中,一个字符串就是在双引号中的任意字符序列: 88 | 89 | ``` 90 | STRING : '"' .*? '"' ; // 匹配在双引号中的任意字符 91 | ``` 92 | 93 | 语法中的点是通配符运算符,它可以匹配任意单个字符。因此,“`.*`”是一个能够匹配任意零个或多个字符的序列的循环。当然,它也将消费字符直到文件结尾,所以不是很有用。幸运的是,ANTLR通过正则表达式表示法(?后缀)提供对非贪婪模式规则的支持。非贪婪模式意味着“直到看见在词法规则中跟在子规则后的字符时才停止吃掉字符”。更确切地说,非贪婪模式规则匹配最小数量的字符,同时仍然允许整个周围的规则被匹配。相反,“`.*`”被认为是贪婪模式,因为它贪婪地消费能够匹配循环内部的所有字符。 94 | 95 | 以上的STRING规则还做得不够好,因为它不允许字符串中有双引号。为了做到这点,大部分语言定义了以反斜杠开始的转义字符。在字符串中的双引号我们可以使用“\"”。为支持常用的转义字符,我们需要使用以下规则: 96 | 97 | ``` 98 | STRING: '"' (ESC|.)*? '"' ; 99 | 100 | fragment 101 | ESC : '\\"' | '\\\\' ; // 匹配字符\"和\\ 102 | ``` 103 | 104 | ANTLR自身也需要避开转义字符,所以这里我们需要用“\\”去指定反斜杠字符。 105 | 106 | 现在,在STRING规则中的循环既可以通过调用fragment规则RULE去匹配转义字符序列,也可以通过点通配符去匹配任意字符。当看到一个非转义双引号字符时“`*?`”子规则运算符终止“`(ESC|.)*?`”循环。 107 | 108 | #### 匹配注释和空格 109 | 110 | 词法分析器会把匹配到的记号通过记号流传递给语法分析器,然后语法分析器检查流的语法结构。但我们希望当词法分析器匹配到注释和空格时能把它们扔掉。那样,语法分析器就不必为匹配无处不在的可选的注释和空格担心。例如,当WS是一个空格的词法规则时以下的语法规则就非常尴尬和容易出错: 111 | 112 | ``` 113 | assign : ID (WS|COMMENT)? '=' (WS|COMMENT)? expr (WS|COMMENT)? ; 114 | ``` 115 | 116 | 定义这些被丢弃的记号和定义非丢弃的记号一样,我们只需要使用skip指令去表明词法分析器应该扔掉它们。以下是匹配那些衍生自C的语言的单行和多行注释的语法规则: 117 | 118 | ``` 119 | LINE_COMMENT : '//' .*? '\r'? '\n' -> skip ; // 匹配"//" stuff '\n' 120 | COMMENT : '/*' .*? '*/' -> skip ; // 匹配"/*" stuff "*/" 121 | ``` 122 | 123 | 在COMMENT中,“`.*?`”消费在“`/*`”和“`*/`”之间的任意字符。在LINE_COMMENT中,“`.*?`”消费“//”之后的任意字符,直到它看到一个换行符。 124 | 125 | 词法分析器接受若干跟随在->运算符后的指令,skip只是它们中的一个。例如,我们可以通过使用channel指令把传递给语法分析器的记号放进隐藏通道。 126 | 127 | 最后,让我们处理空格这个常用记号。大部分编程语言都把空格当作记号分隔符,但某些像Python这样的语言则把空格用作特殊语法目的。以下是告诉ANTLR如何扔掉空格的语法: 128 | 129 | ``` 130 | WS : (' '|'\t'|'\r'|'\n')+ -> skip ; // 匹配一个或多个空格但丢弃 131 | ``` 132 | 133 | 或者 134 | 135 | ``` 136 | WS : [ \t\r\n]+ -> skip ; // 匹配一个或多个空格但丢弃 137 | ``` 138 | 139 | 当换行既是要被忽略的空格又是命令终结符时,就会有个问题。换行是上下文有关的,在语法上下文中,我们需要扔掉换行,但在其它地方,我们需要把它传递给语法分析器以便让它知道某个命令已经结束。该问题以及它的具体解决方案我们将在以后讨论。 140 | -------------------------------------------------------------------------------- /different-channels.md: -------------------------------------------------------------------------------- 1 | # 发送记号到不同的通道 2 | 3 | 对于大多数语法,注释和空格都是语法分析器可以忽略的东西。如果我们不想让注释和空格在语法中到处都是,那么就需要让词法分析器把它们扔掉。不幸的是,这意味着任何后续处理步骤都不能再访问注释和空格。安全地忽略掉注释和空格的方法是把这些发送给语法分析器的记号放到一个“隐藏通道”中,语法分析器仅需要调协到单个通道即可。我们可以把任何我们想要的东西传递到其它通道中。这里是如何实现的语法: 4 | 5 | ``` 6 | COMMENT 7 | : '/*' .*? '*/' -> channel(HIDDEN) // match anything between /* and */ 8 | ; 9 | 10 | WS : [ \r\t\n]+ -> channel(HIDDEN) 11 | ; 12 | ``` 13 | 14 | 就像我们前面讨论过的`-> skip`那样,`-> channel(HIDDEN)`也是一个的词法分析器指令。在这里,它设置那些记号的通道号码以便这些记号可以被语法分析器忽略。记号流仍然维护着原始的记号序列,但在喂食给语法分析器时则略过离线通道中的记号。 15 | -------------------------------------------------------------------------------- /embedding-actions.md: -------------------------------------------------------------------------------- 1 | # 在语法中嵌入任意的操作 2 | 3 | 如果我们不想付出构建语法分析树的开销,或者想要在分析期间动态地计算值或把东西打印出来,那么可以通过在语法中嵌入任意代码实现。它的比较困难的,因为我们必须明白在语法分析器上的操作的影响,以及在哪里放置这些操作。 4 | 5 | 为了解释嵌入在语法中的操作,让我们先来看下文件rows.txt中的数据: 6 | 7 | ``` 8 | parrt Terence Parr 101 9 | tombu Tom Burns 020 10 | bke Kevin Edgar 008 11 | ``` 12 | 13 | 这些列是由TAB分隔的,每一行用一个换行结束。匹配这种类型的输入在语法上还是相当简单的。下面是此语法文件Rows.g的内容: 14 | 15 | ``` 16 | file : (row NL)+ ; // NL is newline token: '\r'? '\n' 17 | row : STUFF+ ; 18 | ``` 19 | 20 | 我们需要创建一个构造器以便我们能传递我们想要的列号(从1开始计数),所以我们需要在规则中添加一些操作来做这些事情: 21 | 22 | ``` 23 | grammar Rows; 24 | 25 | @parser::members { // add members to generated RowsParser 26 | int col; 27 | public RowsParser(TokenStream input, int col) { // custom constructor 28 | this(input); 29 | this.col = col; 30 | } 31 | } 32 | 33 | file: (row NL)+ ; 34 | 35 | row 36 | locals [int i=0] 37 | : ( STUFF 38 | { 39 | $i++; 40 | if ( $i == col ) System.out.println($STUFF.text); 41 | } 42 | )+ 43 | ; 44 | 45 | TAB : '\t' -> skip ; // match but don't pass to the parser 46 | NL : '\r'? '\n' ; // match and pass to the parser 47 | STUFF: ~[\t\r\n]+ ; // match any chars except tab, newline 48 | ``` 49 | 50 | 在上述语法中,操作是被花括号括起来的代码片段;members操作的代码将会被注入到生成的语法分析器类中的成员区;在规则row中的操作访问的$i是由locals子句定义的局部变量,该操作也用$STUFF.text获取最近匹配的STUFF记号的文本内容。STUFF词法规则匹配任何非TAB或换行的字符,这意味着在列中可以有空格字符。 51 | 52 | 现在,是时候去思考如何使用定制的构造器传递一个列号给语法分析器,并且告诉语法分析器不要构建语法分析树了: 53 | 54 | ``` 55 | public class Rows { 56 | 57 | public static void main(String[] args) throws Exception { 58 | ANTLRInputStream input = new ANTLRInputStream(System.in); 59 | RowsLexer lexer = new RowsLexer(input); 60 | CommonTokenStream tokens = new CommonTokenStream(lexer); 61 | int col = Integer.valueOf(args[0]); 62 | RowsParser parser = new RowsParser(tokens, col); // pass column number! 63 | parser.setBuildParseTree(false); // don't waste time bulding a tree 64 | parser.file(); 65 | } 66 | } 67 | ``` 68 | 69 | 现在,让我们核实下我们的语法分析器能否正确匹配一些示例输入: 70 | 71 | ``` 72 | antlr -no-listener Rows.g # don't need the listener 73 | compile *.java 74 | run Rows 1 < rows.txt 75 | ``` 76 | 77 | 这时你会看到rows.txt文件的第1列内容被输出: 78 | 79 | ``` 80 | parrt 81 | tombu 82 | bke 83 | ``` 84 | 85 | 如果将上面命令中的1换成2,你会看到rows.txt文件的第2列内容被输出;如果换成3,那么rows.txt文件的第3列内容将会被输出。 86 | -------------------------------------------------------------------------------- /getting-started.md: -------------------------------------------------------------------------------- 1 | # 入门 2 | 3 | 先看下面这段用于识别像hello world那样的短语的简单文法: 4 | 5 | ``` 6 | grammar Hello; // 定义文法的名字 7 | 8 | s : 'hello' ID ; // 匹配关键字hello和标志符 9 | ID : [a-z]+ ; // 标志符由小写字母组成 10 | WS : [ \t\r\n]+ -> skip ; // 跳过空格、制表符、回车符和换行符 11 | ``` 12 | 13 | 把以上文法保存为Hello.g,然后执行以下命令来生成识别器: 14 | 15 | ``` 16 | $ ./antlr Hello.g 17 | ``` 18 | 19 | 该命令会在相同目录下生成后缀名为tokens、interp和java的8个文件: 20 | 21 | ``` 22 | $ ls Hello* 23 | Hello.tokens HelloLexer.interp HelloLexer.java HelloBaseListener.java Hello.g 24 | Hello.interp HelloLexer.tokens HelloParser.java HelloListener.java 25 | ``` 26 | 27 | 现在开始准备编译由ANTLR生成的Java代码。先写个脚本把编译命令包装起来: 28 | 29 | ``` 30 | #!/bin/sh 31 | javac -cp antlr-4.7.1-complete.jar $* 32 | ``` 33 | 34 | 把它保存为compile文件,然后你就可以用以下命令编译代码: 35 | 36 | ``` 37 | $ ./compile Hello*.java 38 | ``` 39 | 40 | 到此,我们已经有一个可以被执行的识别器,只缺一个主程序去触发语言识别。 41 | 42 | ANTLR运行库有提供一个称之为TestRig的测试工具,可以让你不创建主程序就能测试文法。TestRig使用Java反射调用编译后的识别器,它能显示关于识别器如何匹配输入的大量信息。 43 | 44 | 同样地,创建一个脚本grun来简化以后的打字数: 45 | 46 | ``` 47 | #!/bin/sh 48 | java -cp .:$PWD/antlr-4.7.1-complete.jar org.antlr.v4.gui.TestRig $* 49 | ``` 50 | 51 | 现在,让我们来打印出识别期间创建的那些记号(记号是指像关键字hello和标识符world那样的词汇符号): 52 | 53 | ``` 54 | $ ./grun Hello s -tokens # Hello是文法的名字。s是开始的规则名字。 55 | ``` 56 | 57 | 敲入上述命令并按回车,接着输入以下内容: 58 | 59 | ``` 60 | hello world # 输入并按回车 61 | EOF # Linux系统输入Ctrl+D或Windows系统输入Ctrl+Z并按回车 62 | ``` 63 | 64 | TestRig会打印出记号的列表,每一行输出表示一个记号以及它的相关信息: 65 | 66 | ``` 67 | [@0,0:4='hello',<1>,1:0] 68 | [@1,6:10='world',<2>,1:6] 69 | [@2,13:12='',<-1>,2:0] 70 | ``` 71 | 72 | 这里详细讲解下[@1,6:10='world',<2>,1:6]的意义。@1表示记号索引(从0开始计数);6:10表示记号开始与结束的位置(从0开始计数);<2>表示记号类型,具体数值和类型存储在后缀名为tokens的文件中;最后的1:6表示记号在第一行(从1开始计数),从第6个字符开始(从0开始计数,制表符作为单个字符计算)。 73 | 74 | 或者以LISP风格的文本形式查看记号: 75 | 76 | ``` 77 | $ ./grun Hello s -tree 78 | ``` 79 | 80 | 它会输出如下形式的记号: 81 | 82 | ``` 83 | (s hello world) 84 | ``` 85 | 86 | 最直观的就是以可视化的方式查看语法分析树: 87 | 88 | ``` 89 | $ ./grun Hello s -gui 90 | ``` 91 | 92 | ![](images/parse-tree-hello.png) 93 | 94 | 以下是TestRig可用的所有参数: 95 | 96 | * -tokens 打印出记号流。 97 | * -tree 以LISP风格的文本形式打印出语法分析树。 98 | * -gui 在对话框中可视化地显示语法分析树。 99 | * -ps file.ps 在PostScript中生成一个可视化的语法分析树表示,并把它存储在file.ps文件中。 100 | * -encoding encodingname 指定输入文件的编码。 101 | * -trace 在进入/退出规则前打印规则名字和当前的记号。 102 | * -diagnostics 分析时打开诊断消息。此生成消息仅用于异常情况,如二义性输入短语。 103 | * -SLL 使用更快但稍弱的分析策略。 104 | -------------------------------------------------------------------------------- /grammar-design.md: -------------------------------------------------------------------------------- 1 | # 语法设计 2 | 3 | 在聚焦到具体的语法规则内部结构之前,我们要先讨论下语法的整体剖析以及如何形成一套初始的语法骨架。 4 | 5 | 文法文件通常是由一个命名文法的头和一系列可以彼此调用的规则组成。就像下面的那样: 6 | 7 | ``` 8 | grammar MyG; 9 | rule1 : «stuff» ; 10 | rule2 : «more stuff» ; 11 | ... 12 | ``` 13 | 14 | 设计语法就是要搞清楚«stuff»是什么?哪个规则是开始规则。这要求我们需要知道给定语言的一系列代表性的输入例子。当然,从语言参考手册或者其它语法分析器生成器格式而来的语法也是有帮助的。 15 | 16 | 正确设计语法的方法是借鉴功能分解或者自顶向下的设计,从粗粒度级别到细粒度级别逐步定义语言结构并把它们编码为语法规则。所以,我们的第一个任务就是找到粗粒度语言结构的名字,同时它也是开始规则。在英语中我们使用sentence,对于XML文件来说它则是document。 17 | 18 | 设计开始规则的内容是用英语伪代码描述整个输入格式的问题。例如,“a comma-separated-value (CSV) file is a sequence of rows terminated by newlines.”这段文字,在is a左边的至关重要的单词file是规则名字,在is a右边的所有内容则成为在规则定义右则的«stuff»的内容: 19 | 20 | ``` 21 | file : «sequence of rows that are terminated by newlines» ; 22 | ``` 23 | 24 | 然后我们通过描述在开始规则右侧被确定的元素来进行下一个粒度级别的设计。在规则右侧的名词通常是对记号或尚未定义的规则的引用,这些记号是那些我们在正常情况下视为单词、标点符号、运算符的元素。就像单词是英语句子中的原子成分那样,记号在语法规则中也是如此。规则引用则涉及到像row那样需要被分解为更详细部分的其它语言结构。 25 | 26 | 进入细节的另外一层,我们可以说row是一系列被逗号分隔的field,而field则是一个数字或字符串。就像以下所示: 27 | 28 | ``` 29 | row : «sequence of fields separated by commas» ; 30 | field : «number or string» ; 31 | ``` 32 | 33 | 当没有规则需要再定义时,我们就得到了语法的一个粗略的草图。 34 | 35 | 如果有其它格式的语法作为参考的话设计语法会容易的多,但小心不要盲目地遵循它,否则你会误入歧途的。非ANTLR格式的语法只是让你知道别人是如何决定分解语言中的短语的,它最大的作用就是可以给我们一份规则名称的列表用作参考。 36 | 37 | 不推荐从参考手册上复制粘贴语法到ANTLR,然后再通过细微的调整让它工作。把它当作一套指南而不是一段代码是更好的办法。为了清晰地描述语法,参考手册通常是相当松散的。这意味着语法能识别大量不在语言中的句子,或者语法可能不够明确,可以用多种方法匹配相同的输入序列。例如,语法可能会说表达式可以调用一个构建器或者访问一个函数,问题是像T(i)这样的输入可以同时匹配两者。理想情况下,在语法中是不能有这样的二义性的,每个输入句子我们只需要一种解释。 38 | 39 | 在另一个极端,参考手册中的语法有时过于明确地说明了规则。有些约束是需要在分析完输入后实施的,而不是试图对语法结构实施约束。例如,W3C XML语法就显式地指定标签中什么地方必须要有空格以及什么地方的空格可以省略。但事实是我们可以简单地让词法分析器在把记号发送给语法分析器之前去除空格,不需要在语法中到处测试它。 40 | 41 | 规格还说标签可以有两个附加属性encoding和standalone。我们需要知道约束,但它是很容易去允许任何属性名字,然后在语法分析后检查语法分析树,以确保所有这些限制都满足的。 42 | 归根结底,XML只是嵌在文本中的一对标签,因此它的语法结构是相当直白的。唯一的挑战是如何分别对待什么在标签内以及什么在标签外。 43 | 44 | 识别语法规则并用伪代码表示它们的右侧部分最初是个挑战,但当你为更多的语言构建语法后它会变得越来越容易。一旦我们有了伪代码,我们就需要把它转换成ANTLR表示法,以便能得到一个可工作的语法。 45 | -------------------------------------------------------------------------------- /images/basic-data-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dohkoos/antlr4-short-course/282aeff6b4f4cc6cd64910c8cd11d9a285025d6c/images/basic-data-flow.png -------------------------------------------------------------------------------- /images/parse-tree-hello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dohkoos/antlr4-short-course/282aeff6b4f4cc6cd64910c8cd11d9a285025d6c/images/parse-tree-hello.png -------------------------------------------------------------------------------- /installing-antlr.md: -------------------------------------------------------------------------------- 1 | # 安装ANTLR 2 | 3 | ANTLR是由Java写成的,所以在安装ANTLR前必须保证已经安装有Java 1.6或以上版本。你可以到[这里](http://www.antlr.org/download.html)下载ANTLR的最新版本,或者也可使用命令行工具下载: 4 | 5 | ``` 6 | $ curl -O https://www.antlr.org/download/antlr-4.7.1-complete.jar 7 | ``` 8 | 9 | 归档文件包含运行ANTLR工具的所有必要依赖,以及编译和执行由ANTLR生成的识别器所需的运行库。简而言之,就是ANTLR工具将文法转换成识别程序,然后识别程序利用ANTLR运行库中的某些支持类识别由该文法描述的语言的句子。此外,该归档文件还包含两个支持库:TreeLayout(一个复杂的树布局库)StringTemplate(一个用于生成代码和其它结构化文本的模板引擎)。 10 | 11 | 现在来测试下ANTLR工具是否工作正常: 12 | 13 | ``` 14 | $ java -jar antlr-4.7.1-complete.jar # 启动org.antlr.v4.Tool 15 | ``` 16 | 17 | 如果正常的话会看到以下帮助信息: 18 | 19 | ``` 20 | ANTLR Parser Generator Version 4.7.1 21 | -o ___ specify output directory where all output is generated 22 | -lib ___ specify location of grammars, tokens files 23 | -atn generate rule augmented transition network diagrams 24 | -encoding ___ specify grammar file encoding; e.g., euc-jp 25 | -message-format ___ specify output style for messages in antlr, gnu, vs2005 26 | -long-messages show exception details when available for errors and warnings 27 | -listener generate parse tree listener (default) 28 | -no-listener don't generate parse tree listener 29 | -visitor generate parse tree visitor 30 | -no-visitor don't generate parse tree visitor (default) 31 | -package ___ specify a package/namespace for the generated code 32 | -depend generate file dependencies 33 | -D