【问题标题】:parsing maxscript - problem with newlines解析 maxscript - 换行问题
【发布时间】:2020-02-03 23:30:50
【问题描述】:

我正在尝试使用他们的官方grammar description 语言为 MAXScript 语言创建解析器。我使用 flexbison 来创建词法分析器和解析器。

但是,我遇到了以下问题。在传统语言(例如 C)中,语句由特殊标记(C 中的;)分隔。但在 MAXScript 中,复合表达式内的表达式可以用;newline 分隔。还有其他语言在其解析器中使用空格字符,例如 Python。但是Python对newline的放置要严格得多,Python中的以下程序无效:

# compile error
def 
foo(x):
  print(x)

# compile error
def bar
(x):
  foo(x)

但是在 MAXScript 中以下程序是有效的:

fn
foo x =
(              // parenthesis start the compound expression
   a = 3 + 2;  // the semicolon is optional
   print x
)

fn bar
x =
   foo x

你甚至可以这样写:

for
x
in
#(1,2,3,4)
do
format "%," x

这将评估罚款并将1,2,3,4, 打印到输出。所以newlines 可以插入很多地方,没有特殊意义。

但是,如果您像这样在程序中再插入一个newline

for
x
in
#(1,2,3,4)
do
format "%,"
x

您将收到运行时错误,因为format 函数需要传递多个参数。

这是我拥有的野牛输入文件的一部分:

expr:
    simple_expr
|   if_expr
|   while_loop
|   do_loop
|   for_loop
|   expr_seq

expr_seq:
    "(" expr_semicolon_list ")"

expr_semicolon_list:
    expr
|   expr TK_SEMICOLON expr_semicolon_list
|   expr TK_EOL expr_semicolon_list

if_expr:
    "if" expr "then" expr "else" expr
|   "if" expr "then" expr
|   "if" expr "do" expr

// etc.

这将仅解析仅使用newline 作为表达式分隔符的程序,并且不会期望newlines 分散在程序的其他地方。

我的问题是:有没有办法告诉野牛将令牌视为可选令牌?对于野牛,这意味着:

  • 如果您找到newline 令牌并且您可以使用它进行转换或减少,那么就这样做。
  • 否则只需丢弃newline 令牌并继续解析。

因为如果没有办法做到这一点,我能想到的唯一其他解决方案是修改野牛语法文件,以便它在任何地方都需要那些newlines。并提高 newline 充当表达式分隔符的规则的优先级。像这样:

%precedence EXPR_SEPARATOR   // high precedence

%%

// w = sequence of whitespace tokens
w:  %empty    // either nothing
|   TK_EOL w  // or newline followed by other whitespace tokens

expr:
    w simple_expr w
|   w if_expr w
|   w while_loop w
|   w do_loop w
|   w for_loop w
|   w expr_seq w

expr_seq:
    w "(" w expr_semicolon_list w ")" w

expr_semicolon_list:
    expr
|   expr w TK_SEMICOLON w expr_semicolon_list
|   expr TK_EOL w expr_semicolon_list           %prec EXPR_SEPARATOR

if_expr:
    w "if" w expr w "then" w expr w "else" w expr w
|   w "if" w expr w "then" w expr w
|   w "if" w expr w "do" w expr w

// etc.

但是这看起来很丑陋且容易出错,如果可能的话,我想避免这样的解决方案。

