从上图可以看出javacc在解析数据流的核心是token manager,它涵盖了词法状态(Lexical States)和词法动作(Lexical Actions)。
词法状态(Lexical States)
JavaCC词法规范被组织成一组词法状态,token manager随时处于这些词法状态之一。 当token manager初始化时,默认是' DEFAULT '状态。在构造令牌管理器对象时,也可以指定初始的词法状态。
如前面的文章所述,javacc有4种不同类型的正则表达式来定义词法规范,这里我们回忆一下:
Type | Action |
---|---|
SKIP | 简单地丢弃匹配的字符串(在执行任何词法操作之后). |
MORE | 继续到下一个状态,带上匹配的字符串。这个字符串将是新匹配字符串的前缀。 |
TOKEN | 使用匹配的字符串创建一个令牌,并将其发送给解析器(或任何调用者)。 |
SPECIAL_TOKEN | 创建不参与解析的特殊令牌。 |
而有些时候我们是需要定义多组词法规范的,每套词法规范都包含不同的TOKEN,SIKP....,这个时候我们就需要使用词法状态来帮我们做管理。下面的例子展示了当遇到不同的字符串切换的不同词法状态:
<DEFAULT> MORE : { "a" : S1 }
<S1> MORE :
{
"b"
{ int l = image.length()-1; image.setCharAt(l, image.charAt(l).toUpperCase()); }
^1 ^2
: S2
}
<S2> TOKEN :
{
"cd" { x = image; } : DEFAULT
^3
}
如上所述,token manager在任何时刻都只处于一种状态。此刻,令牌管理器只考虑在此状态下定义的正则表达式进行匹配。 在匹配之后, 可以指定要执行的操作以及要移动到的新词法状态。如果没有指定新的词法状态,令牌管理器将保持当前状态。
所有处于当前词法状态的正则表达式都被认为是潜在的匹配候选。token manager使用输入流中匹配这些正则表达式的最大字符数。也就是说,token manager遵循最长匹配原则。 如果有多个长度相同的最长匹配,匹配的正则表达式是语法文件中出现顺序最早的正则表达式
词法动作(Lexical Actions)
所谓的词法动作就是当一个正则表达式成功匹配时要采取的操作:匹配正则表达式后,执行词法动作。
' TOKEN_MGR_DECLS '区域(见下文)中声明的所有变量和方法都可以在这里使用。此外,下面列出的变量和方法也可以使用。 之后,令牌管理器立即将状态更改为指定的状态(如果有的话)。
之后,执行由正则表达式类型指定的操作(' SKIP ', ' MORE '等)。如果类型是' TOKEN ',则返回匹配的TOKEN。 如果类型是' SPECIAL_TOKEN ',匹配的token会被保存并与下一个匹配的' token '一起返回。
动作变量
以下变量可用于词法动作:
StringBuffer image (READ/WRITE)
int lengthOfMatch (READ ONLY)
int curLexState (READ ONLY)
inputStream (READ ONLY)
Token matchedToken (READ/WRITE)
void SwitchTo(int)
我们逐一作一个了解:
image 是一个' StringBuffer ',包含自上次' SKIP ', ' TOKEN '或' SPECIAL_TOKEN'之后匹配的所有字符。 只要不将其赋值为“null”,可以自由地对其进行任何更改,因为生成的token manager也使用这个变量。
如果你修改image, 此更改将传递给后续匹配(如果当前匹配的是“MORE”)。' image '的内容不会自动分配到匹配的token的' image '字段。 也就是说即便你改变了image变量,其token的image字段也不会有任何改变,image字段只能做临时改变,并传递给后面的匹配。
在上面的例子中,' image '在' ^1 ',' ^2 '和' ^3 '标记的3个点上的值为:
At ^1: "ab"
At ^2: "aB"
At ^3: "aBcd"
lengthOfMatch 只读变量,代表
当前匹配的长度,其并不会对MORE上匹配的长度进行累加!
使用与上面相同的例子, lengthOfMatch 的值是:
At ^1: 1 (the size of "b")
At ^2: 1 (does not change due to lexical actions)
At ^3: 2 (the size of "cd")
curLexState 只读变量,
tokenManager在解析数据流的过程中,会在各种词法状态之间转换,这个变量可以获取tokenManager当前词法状态的索引,具体可以看javacc代码生成后的xxxConstants。
public interface SimpleConstants {
/** End of File. */
int EOF = 0;
/** RegularExpression Id. */
int LBRACE = 5;
/** RegularExpression Id. */
int RBRACE = 6;
/** Lexical state. */
int DEFAULT = 0;
/** Literal token values. */
String[] tokenImage = {
"<EOF>",
"\" \"",
"\"\\t\"",
"\"\\n\"",
"\"\\r\"",
"\"{\"",
"\"}\"",
};
}
inputSteam 只读变量tokenManager的
输入流,取决于javacc语法文件options区域的选项' UNICODE_INPUT '和' JAVA_UNICODE_ESCAPE '的值,它是以下类型之一:
ASCII_CharStream
ASCII_UCodeESC_CharStream
UCode_CharStream
UCode_UCodeESC_CharStream
流当前位于本次匹配消耗的最后一个字符处。可以调用' inputStream '的方法。例如,可以调用' getEndLine '和' getEndColumn '来获取当前匹配的行号和列号信息。
matchToken (读/写)这个变量只能在与' TOKEN '和' SPECIAL_TOKEN '正则表达式相关的操作中使用。之前的image变量即便修改也无法改变其token的image字段。这里可以将变量' image '的值赋给'matchedToken.image'。
如果我们修改上面例子的最后一个正则表达式规范为:
<S2> TOKEN :
{
"cd" { matchedToken.image = image.toString(); } : DEFAULT
}
然后返回给解析器的token将其' image '字段设置为' aBcd '。 如果这个赋值没有被执行,那么'。Image '字段将保持为' abcd '。
SwitchTo(int) 调用此方法将切换到指定的词法状态,可结合curLexState变量切换到其他的状态,在生成的tokenManager中可以看到该方法的使用,在词法动作中也可以使用,但是要谨慎,容易产生bug。
如果使用':state '语法指定了一个状态变化,它会覆盖所有的' switchTo '调用,因此,当明确指定了状态变化时,在ACTION中调用“switchTo”是没有意义的。
{
token_source.SwitchTo(name_of_state);
}
访问类级别声明
词法动作可以访问一组类级别声明。这些声明使用以下语法在JavaCC文件中引入:
TOKEN_MGR_DECLS :
{
int stringSize;
}
MORE :
{
"\"" {stringSize = 0;} : WithinString
}
<WithinString> TOKEN :
{
<STRLIT: "\""> {System.out.println("Size = " + stringSize);} : DEFAULT
}
<WithinString> MORE :
{
<~["\n","\r"]> {stringSize++;}
}
SPECIAL_TOKEN
特殊令牌与令牌类似,不同的是它们被允许出现在输入文件的任何地方(任何两个令牌之间),
可作如下定义:
SPECIAL_TOKEN :
{
<SINGLE_LINE_COMMENT: "//" (~["\n","\r"])* ("\n"|"\r"|"\r\n")>
}
任何被定义为' SPECIAL_TOKEN '的正则表达式都可以从词汇和语法规范中的用户操作中以特殊的方式访问。 这允许在解析期间恢复这些标记,同时这些标记不参与解析。
嗯!这段解释出自官方文档的直译,基本上不能算是人话。
官方有一个示例,可大略窥其一斑。
我们可以观察到,jj文件编译后生成的代码里,Token.class里有这样一个属性:
注释如下:
此字段用于访问在此TOKEN之前,但在紧接的常规TOKEN之后出现的SPECIAL_TOKEN 。如果没有这样的SPECIAL_TOKEN ,该字段将被设置为空。当有多个这样的SPECIAL_TOKEN 时,该字段引用这些SPECIAL_TOKEN 中的最后一个,而最后一个又通过其specialToken字段引用下一个前一个SPECIAL_TOKEN ,以此类推,直到第一个SPECIAL_TOKEN (其specialToken字段为空)。
假设你想打印普通TOKEN' t '之前的所有的SPECIAL_TOKEN (但只打印那些在' t '之前的普通TOKEN之后的SPECIAL_TOKEN):
if (t.specialToken == null) {
return;
}
// 回溯specialToken链,直到到达前一个常规token之后的第一个specialToken
Token tmp_t = t.specialToken;
while (tmp_t.specialToken != null) {
tmp_t = tmp_t.specialToken;
}
// 现在向前遍历specialToken链,并在此过程中打印它们
while (tmp_t != null) {
System.out.println(tmp_t.image);
tmp_t = tmp_t.next;
}
编程语言的编码格式一般如下:
public void setName(String n) {
name = n;
}
/**
* {@inheritDoc}
* @see org.javacc.examples.jjtree.eg2.SimpleNode#toString()
*/
public String toString() {
return "Identifier: " + name;
}
普通token “}”和“public ”中间有一大段注释,这些注释我们不会产生任何动作,但是我们需要将其输出到生成的文件中,所以当我们获取一个token时可以使用其specialToken属性,获取它和上一个token中间的所有注释,并将其打印,这是specialToken的一个典型用法。