《搜索引擎零距离》第三章 IRS虚拟机及编译器实现原理(5)
roki
2009-06-24
3.3.2.2 SableCC4的新特性
SableCC在经历了SableCC2、SableC3和SableCC4几个主要版本的进化之后,已经变得越来越完善和易用。笔者在设计IRS语言的时候,是按照SableCC3的语法设计的,但是在编写本书的时候,SableCC4已经可用了,并加上很多优秀的特性。 这里通过简单的例子展现SableCC4的词法分析器的语法: Language simple_example; Syntax 4; Lexer Helpers /* regular expression matching all valid 16-bit characters */ any = #x0..#xffff; letter = 'a'..'z'; number = '0'..'9'; cr = #13; lf = #10; eol = cr | lf | cr lf; Tokens identifier = letter (letter | number)*; line_comment = '//' (any - (cr | lf))* eol?; Ignored line_comment; 这个词法分析器包含一个Helpers段,这个Helpers段定义了一些在token定义中有用的正则表达式。它也有一个Tokens段,这个Tokens段定义了两个token,“变量”(identifier)和“行注释”(line_comment).最后,它包含了一个Ignored(忽略)段,这个Ignored段定义了两个不返回给语法分析器的token。 这个简单的词法分析器同样展现了两个特性:区间(比如 'a'..'z)和正则表达式减法(比如any - (cr | lf)) 组和优先级 用相互重叠的正则表达式来定义token(词语单元)经常是方便的。一个典型的例子是对关键字和标识符的匹配。SableCC 4需要对相互重叠的终结符做显式的优先级定义。以下是一个例子: ... Tokens Group keywords: if = 'if'; for = 'for'; begin = 'begin'; end = 'end'; Group others: identifier = letter (letter | number)*; line_comment = '//' (any - (cr | lf))* eol?; Priorities /*如果一个词既可以是keyword 又可以是identifier ,那么把他识别为keyword */ keywords Over identifier; Ignored line_comment; 在这个例子里,可以看到了如何在Group(组)中声明token(词语单元),只需在一系列的token后简单地加上Group 组名。也知道了如何显式声明Token之间的优先级。注意到keywords组是有用的,没有它的话,我们就必须写成: Priorities if, for, begin, end Over identifier; 优先级声明是必要的,因为一个字符串,比如begin被两种token的定义所匹配:begin和identifier。这以某种方式告诉我们,这种标识符定义是错的。正确的结果应该是:我们不把字符串begin认为是标识符,而(letter (letter | number)*)可以匹配它。确定的定义应该是: identifier = (letter (letter | number)*) - ('if' | 'for' | 'begin' | 'end'); 很容易看出来,对于一个复杂的语言定义准确的标识符会是一件痛苦的工作。通过一个简单通用的正则表达式,显式的声明token之间的优先级,是一个方便得多的方法。 SableCC will report an error if two tokens overlap and no priority is provided. It will also report an error when a token is never matched (possibly because of an erroneous priority declaration) or when a priority declaration is never used. 如果两个token重叠了而却没有提供优先级,那么SableCC将会报错。如果一个token没有从未匹配到(可能因为一个错误的优先级定义),或者一个优先级定义从未被使用到,SableCC也会报错。 前向检查 Lookahead Infinite; 类似SableCC生成的这种基于DFA(有限自动机)的词法分析器(lexer),以其高效著称,他们被期望有线性的、最坏情况是O(n)的算法复杂度(n是输入字符的数量)。但是很少有人知道,在一些不引人注目的情况下,这种效率是不成立的。下面是一个例子: Tokens left_parenthese = '('; parenthesized_word = '(' letter+ ')'; 这个例子除了说明问题之外不做其他事。下面是这个例子的实际情况: 现在把下列内容输送进词法分析器(lexer):"(adggsjfgsgfjasdkjhgfg"。正如你看到的,最长匹配是left_parenthese("("),然而,在遇到"adggsjfgsgfjasdkjhgfg"后缀而发现没有闭合的)之前,lexer不能决定是否它必须匹配left_parenthese或者parenthesized_word。在典型的例子中,一个lexer将会持续消耗字符直到它确定无法匹配到合法的token。在这个例子里,它不得不一直查看到输入的末尾。 下面的输入展现了一个最坏的场景:: "((((((((((((((((((((((((((((((((((((((((((((((((((".在这样的一个例子中,lexer必须匹配50个left_parenthese token,但是对于每个token,lexer会试图查看所有剩下的输入字符。代价是50 + 49 + 48 + ... + 1, 也就是O(n^2)。 SableCC 4会自动探测到这个问题并且报错。然而,如果用户确实需要这样一个降级了的lexer,那么SableCC用如下的方式允许这样做: Tokens left_parenthese = '('; parenthesized_word = '(' letter+ ')'; Lookahead /* tell SableCC that we expect O(n^2) instead of O(n) */ Infinite; 为了避免用户养成这种有隐患的习惯,SableCC对非必要的“无限向前查看”(infinite lookahead)声明会报错。 前向查看关闭 通常,SableCC需要定义一个定长的向前查看缓冲来匹配最长的token,然而,有时lexer需要不向前查看。SableCC允许用户定义不向前查看的需求,然后如果token定义需要向前查看的话,就报错: Example: Tokens begin = 'begin'; end = 'end'; success = 'success'; error = 'error'; Lookahead /* tell SableCC not to allow for lookahead buffer */ None; Let's look at a sample input: "beginsuccessend". You can easily see that a lexer needs not look beyond the "n" of "begin" to know that it must match the begin token. In this case, three tokens will be matched: begin, success, and end. We think that this feature might be appreciated by stream protocol designers. 这个示例输入:beginsuccessend. 你能简单地看出来,当遇到n之后,不需要再往前查看了就能确定它已经匹配到begin了。在这个例子里,三个token必须被匹配: begin, success, 和 end。这个特性可能会受到流协议的设计者的赞赏。 Lookahead Buffer In earlier SableCC versions, users had to provide a PushbackReader object to generated lexers. One had to resort to some kind of black magic to guess the correct push back buffer size, in order for the lexer not to throw an IOExeption for lack of buffer space. The black magic usually consisted of error-prone trial and error, or more commonly, using an arbitrary 1024 character buffer. 前向查看缓存 在早期版本的SableCC中,用户必须提供一个PushbackReader对象来生成lexer。为了避免lexer在缓冲区不足的情况下抛出一个IOExeption,用户必须求 助与一些巫术来猜测正确的push back缓冲大小。这种尝试通常容易引发错误,更通常的做法是指定固定的1024个字符作为缓冲。在第4版中,这个确定的push back缓存需求现在由SableCC计算出来并且直接编码进lexer。这完全根除了这个常见的陷阱。 词法分析器状态 SableCC允许用户声明“词法分析器状态”。某种意义上来说,每个“词法分析器状态”都是一个独立的词法分析器。每个状态识别一组特定的token。 “词法分析器状态”对多层次的语言是有用的。 一个普通的例子是HTML语言。下面是一段示例HTML: <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"> <head> <title>The Title</title> </head> <body> Here is a <a href="link">link</a>. </body> </html> 请注意< ... >之间的结构的语法,它和它外面的自由文本是不同的。特别是Here is a ...这个句子中的a是一个文本中的简单单词,而在<a href="link">中,a被匹配为一个标识符。没有“词法分析器状态”的话,为HTML写一个语法分析器会非常痛苦,因为以下直觉容易理解的语法是无效的: /* This does not work! */ Lexer Tokens text = (any - '<')+; identifier = letter+; tag_start = '<'; tag_end = '>'; slash = '/'; equal = '='; string = '"' (any - ('"' | cr | lf))* '"'; blank = (' ' | eol)+; Ignored blank; Parser mini_broken_html = part*; part = {tag} open_tag part* close_tag | {text} text; open_tag = tag_start identifier tag_body_part* tag_end; close_tag = tag_start slash identifier tag_body_part* tag_end; tag_body_part = identifier equal string; What's wrong with it? Many things. For one thing, SableCC will complain about the overlapping text and identifier tokens. And, no, there is no good priority between these two tokens. But it gets worse... The following fragment will be rejected <html>"Hello!"</html>. Why? because "Hello!" will probably be matched as a string token, instead of a text token. We really need two lexers: one that will match text and tag_start, and one that will match the remaining tokens. 出了什么错?这里很多错。 首先,SableCC会对重叠的text和identifier词语单元(token)报错。其次,两个token没有正确优先级。但是使得问题更严重的是…后面跟着的段落将拒绝<html>"Hello!"</html>。为什么? 因为"Hello!"可能被作为一个string token匹配,而不是一个text token。因此确实需要两个词法分析器:一个匹配text 和tag_start,一个匹配其他的token。 以下是“词法分析器状态”特性如何解决以上问题。在SableCC 4中“词法分析器状态”的语法如下: Lexer Tokens text = (any - '<')+; identifier = letter+; tag_start = '<'; tag_end = '>'; slash = '/'; equal = '='; string = '"' (any - ('"' | cr | lf))* '"'; blank = (' ' | eol)+; States State free_text: Tokens text, tag_start; State inside_tag: Tokens identifier, tag_end, slash, equal, string, blank; Ignored blank;现在,还需要确认这个词法分析器在匹配token的时候在正确的状态下。 按照传统的做法,词法分析器生成器提供了一个语法来指定状态转换。 问题是,通过这个方法,确定正确的状态转换成了程序员的责任。这是容易出错的。 SableCC 4完成了其他的分析器生成器以前从未做过的工作:它允许分析器自动完成状态转换。用户只需要简单地在状态中声明语法产生式,如下: Parser State free_text: mini_broken_html = part*; part = {tag} open_tag part* close_tag | {text} text; open_tag = tag_start open_tag_body; close_tag = tag_start close_tag_body; State inside_tag: open_tag_body = identifier tag_body_part* tag_end; close_tag_body = slash identifier tag_body_part* tag_end; tag_body_part = identifier equal string;正如你所见的,这个过程是简单、容易理解的。然而,这个方法相对传统方法来说有一个巨大的优势:它能够捕捉任何二义性的状态转化! 在以上的语法中,很容易看出来状态转化如何在tag_star和tag_end上进行。但是,对于很多其他语言,就不那么简单了。想象一下,例如,允许内部标签(比如<id <id>>)。通过把状态转换工作交给分析器来执行,SableCC自动处理这样的问题。另外,SableCC也会对二义性的状态转换报错。下面是例子: Parser State free_text: ... part = {tag} open_tag part* close_tag | {ambiguous} tag_start text | {text} text; open_tag = tag_start open_tag_body; ... State inside_tag: open_tag_body = identifier tag_body_part* tag_end; ... 这个语法没有LR冲突!一个传统的语法分析器(parser)生成器将会简单地接受它。问题是与状态转化相关的:当词法分析器遇到一个tag_start token之后,它的状态是否应该转移?没有好的回答。如果状态转变了,那么parser匹配不到part.ambiguous这个替换式,如果不转变的话,parser匹配不到part.tag这个替换式。具体来说,因为词法分析器的状态转移问题,这个语法是二意的。SableCC4会正确的检测到这个问题并且报错。希望用户喜欢这个特性。 内部状态、语义选择子与调查子 有时,正则表达式不能描述token。比如,递归注释不能使用正则表达式来描述。例如: /* This is /* a comment within */ another comment */ 理论证明已经存在(著名的pumping定理),以上的式子无法用正则表达式来描述。然而,有些语言,比如Pascal,确实使用这种注释。事实上,他们是很有用的,因为他们允许你注释一个程序段,即使里面已经包含注释了。早期版本的SableCC允许用非常技巧性的定制词法分析器用一些丑陋的做法来识别递归注释。如果你真的很好奇的话,就去读一下SableCC的硕士论文,那里面作了解释。 SableCC 4引入了一个概念“不完整tokens和内部词法分析器状态,加上语义调查子和选择子”来优雅地处理这种问题。这些到底是什么?内部词法分析器是简单的、带有识别特性的词法分析状态:他们不是向parser返回匹配的token,而是把匹配到的token加到原来导致内部状态转换的不完整token上去。 下面是递归注释的词法分析规格定义: Tokens Group default: /* incomplete token definition, the internal state will complete the token */ comment = '/*' ...; Group comment_parts: nested_comment_start = '/*'; slash = '/'; star = '*'; text = (any - ('*' | '/'))+; nested_comment_end, external_comment_end : comment_end_selector() = '*/'; Investigators nested_comment_start : comment_depth_increment(); nested_comment_end : comment_depth_decrement(); States State normal: Tokens comment; Internal State comment_body: Tokens comment_parts; Transitions normal ->( comment )-> comment_body; comment_body ->( external_comment_end )-> Back;让我们慢慢来研究。首先,是一个奇怪的注释token定义: comment = '/*' ...; 接下去阐述它起到什么作用。 它简单地定义了一个可以使用正则表达式/*匹配到的token。然而,它也告诉我们,最终的token会比/*长,它还包含所有被“内部状态”添加的文本。 还有一个“调查子”段。 Investigators nested_comment_start : comment_depth_increment(); nested_comment_end : comment_depth_decrement(); 一个“调查子”只是一个简单的方法,它会在lexer每次匹配到一个token的时候被调用。你可能忽略了“语义选择子”。下面就是它: nested_comment_end, external_comment_end : comment_end_selector() = '*/'; “语义选择子”按下面的方式工作。首先lexer用给定的正则表达式('*/')匹配token,然后,它调用特定的方法来选择返回什么token。一个典型“调查子”与“选择子”的Java实现看上去是这个的样子: public class RecursiveCommentLexer extends Lexer { private int depth = 0; @Override public void commentDepthIncrement( Token token) { this.depth++; } @Override public void commentDepthDecrement( Token token) { this.depth--; } @Override public CommentEndSelectorResult commentEndSelector(String text) { if(this.depth == 0) { return CommentEndSelectorResult.EXTERNAL_COMMENT_END; } return CommentEndSelectorResult.NESTED_COMMENT_END; }(假定CommentEndSelectorResult是一个由SableCC生成的Java枚举类) 现在,最后的一段:词法分析器状态转换。有两个转换: Transitions normal ->( comment )-> comment_body; comment_body ->( external_comment_end )-> Back; 当然,对于每一个不完整注释,SableCC需要一个像第一个转换那样的转换。 状态转移的目标状态必须总是一个内部状态,或者Back。 一个Back转移表示已经完整匹配到所有不完整token的每个部分,现在它可以被构造并发送到语法分析器了。 于是,来完整地看一个例子,使用上文的递归注释: /* This is /* a comment within */ another comment */ 由于lexer使用默认状态初始化,因此它会把"/*"作为“不完整注释token”来匹配,然后状态转移到comment_body状态。注意,这个comment token(比如Java中的TComment实例)到这个阶段还没有被产生出来。下一个匹配到的token是文本(This is )。由于词法分析器处在comment_body状态,这段文本被简单的追加,变成(/* This is ) 然后,一个nested_comment_start("/*") token被匹配。由于这个token上存在一个已注册的调查子,所以它被调用。这个commentDepthIncrement方法被调用,设置实例变量depth为1.和通常一样,这个文本被添加产生/* This is /*。再一次。一段文本(a comment within ) 被匹配到,并且追加,形成/* This is /* a comment within 接着,*/被匹配。这时候该选择token,所以我们调用selector方法。commentEndSelector被调用;由于当前的深度是1而不是0,所以将选择nested_comment_end("*/") token。由于这个注释上有一个已注册的调查子, commentDepthDecrement方法将被调用,并且减小depth实例变量到0。 接下去, "*/"被匹配到。这是一个“token selection”,所以选择子方法被调用。commentEndSelector方法被调用:因为当前的深度是0,于是external_comment_end被选中。我们在内部状态中,所以文本将被添加,也就是结果是"/* This is /* a comment within */ another comment */。 由于external_comment_end上存在一个已经注册了的状态转移,所以这个转移被执行。由于这个转移是Back转移,于是词法分析器的状态被转化成最后一个非内部的状态(当前这个例子里是nornal),最后,注释(/* This is /* a comment within */ another comment */)token被生成,返回给词法分析器。 正如你看到的,所需的只是很少的一些与特定目的相关的代码(比如Java代码),把这些代码加到继承类里。大部分的魔术般的工作由生成的lexer完成(状态转移、文本累积和产生token)。最终的词法分析器规则说明是非常简单的(我们认为是优雅的)。选择子的返回类型的类型稳定性(使用Java枚举类型)使得用户很少有机会犯错。 新的正则表达式操作符 最后,但同样重要的是SableCC 4引入了非常方便的新的正则表达式操作符。 第一个操作符是“减操作符”(-),前面你已经看到过它的使用了。 它是非常方便,它允许写出一些如果不用它写的话就很难写的正则表达式。例如,下面是一个匹配除了包含"etienne"子串的字符串之外的所有字符串的正则表达式: all_but_etienne = any* - (any* 'etienne' any*); 这看上去很简单,不是吗?然后,试试不使用“减操作符”来写?是的,理论证明解决方法是有的。实际上,有无限数量的解法! 我把它作为一个作业来留给你去找一个较简单的解法。 第二个操作符是“最短操作符”: comment = Shortest('/*' any* '*/'); 在这里,最短匹配是非常重要的,如果你选择了最长匹配,那么一个注释会从第一个/*开始匹配到文件中最后一个*/!没有最短操作符的话,正确的定义是这样的: comment = '/*' not_star* ('*' (not_star_slash not_star*)?)* '*/'; 大多数人都会发现第一种定义更清晰简洁。 最后。为了完整性,一个“交集操作符”被添加。如果你找到一个自然使用例子,请告诉我们。下面是一个无用的示例: b_star = Intersection( ('a'|'b')*, ('b'|'c')* ); 结论 总而言之,希望这个文档已经让你对SableCC 4的词法分析器的语法和特性有了大致了解。SableCC 4旨在为编写词法分析器提供一个有力的,高效的、直觉容易理解的稳定环境。 好好用用它吧! 3.3.2.3 面向对象的编译器框架 1998年,McGill大学计算机专业的Etienne Gagnon 以SableCC为题写出了他的硕士论文《SABLECC,AN OBJECT-ORIENTED COMPILER FRAMEWORK》,这篇论文经常被SableCC相关的文章引用。 以下是这篇论文的摘要与中文翻译: In this thesis, we introduce SableCC, an object-oriented framework that generates compilers (and interpreters) in the Java programming language. This framework is based on two fundamental design decisions. Firstly, the framework uses objectoriented techniques to automatically build a strictly-typed abstract syntax tree that matches the grammar of the compiled language and simplifies debugging. Secondly,the framework generates tree-walker classes using an extended version of the visitor design pattern which enables the implementation of actions on the nodes of the abstract syntax tree using inheritance. These two design decisions lead to a tool that supports a shorter development cycle for constructing compilers. We conclude that the use of object-oriented techniques significantly reduces the length of the programmer written code, can shorten the development time and finally,makes the code easier to read and maintain. 这篇论文介绍了SableCC,一个面向对象的Java语言编译器(解释器)生成框架。这个框架基于两个基本的设计决定。 首先,这个框架使用面向对象技术来自动构建一个类型严格的抽象语法树,这个语法树可以匹配待编译的语言的语法并简化调试过程。 其次,这个框架生成“树遍历”类,这些类是扩展版本的“访问者设计模式”,它们使用“继承”来实现抽象语法树节点上的动作(action)的实现。 这两个设计决定使得这个工具对于构建编译器有了较短的开发周期。这个工具的使用能够大大减少程序员编写的代码,缩短开发时间,最终,使得代码易于阅读和维护。 3.3.2.4 SableCC with Eclipse 原文网址: http://www.comp.nus.edu.sg/~sethhetu/rooms/Tutorials/EclipseAndSableCC.html 作者联系方式: hetus@rpi.edu 在Eclipse中使用SableCC的入门手册 介绍: 出于以下两点,我写了这个关于如何学习和使用SableCC的教程。 1)SableCC是最容易使用的编译器生成器,它的设计决定是在仔细研究之后作出的。 2)如果你不是精确的知道自己在做什么的话,SableCC是非常难于安装的。而且,所有的文档看上去都假设你熟悉Linux或者熟悉Eclipse的使用。与此形成反差的,Antlr,一个LL(k)的分析器生成器拥有大量的有用文档和教程。所以,我的目标是把这个差距略微减小一些。SableCC是一个快速开发编译器的有效途径, 我将要教你如何顺畅地在Eclipse中使用它。 首先,需要知道一些关于Eclipse的事:它定义自己的classpath变量,所以你不需要麻烦地设置环境变量, 除非你是在Eclipse之外使用SableCC。 然后,你需要从http://www.eclipse.org 下载并安装Eclipse,从http://sablecc.org/ 下载SableCC。 启动Eclipse创建一个叫做Test Sable Project的项目。 a)选择File->New->Project命令。 b)选择Java,选择Java Project。 c)在下一个窗口中,输入Test Sable Project作为项目名。点击Finish。 加一个新文件到你的项目里去(在项目的Package Explore上右击,选择New->File),命名为simpleAdder.sable。点击"Finish" 下面是一个非常简单的SableCC 语法定义,它接受[INT] + [INT]形式的表达式并打印初结果。复制粘贴这些内容到新文件里,并保存: /* simpleAdder.sable - A very simple program that recognizes two integers being added. */ Package simpleAdder ; Helpers /* Our helpers */ digit = ['0' .. '9'] ; sp = ' ' ; nl = 10 ; Tokens /* Our simple token definition(s). */ integer = digit+ sp*; plus = '+' sp*; semi = ';' nl?; Productions /* Our super-simple grammar */ program = [left]:integer plus [right]:integer semi; 现在该调用SableCC了。是否记得之前说过Eclipse自己管理它的classpath变量?这些变量在不同项目中是不同的,我们将会修改classpath,这会省却以后的一些头疼的事。右击你的项目,点击Properties,然后在左面的菜单上选择"Java Build Path",选择Libraries标签,点击Add External Jars... 浏览你解压缩出来的SableCC文件夹,双击"lib",然后点击"sablecc.jar"文件,点击"Open",它现在已经在build path里了!点击OK 既然已经设置好了路径,我们将生成一个工具来帮助你快速编译.sable文件。只需要这样做一次,就会在后面帮你省下好多时间。选择Run -> External Tools -> External Tools...,然后单击Program,点击 New命令。 把这个工具命名为SableCC Compiler。跳出的界面中的location填你的sdk的javaw.exe,working directory应该设成${container_loc}。设置arguments为-classpath C:\sablecc\\lib\sablecc.jar org.sablecc.sablecc.SableCC ${resource_name},这被转化为: a) –classpath表示你在设置classpath。 b) C:\sablecc\lib\sablecc.jar是你的jar包的路径。 c) org.sablecc.sablecc.SableCC这个类是调用SableCC所需的主类。 d) ${resource_name}是需要处理的 sableCC规格定义文件。 单击Apply和Run按钮你将得到一些控制台里的文本,告诉你SableCC在做什么。现在,点击你项目的包浏览器并按F5键刷新,SableCC已经产生了几个新的文件夹。 现在可以测试我们编译好了的编译器了:为了测试你添加的语法是否正确,我们需要一个解释器。右击你的项目,选择New->Package,命名这个package为simpleAdder.interpret。右击你的新包,选择New->Class,命名你的类为Interpreter,然后输入如下代码: /* An interpreter for the simple math language we all espouse. */ package simpleAdder.interpret; import simpleAdder.node.* ; import simpleAdder.analysis.* ; import java.lang.System; public class Interpreter extends DepthFirstAdapter { public void caseAProgram(AProgram node) { String lhs = node.getLeft().getText().trim(); String rhs = node.getRight().getText().trim(); int result = (new Integer(lhs)).intValue() + (new Integer(rhs)).intValue(); System.out.println(lhs + "+" + rhs + "=" + result); } }现在,需要一个主文件来运行整个项目。右击你的项目,选择New->Class命令,命名为Main,并输入如下代码: /* Create an AST, then invoke our interpreter. */ import simpleAdder.interpret.Interpreter; import simpleAdder.parser.* ; import simpleAdder.lexer.* ; import simpleAdder.node.* ; import java.io.* ; public class Main { public static void main(String[] args) { if (args.length > 0) { try { /* Form our AST */ Lexer lexer = new Lexer (new PushbackReader( new FileReader(args[0]), 1024)); Parser parser = new Parser(lexer); Start ast = parser.parse() ; /* Get our Interpreter going. */ Interpreter interp = new Interpreter () ; ast.apply(interp) ; } catch (Exception e) { System.out.println (e) ; } } else { System.err.println("usage: java simpleAdder inputFile"); System.exit(1); } } }运行这个复杂的东西, 右击Main.java,选择Run->Run...命令,单击New,一个配置会自动生成。选中Arguments标签,在Program Arguments输入框中输入tester.sa(文件内容是“34 + 12;”,你需要自己建立这个文件,或者你可以让用户输入这些信息),点击Run按钮,然后看到你的程序执行了! 每次你修改了你的语法(在我们的例子里是simpleAdder.sable),你需要运行你的SableCC工具来重新生成parser/lexer等等。 如果只是修改了解释器,你很少需要重新编译java代码。这是SableCC最大的有点之一。 在重新生成你的语法文件之前,最好删除你之气生成的文件。否则,老代码可能会和新代码冲突。 |
相关讨论
相关资源推荐
- MC34063AD+IRS2184 DC-Motor电机驱动板ALTIUM设计硬件原理图+PCB文件.zip
- 《搜索引擎零距离》第三章 IRS虚拟机及编译器实现原理(4)
- 《搜索引擎零距离》第三章 IRS虚拟机及编译器实现原理(3)
- 《搜索引擎零距离》第三章 IRS虚拟机及编译器实现原理(1)
- 《搜索引擎零距离》第三章 IRS虚拟机及编译器实现原理(2)
- CH340 USB转IRS232串口 TTL串口protel设计硬件原理图+PCB文件.zip
- MC34063AD+IRS2184r直流电机驱动板AD设计硬件原理图+PCB文件.zip
- IR2304半桥驱动集成电路的功能原理及应用
- PHP实现深度优先搜索算法(DFS,Depth First Search)详解
- USB转串口CH340 USB转IRS232串口 TTL串口protel99SE设计硬件原理图PCB工程文件.zip