解析字符串 antlr

3

我将字符串作为解析规则而不是词法分析器,因为字符串可能包含转义字符和表达式,例如"The variable is \(variable)"

string
 : '"' character* '"'
 ;

character
 : escapeSequence
 | .
 ;

escapeSequence
 : '\(' expression ')'
 ;

IDENTIFIER
 : [a-zA-Z][a-zA-Z0-9]*
 ;

WHITESPACE
 : [ \r\t,] -> skip
 ;

这种方法行不通,因为.匹配任何标记而不是任何字符,所以会匹配许多标识符并完全忽略空格。

我如何解析可能包含表达式的字符串?

查看Swift和Javascript的解析器,这两种语言都支持这样的事情,我无法弄清它们是如何工作的。据我所知,它们只输出一个字符串,例如"我的字符串中有(变量)",而不能将变量解析为自己的东西。


研究一下词法分析器模式。 - undefined
@sepp2k 我不确定它们会如何帮助,如果字符串中有一个字符串和其中的括号,那就需要将整个解析器写入标记器,除非我漏掉了什么。 - undefined
我根本不会使用词法分析器或解析器来进行字符串插值。将字符串作为一个整体进行解析,然后在后处理步骤中对插值的值进行搜索和替换。在我看来,这比发明复杂的插值语法规则要好得多。附注:在解析器中解析字符串是行不通的,因为这样会允许引号和内容之间有空格,而输出中并不存在这些空格。 - undefined
2
@MikeLischke 假设在 \() 之间允许任意表达式,那并不像这么简单。你要么在 \( 之后再次调用解析器,要么在后处理中重复大量解析器的工作。 - undefined
我猜这里最简单的“解决方案”就是使用字符串连接运算符,比如 .. - undefined
显示剩余2条评论
1个回答

4
这个问题可以通过使用词法模式来解决,其中一个模式用于字符串内部,另一个(或更多)用于外部。在外部看到"会切换到内部模式,在内部看到\("会切换回外部。唯一复杂的部分是在外部看到):有时它应该切换回内部(因为它对应于\(),有时则不应该(当它对应于普通的()。

最基本的实现方式如下:

词法分析器:

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上找到。

看起来这个可能会起作用,也有可能可以跳过为嵌入式而设置的单独模式,如果字符串中的if语句推动默认模式。 - undefined
1
问题是如果输入只是。那么对于的规则将弹出空堆栈。当然,那个代码本身就是无效的,但通常你希望得到一个正确的语法错误,而不是一个空的堆栈异常。我个人更喜欢带有操作的版本,因为它避免了第三个模式,但也避免了异常。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接