【问题讨论】:

    标签: parsing bison maxscript


    【解决方案1】:

    我的问题是:有没有办法告诉野牛将令牌视为可选令牌?

    不,没有。 (请参阅下面的图表以获得更长的解释。)

    不过,解决方法并不像您想象的那么难看,尽管它并非没有问题。

    为了简化事情,我将假设无论程序文本中出现多少连续换行符,包括分散有 cmets 的情况,词法分析器都可以被说服只生成一个 '\n' 标记在空白行之间。这可以通过复杂的正则表达式来实现,但更简单的方法是使用开始条件来抑制 \n 标记,直到遇到常规标记。词法分析器的初始启动条件应该是抑制换行标记的条件,这样程序文本开头的空白行就不会混淆任何内容。

    现在,关键的见解是我们不必在整个语法中插入“可能是换行符”标记,因为每个换行符都必须出现在某个真正的标记之后。这意味着我们可以为每个终端添加一个非终端:

    tok_id: ID | ID '\n'
    tok_if: "if" | "if" '\n'
    tok_then: "then" | "then" '\n'
    tok_else: "else" | "else" '\n'
    tok_do: "do" | "do" '\n'
    tok_semi: ';' | ';' '\n'
    tok_dot: '.' | '.' '\n'
    tok_plus: '+' | '+' '\n'
    tok_dash: '-' | '-' '\n'
    tok_star: '*' | '*' '\n'
    tok_slash: '/' | '/' '\n'
    tok_caret: '^' | '^' '\n'
    tok_open: '(' | '(' '\n'
    tok_close: ')' | ')' '\n'
    tok_openb: '[' | '[' '\n'
    tok_closeb: ']' | ']' '\n'
    /* Etc. */
    

    现在,这只是用上面定义的相应非终端替换终端的使用的问题。 (不需要w 非终端。)一旦我们这样做,bison 将在刚刚添加的非终端定义中报告一些移位减少冲突;任何可以出现在表达式末尾的终端都会引发冲突,因为换行符可以被终端的非终端包装器或expr_semicolon_list 产生式吸收。我们希望换行符成为expr_semicolon_list 的一部分,因此我们需要添加以换行符开头的优先级声明,使其优先级低于任何其他标记。

    这很可能适用于您的语法,但并非 100% 确定。基于优先级的解决方案的问题在于,它们可能具有隐藏真正的减少班次冲突问题的效果。所以我建议在语法上运行野牛,并在添加优先声明之前验证所有移位减少冲突是否出现在预期的位置(在包装产品中)。


    为什么令牌回退不像看起来那么简单

    理论上,可以实现与您建议的功能相似的功能。 [注1]

    但这并不重要,因为 LALR 解析器构造算法组合状态的方式。结果是解析器可能不“知道”先行标记在完成一个或多个归约之前不能移动。因此,当它发现前瞻令牌无效时,它已经执行了缩减,必须撤消这些缩减才能在没有前瞻令牌的情况下继续解析。

    如果该标记的状态中的默认操作是减少,则大多数解析器生成器通过删除与前瞻标记对应的错误操作来复杂化问题。其效果还是将错误检测延迟到一次或多次徒劳的减少之后,但它的好处是显着减少了转换表的大小(因为不需要显式存储默认条目)。由于延迟错误将在消耗更多输入之前检测到,因此延迟通常被认为是可以接受的。 (不过,Bison 可以选择阻止这种优化。)

    作为一个实际的例子,这里有一个非常简单的表达式语法,只有两个运算符:

    prog: expr '\n' | prog expr '\n'
    expr: prod      | expr '+' prod
    prod: term      | prod '*' term
    term: ID        | '(' expr ')'
    

    这导致了这个状态图[注2]:

    假设我们想以 Python 方式忽略换行符,允许输入

    ( 
      a + b
    )
    

    这意味着解析器必须忽略b 之后的换行符,因为输入可能是

    (
      a + b
      * c
    )
    

    (这在 Python 中很好,但在 MAXScript 中,如果我理解正确的话。)

    当然,如果输入没有括号,换行符将被识别为语句分隔符:

    a + b
    

    查看状态图,我们可以看到在读取b 后,无论表达式是否带括号,解析器最终都会进入状态15。在该状态下,换行符被标记为归约操作的有效前瞻,因此将执行归约操作,大概为总和创建一个 AST 节点。只有在这种减少之后,解析器才会注意到换行没有任何动作。如果它现在丢弃换行符,那就太晚了;现在没有办法减少 b * c 以使其成为和的操作数。

    Bison 确实允许您请求不合并状态的规范 LR 解析器。结果,状态机变得非常非常大。以至于 Canonical-LR 对于非玩具语法仍然被认为是不切实际的。在上面简单的双运算符表达式语法中,请求一个 Canonical LR 解析器只会将状态计数从 16 增加到 26,如下所示:

    在 Canonical LR 解析器中,减少 term: term '+' prod 有两种不同的状态。状态 16 适用于顶级,因此前瞻包括换行符但不包括 ) 在括号内,解析器将改为到达状态 26,其中 ) 是有效的前瞻,但换行符不是。因此,至少在某些语法中,使用 Canonical LR 解析器可以使预测更加精确。但是依赖于使用庞大的解析自动机的功能并不是特别实用。

    另一种方法是解析器通过首先模拟归约操作来对换行做出反应,以查看转换是否最终会成功。如果您请求前瞻校正 (%define parse.lac full),bison 将插入代码来精确执行此操作。这段代码会产生很大的开销,但很多人还是要求它,因为它使详细的错误消息更加准确。因此,当然可以重新利用这段代码来进行令牌回退处理,但据我所知,实际上没有人这样做。

    注意事项:

    1. 一个类似的问题不时出现,如果无法转移令牌,您是否可以告诉 bison 将令牌重新分类为备用令牌。 (这对于解析像 SQL 这样有很多非保留关键字的语言很有用。)

    2. 我使用 Bison 的 -g 选项生成了状态图:

      bison -o ex.tab.c --report=all -g ex.y
      dot -Tpng -oex.png ex.dot
      

      为了生成 Canonical LR,我将 lr.type 定义为 canonical-lr

      bison -o ex_canon.c --report=all -g -Dlr.type=canonical-lr ex.y
      dot -Tpng -oex_canon.png ex_canon.dot
      

    【讨论】:

    • 感谢您提供如此详细的回答。我会尝试按照您的建议包装令牌。
    • @jeysym:我编写了一个概念验证语法,它比完整的语言小很多,它导致几个包装器被标记为无用的规则。回想起来,这并不奇怪。这些终端永远不会出现在可以忽略换行符的上下文中(在我的简单语言子集中)。我不知道完整语言是否仍然如此。无论如何,您可以忽略警告,或者您不能包装引发它的终端。
    • 我明天会编辑答案,也许会附上一个注释。
    猜你喜欢
    • 1970-01-01
    • 2017-07-02
    • 1970-01-01
    • 2020-10-18
    • 2017-01-24
    • 1970-01-01
    • 2020-10-22
    • 2011-11-23
    • 1970-01-01
    相关资源
    最近更新 更多