第一个例子,我们做一个花括号的匹配,控制台输入一组左大括号,后面跟着相同数量的右大括号,最后是零个或多个行结束符(回车),最后是文件结束符(ctrl+d)。
合法字符串的示例:
"{}", "{{{{{}}}}}", ...
非法字符串的例子:
"{{{{", "{}{}", "{}}", "{{}{}}", ...
第一个实例的代码如下:
PARSER_BEGIN(Simple)
package com.github.gambo.javacc.simple;
/** Simple brace matcher. */
public class Simple {
/** Main entry point. */
public static void main(String args[]) throws ParseException {
Simple parser = new Simple(System.in);
parser.Input();
}
}
PARSER_END(Simple)
/** Root production. */
void Input() :
{}
{
MatchedBraces() ("\n"|"\r")* <EOF>
}
/** Brace matching production. */
void MatchedBraces() :
{}
{
"{" [ MatchedBraces() ] "}"
}
终结符,非终结符和产生式
JavaCC输入语法中的token(正则表达式)要么是简单的字符串,或者更复杂的正则表达式。我们看到input方法里有一个这样的正则表达式“<EOF>”,它匹配文件结束符。所有复杂正则表达式都包含在尖括号内。而这种代表具体的符号正则表达式我们称之为“终结符”,("\n"|"\r")则可以归为此类。
而上例的两个方法Input和MatchedBraces则可以称之为非终结符。其可以被定义为一组终结符和/或其他非终结符所组成序列而形成的语法。
非终结符各在后面定义了一个大括号,这个大括号内一般包含了一堆声明和语句,这些声明和语句作为通用声明和语句生成到生成的方法中。在上面的例子中,没有任何声明,因此显示为大括号是空的。后面继续跟着一个大括号括起来的拓展。
非终结符“Input”可以展开为非终结符“matchedbrace”,后面跟着零个或多个行结束符("\n"或"\r"),然后是文件结束符。
而非终结符“matchedbrace”展开以为token“{”开头,后面是可选的嵌套“matchedbrace”,最后以token“}”结尾。方括号[xxx]在JavaCC输入文件中表示xxx是可有可无的。也可以写成 (xxx)?。这两种形式是等价的。非终结符中的元素还可以定义为以下结构:
e1 | e2 | e3 | ... : e1, e2, e3, 等当中选择一个.
( e )+ :e出现一次或多次
( e )* : e出现0次或多次
这些结构也可以彼此嵌套,所以我们可以得到这样的结构:
(( e1 | e2 )* [ e3 ] ) | e4
而上述非终结符 Input
和 MatchedBraces
定义出的整个语法规则称之为产生式,其包含了非终结符的定义、变量的定义和逻辑的描述。
构建解析器
要构建这个解析器,只需在这个文件上运行JavaCC命令并编译生成的Java文件:
javacc Simple.jj
javac *.java
由于我们搭建了ant环境,可以按照上一节的方法构建解析器。构建操作和生成的代码如下:
接下来我们运行生成的解析器验证一下结果
正确运行结果如下:
{{{}}}
^D
Disconnected from the target VM, address: '127.0.0.1:54377', transport: 'socket'
错误实例的运行结果如下:
{{{}}}}}}{}
Exception in thread "main" com.github.gambo.javacc.simple.ParseException: Encountered " "}" "} "" at line 1, column 7.
Was expecting one of:
<EOF>
"\n" ...
"\r" ...
at com.github.gambo.javacc.simple.Simple.generateParseException(Simple.java:249)
at com.github.gambo.javacc.simple.Simple.jj_consume_token(Simple.java:187)
at com.github.gambo.javacc.simple.Simple.Input(Simple.java:44)
at com.github.gambo.javacc.simple.Simple.main(Simple.java:11)
接下来我们对上面的解析器做一个拓展,使其支持空格字符穿插在大括号之间:
"{{ }\n}\n\n"这将是合法的。代码如下:
词法规范
PARSER_BEGIN(Simple2)
package com.github.gambo.javacc.simple;
/** Simple brace matcher. */
public class Simple2 {
/** Main entry point. */
public static void main(String args[]) throws ParseException {
Simple2 parser = new Simple2(System.in);
parser.Input();
}
}
PARSER_END(Simple2)
SKIP :
{
" "
| "\t"
| "\n"
| "\r"
}
/** Root production. */
void Input() :
{}
{
MatchedBraces() <EOF>
}
/** Brace matching production. */
void MatchedBraces() :
{}
{
"{" [ MatchedBraces() ] "}"
}
这里比之前的实例多了一个以“SKIP”开头的区域。在这个区域中有4个正则表达式——空格、制表符、换行符和return。这表示忽略这些正则表达式匹配到的部分。因此,无论何时遇到这4个字符中的任何一个,它们都会被丢弃。
除了SKIP之外,JavaCC还有另外三个词法规范:
- TOKEN: 这用于指定词法标记。
- SPECIAL_TOKEN: 这用于指定在解析过程中要忽略的词法令牌。从这个角度上看SPECIAL_TOKEN与SKIP相同。但是,可以在解析器操作中恢复这些令牌,以便进行适当处理。
- MORE: 这指定了部分令牌。一个完整的令牌由一系列MORE组成,后面跟着一个token或SPECIAL_TOKEN。
SPECIAL_TOKEN和MORE我们会在后面的章节做介绍。
我们进一步对上面的例子做一个拓展使其计算匹配大括号的数量。
PARSER_BEGIN(Simple)
package com.github.gambo.javacc.simple;
/** Simple brace matcher. */
public class Simple {
/** Main entry point. */
public static void main(String args[]) throws ParseException {
Simple parser = new Simple3(System.in);
parser.Input();
}
}
PARSER_END(Simple)
SKIP :
{
" "
| "\t"
| "\n"
| "\r"
}
TOKEN :
{
<LBRACE: "{">
| <RBRACE: "}">
}
/** Root production. */
void Input() :
{ int count; }
{
count=MatchedBraces() <EOF>
{ System.out.println("The levels of nesting is " + count); }
}
/** Brace counting production. */
int MatchedBraces() :
{ int nested_count=0; }
{
<LBRACE> [ nested_count=MatchedBraces() ] <RBRACE>
{ return ++nested_count; }
}
这里比上个示例又多了个TOKEN定义。
在JavaCC的词法规范中,TOKEN
是用来定义终结符的部分,它由一组终结符的定义组成。每个终结符定义都由一个或多个词法单元组成,这些词法单元可以是一个字符串、一个正则表达式,或是其他的一些定义方式。本例中使用TOKEN
定义了两个终结符,分别是LBRACE
和RBRACE代表
左花括号和右花括号。并且用尖括号括起来。
这样的标记规范用于一般用于复杂的正则标记,对于简单的token一般保持原样。
本例中需要计算匹配大括号的数量。使用了声明区域来声明变量"count"和"nested_count"。注意非终结符“matchedbrace”如何将其值作为函数返回值返回。