【问题标题】:disambiguate tokens without using tokenizer state在不使用标记器状态的情况下消除标记的歧义
【发布时间】:2014-06-08 08:10:44
【问题描述】:

我无法让 JavaCC 通过它们在语法中的位置来正确地消除标记的歧义。我有以下 JJTree 文件(我称之为bug.jjt):

options
{
  LOOKAHEAD = 3;
  CHOICE_AMBIGUITY_CHECK = 2;
  OTHER_AMBIGUITY_CHECK = 1;
  SANITY_CHECK = true;
  FORCE_LA_CHECK = true;
}
PARSER_BEGIN(MyParser)
import java.util.*;
public class MyParser {
    public static void main(String[] args) throws ParseException {
        MyParser parser = new MyParser(new java.io.StringReader(args[0]));
        SimpleNode root = parser.production();
        root.dump("");
    }
}
PARSER_END(MyParser)

SKIP:
{
    " "
}

TOKEN:
{
     <STATE:  ("state")>
     |<PROD_NAME: (["a"-"z"])+ >
}


SimpleNode production():
{}
{
    (
        <PROD_NAME>
        <STATE>
        <EOF>
    )
    {return jjtThis;}
}

使用以下代码生成解析器代码:

java -cp C:\path\to\javacc.jar jjtree bug.jjt
java -cp C:\path\to\javacc.jar javacc bug.jj

现在编译后,您可以从命令行运行 MyParser 并使用要解析的字符串作为参数。如果成功,它会打印production,如果失败,则会发出错误。

我尝试了两个简单的输入:foo statestate state。第一个解析,但第二个不解析,因为两个state 字符串都被标记为&lt;STATE&gt;。当我将LOOKAHEAD 设置为3 时,我希望它使用语法并看到一个字符串state 必须是&lt;STATE&gt; 而另一个必须是&lt;PROD_NAME。然而,没有这样的运气。我尝试更改各种前瞻参数无济于事。我也无法使用标记器状态(您可以在其中定义不同状态下允许的不同标记),因为此示例是更复杂系统的一部分,可能会有很多此类歧义。

谁能告诉我如何在不使用标记器状态的情况下使 JavaCC 正确地消除这些标记的歧义?

