这个问题可以通过使用词法模式来解决,其中一个模式用于字符串内部,另一个(或更多)用于外部。在外部看到
"
会切换到内部模式,在内部看到
\(
或
"
会切换回外部。唯一复杂的部分是在外部看到
)
:有时它应该切换回内部(因为它对应于
\(
),有时则不应该(当它对应于普通的
(
)。
最基本的实现方式如下:
词法分析器:
lexer grammar StringLexer;
IDENTIFIER: [a-zA-Z_][a-zA-Z0-9_]* ;
DQUOTE: '"' -> pushMode(IN_STRING);
LPAR: '(' -> pushMode(DEFAULT_MODE);
RPAR: ')' -> popMode;
mode IN_STRING;
TEXT: ~[\\"]+ ;
BACKSLASH_PAREN: '\\(' -> pushMode(DEFAULT_MODE);
ESCAPE_SEQUENCE: '\\' . ;
DQUOTE_IN_STRING: '"' -> type(DQUOTE), popMode;
解析器:
parser grammar StringParser;
options {
tokenVocab = 'StringLexer';
}
start: exp EOF ;
exp : '(' exp ')'
| IDENTIFIER
| DQUOTE stringContents* DQUOTE
;
stringContents : TEXT
| ESCAPE_SEQUENCE
| '\\(' exp ')'
;
每当我们看到一个
(
或
\(
时,就会将默认模式推入栈中,并在每次看到一个
)
时弹出模式。这样,只有在栈顶的模式是字符串模式时,它才会返回到字符串内部,这种情况只有在上次
\(
后没有未关闭的
(
时才会发生。
这种方法虽然有效,但缺点是如果存在不匹配的
)
,则会导致空堆栈异常而不是正常的语法错误,因为我们正在对空堆栈调用
popMode
。
为了避免这种情况,我们可以添加一个成员来跟踪括号嵌套的深度,并且在嵌套级别为0(即堆栈为空)时不弹出堆栈:
@members {
int nesting = 0;
}
LPAR: '(' {
nesting++;
pushMode(DEFAULT_MODE);
};
RPAR: ')' {
if (nesting > 0) {
nesting--;
popMode();
}
};
mode IN_STRING;
BACKSLASH_PAREN: '\\(' {
nesting++;
pushMode(DEFAULT_MODE);
};
(我省略的部分与之前版本相同)。
这个方法可以正常工作,并会产生不匹配 )
的语法错误。然而,它包含了一些操作,因此不再是语言无关的,只有当您计划从多种语言使用语法时才会成为问题(并且根据语言的不同,您甚至可能很幸运,代码在所有目标语言中都是有效的)。
如果您想避免操作,最后一个选择是拥有三种模式:一个用于任何字符串之外的代码,一个用于字符串内部,一个用于 \()
内部。第三种几乎与第一种相同,只是在看到括号时会推入和弹出模式,而第一种则不会。为了使两种模式生成相同类型的令牌,第三种模式中的规则都将调用 type()
。代码如下:
lexer grammar StringLexer;
IDENTIFIER: [a-zA-Z_][a-zA-Z0-9_]* ;
DQUOTE: '"' -> pushMode(IN_STRING);
LPAR: '(';
RPAR: ')';
mode IN_STRING;
TEXT: ~[\\"]+ ;
BACKSLASH_PAREN: '\\(' -> pushMode(EMBEDDED);
ESCAPE_SEQUENCE: '\\' . ;
DQUOTE_IN_STRING: '"' -> type(DQUOTE), popMode;
mode EMBEDDED;
E_IDENTIFIER: [a-zA-Z_][a-zA-Z0-9_]* -> type(IDENTIFIER);
E_DQUOTE: '"' -> pushMode(IN_STRING), type(DQUOTE);
E_LPAR: '(' -> type(LPAR), pushMode(EMBEDDED);
E_RPAR: ')' -> type(RPAR), popMode;
请注意,由于不能在多个词法规则使用相同的字符文字时使用字符串字面量,因此我们现在无法在解析器语法中使用字符串字面量。因此,我们必须在解析器中使用
LPAR
而不是
'('
等等(由于相同原因,我们已经不得不为
DQUOTE
做出了这样的更改)。
由于此版本涉及大量重复(特别是随着令牌数量的增加),并且防止在解析器语法中使用字符串字面量,我通常更喜欢带有操作的版本。
所有三种选择的完整代码也可以在
GitHub上找到。
\()
之间允许任意表达式,那并不像这么简单。你要么在\(
之后再次调用解析器,要么在后处理中重复大量解析器的工作。 - undefined..
。 - undefined