JParsec中如何在parser规则里引用lexer规则

Java 的 Parser combinator 中最有名应该是 JParsec 了。随着 Java 8 的发布,我们也可以用 lambda 表达式来写规则了。刚开始的时候我以为它跟 C# 下面的 Sprache 会比较像,API 应该很容易。结果我错了。Sprache 的思想跟《Monadic Parser Combinators》这篇论文里的提到的设计方法如出一辙,是不区分 lexer 和 parser 的。但是 JParsec 区分。如果在 parser 对应的规则里面使用 lexer 的规则,会导致异常。可以参看 ParserState 类:

@Override CharSequence characters() {
    throw new IllegalStateException(
        "Cannot scan characters on tokens.");
}


如果你在尝试用 JParsec 写规则时遇到了不知道怎么在 parser 规则中引用 lexer 规则的问题,恭喜你,看完下面的代码你会应该知道怎么做了。

整体示意图:



比如解析一段 key/value 文本:

"update": "2014-10-28"


为了解析双引号的部分写一个 lexer 规则:

private static final
Parser<Tokens.Fragment> STRING_LITERAL =
    Scanners.DOUBLE_QUOTE_STRING
            .map(patchTag(Tag.STRING))
            .label("string literal");

enum Tag {
    STRING
}

private static
Map<String, Tokens.Fragment> patchTag(Object tag) {
    return str -> new Tokens.Fragment(str, tag);
}


除 patchTag 函数和 Tag 枚举之外其它的都是 JParsec 的 API。patchTag 的作用就让 lexer 在运行的时候将 STRING_LITERAL 规则所解析出来的字符串打上 STRING 这个 tag。这样在后面的 parser 规则部分用这个 tag 将字符串规则取出来:

private final
Parser<String> stringLiteral =
    Parsers.token(withTag(Tag.STRING))
           // 除掉引号
           .map(s -> s.substring(1, s.length() - 1))
           .label("string literal");

private static TokenMap<String>
withTag(Object tag) {
    return token -> {
        Object value = token.value();
        if (value instanceof Tokens.Fragment) {
            Tokens.Fragment fragment =
                (Tokens.Fragment) value;
            if (tag.equals(fragment.tag())) {
                return fragment.text();
            }
        }
        return null;
    };
}


这样就可以做一个规则来匹配 "update": "2014-10-28" 这样的文本了:

private Parser<StatAstKeyValue> keyValue =
    Parsers.sequence(
        stringLiteral,
        OPERATORS.token(":"),
        stringLiteral,
        (k, ignored, v) ->
            new StatAstKeyValue(k, v))
    .label("keyValue");

private static final
Terminals OPERATORS =
    Terminals.operators("{", "}", ",", ":");


文本中的空格部分要求在 lexer 中额外定义一个空白字符的规则:

private static final
Parser<Void> WHITESPACES = Scanners.WHITESPACES;


为了达到忽略空白字符的目的,我们可以用 Parser#skipMany() 这样的 combinator,也可以用 JParsec 的官方教程上的做法:

private Parser<List<Token>>
TERMINALS = TOKENIZER.lexer(
    WHITESPACES.or(Parsers.always())
);


这里将 WHITESPACES.or(Parsers.always()) 作为分隔符来分隔 TOKENIZER 解析出来的 token。如果不加 Parsers.always(),会导致可以解析

"k" : "v"


但不能解析

"k": "v"


注意第 2 个例子中冒号的前面没有空格。

猜你喜欢

转载自yangdong.iteye.com/blog/2227533