【问题讨论】:

    标签: java parsing tokenize javacc


    【解决方案1】:

    这在问题 4.19 下的FAQ 中有介绍。

    这里概述了三种策略

    将选择放入语法中。参见 Bart Kiers 的回答。

    使用语义前瞻。对于这种方法,您可以摆脱定义 STATE 的产生式并像这样编写语法

    void SimpleNode production():
    {}
    {
        (
            <PROD_NAME>
            ( LOOKAHEAD({getToken(1).kind == PROD_NAME && getToken(1).image.equals("state")})
             <PROD_NAME>
             ...
            |
             ...other choices...
            )
        )
        {return jjtThis;}
    }
    

    如果没有其他选择,那么

    void SimpleNode production():
    {}
    {
        (
            <PROD_NAME>
            ( LOOKAHEAD({getToken(1).kind == PROD_NAME && getToken(1).image.equals("state")})
             <PROD_NAME>
             ...
            |
             { int[][] expTokSeqs = new int[][] { new int[] {STATE } } ;
               throw new ParseException(token, expTokSeqs, tokenImage) ; }
            )
        )
        {return jjtThis;}
    }
    

    但是,在这种情况下,您需要 STATE 的产生式,正如 expTokSeqs 的初始化中提到的那样。所以你需要一个产品。

    < DUMMY > TOKEN : { < STATE : "state" > }
    

    DUMMY 是一个永远不会去的状态。

    使用词汇状态。 OP 问题的标题表明他不想这样做,但不是为什么。如果状态切换可以包含在令牌管理器中,则可以完成。假设一个文件是一系列产品,每个产品看起来像这样。

    name state : "a" | "b" name ;
    

    也就是说,它以名称开头,然后是关键字“state”一个冒号,一些标记,最后是一个分号。 (我只是在编造这个,因为我不知道 OP 试图解析哪种语言。)然后你可以使用三个词法状态 DEFAULT、S0 和 S1。

    • 在 DEFAULT 中,任何字母序列(包括“状态”)都是 PROD_NAME。在 DEFAULT 中,识别 PROD_NAME 会将状态切换到 S0。
    • 在 S0 中,除“state”外的任何字母序列都是 PROD_NAME,“state”是 STATE。在 S0 中,识别 STATE 令牌会导致分词器切换到状态 S1。
    • 在 S1 中,任何字母序列(包括“状态”)都是 PROD_NAME。在 S1 中,识别分号会将状态切换为 DEFAULT。

    所以我们的例子是这样标记的

    name  state  : "a" | "b" name ;
    |__||______||_________________||_________
    DEF-   S0             S1                    DEFAULT
    AULT
    

    作品是这样写的

    <*> SKIP: { " " }
    
    <S0> TOKEN: { <STATE: "state"> : S1 }
    
    <DEFAULT> TOKEN:{ <PROD_NAME: (["a"-"z"])+ >  : S0 }
    <S0,S1> TOKEN:{ <PROD_NAME: (["a"-"z"])+ >  }
    
    <S1> TOKEN: { <SEMICOLON : ";" > : DEFAULT
    <S0, DEFAULT> TOKEN : { <SEMICOLON : ";" > }
    
    <*> TOKEN {
         COLON : ":"
    |    ...etc...
    }
    

    解析器可以将状态切换命令发送回分词器,但要使其正确且脆弱是很棘手的。请参阅 FAQ 的问题 3.12。

    【讨论】:

    • 啊,前瞻关键字更有意义。如果前瞻失败并且没有任何其他选项,为什么我需要抛出错误?解析器不应该处理前瞻未找到任何内容时会发生什么吗?
    • 你是对的。但是,当在别无选择的情况下使用语义前瞻时,错误消息对用户的帮助不大。我建议的方式给出了更好的错误信息。我将通过添加行号和列号来编辑答案以进一步改进错误消息。
    【解决方案2】:

    Lookahead 与词法分析器无关,因为它将字符组合成标记。当它匹配由终端(令牌)组成的非终端时,解析器会使用它。

    如果您将“状态”定义为产生一个令牌状态,那么它就是这样。

    我同意你的观点,标记器状态不是允许将关键字用作标识符的好解决方案。这真的有必要吗? HLL 不允许这样做是有充分理由的。

    OTOH,如果您可以仅使用 &lt;PROD_NAME&gt;s 重写您的语法,您可能会在语义分析期间推迟对关键字的识别。

    【讨论】:

    • 啊,是的,我现在明白了。我没有意识到标记化和解析如此分离,这似乎是一个障碍,但可能有助于提高性能,对吧?所以答案是使用更通用的标记&lt;PROD_NAME&gt;,然后当它应该是&lt;STATE&gt;时手动验证值。
    • 对不起,你知道怎么做吗?我需要一个reject 方法或其他东西。 if(!t.image.equals("state"){reject parse here}.
    • 我认为将您推荐给常见问题解答 4.19 的答案应该可以帮助您克服所有困难。
    【解决方案3】:

    LOOKAHEAD 选项仅适用于解析器(生产规则)。标记器不受此影响:它会生成标记,而不用担心生产规则试图匹配什么。输入"state" 将始终被标记为STATE,即使解析器试图匹配PROD_NAME

    你可以做这样的事情(未经测试的伪语法代码!):

    SimpleNode production():
    {}
    {
        (
            prod_name_or_state()
            <STATE>
            <EOF>
        )
        {return jjtThis;}
    }
    
    SimpleNode prod_name_or_state():
    {}
    {
        (   
            <PROD_NAME>
        |   <STATE>
        )
        {return jjtThis;}
    }
    

    这将匹配"foo state""state state"

    或等效的,但更紧凑:

    SimpleNode production():
    {}
    {
        (
            ( <PROD_NAME> | <STATE> )
            <STATE>
            <EOF>
        )
        {return jjtThis;}
    }
    

    【讨论】:

    • 对,但是 不包含在原始语法中。
    • @laune,我发布的规则不允许&lt;PROD_NAME&gt; &lt;PROD_NAME&gt;。它匹配&lt;PROD_NAME&gt; &lt;STATE&gt;&lt;STATE&gt; &lt;STATE&gt;
    • 抱歉,&lt;STATE&gt; &lt;STATE&gt; 不在原始语法中。
    • @laune,这是正确的,但正如他所指出的,OP 确实想要解析输入 "state state"。将其解析为 &lt;STATE&gt; &lt;STATE&gt; 并不重要:在进一步处理 AST 时,只需将匹配 prod_name_or_state() 的令牌视为 &lt;PROD_NAME&gt;
    • 这意味着所有语义操作都必须重复。我从 OP 的 Q 中假设,这不是允许“关键字”作为标识符的唯一情况,所以这不是一个非常令人愉快的选择。 - 从技术上讲,您是正确的,但它会产生一个“好”的解决方案 - 我们在不了解整体情况的情况下无法判断?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-12-08
    • 2013-05-20
    • 2017-06-29
    • 1970-01-01
    • 1970-01-01
    • 2020-07-13
    • 1970-01-01
    相关资源
    最近更新 更多