0

例如,考虑以下语法:

source_file: $ => $._expression,

_expression: $ => choice(
  $.identifier,
  $.operator
),

identifier: $ => /\w*[A-Za-z]\w*/,

operator: $ => seq(
  repeat1(seq($._expression, '\\X')),
  $._expression
)

因此,如果我有输入字符串a \X b \X c \X d,我希望它匹配为:

(source_file
  (operator
    (identifier)
    (identifier)
    (identifier)
    (identifier)))

但是,我实际上可以获得此行为的唯一方法是执行以下操作:

operator: $ => choice(
  $._operator_2,
  $._operator_3,
  ...
),

_operator_2: $ => prec(1, seq(
  $._expression, '\\X', $._expression)
),

_operator_3: $ => prec(2, seq(
  $._expression, '\\X', $._expression, '\\X', $._expression)
),

...

所以我必须硬编码所有的表达式长度,随着长度的增加优先级增加,并且无法弄清楚如何编写一个包罗万象的_operator_n规则。我该如何做到这一点?指定冲突然后分配动态优先级的某种组合?

4

1 回答 1

-1

答案就在保姆的conflicts领域;来自文档

冲突 - 规则名称数组的数组。每个内部数组表示一组规则,这些规则涉及旨在存在于语法中的 LR(1) 冲突。当这些冲突在运行时发生时,Tree-sitter 将使用 GLR 算法来探索所有可能的解释。如果多个解析最终成功,则 Tree-sitter 将选择其相应规则具有最高总动态优先级的子树。

考虑以下标记序列:

a \X b \X c

显然,这个字符串有多种可能的解释,例如你想要的那个:

(source_file
  (operator
    (identifier a)
    (identifier b)
    (identifier c)
  )
)

或左结合:

(source_file
  (operator
    (operator
      (identifier a)
      (identifier b)
    )
    (identifier c)
  )
)

或右结合:

(source_file
  (operator
    (identifier a)
    (operator
      (identifier b)
      (identifier c)
    )
  )
)

请注意,可能的解释数量呈指数增长,每增加一个运算符就会加倍。

因此,如果没有进一步的方向,tree-sitter 将正确地将其识别为本地 LR(1) 冲突,这意味着它无法在逐个标记读取字符串标记时仅使用一个前瞻标记来确定哪种解释是正确的。实际上这是一个全面的非局部歧义,因此为了解决这个问题,我们可以告诉 tree-sitter 使用冲突字段从 LR 切换到 GLR 解析算法:

conflicts: $ => [
  [$.operator, $.operator]
]

GLR 算法不确定地跟踪所有可能的解析解释,然后(如上面的冲突文档中所述)使用具有最高总动态优先级的解释。所以,我们要做的就是巧妙地利用prec.dynamic注解让 tree-sitter 识别出我们想要的解释得分最高!我们如何做到这一点?

这有点棘手,但是通过上面的解释我们可以看到我们想要最小化解析中的运算符规则的数量。因此,我们可以在运算符的动态优先级上添加一个惩罚(负数)!

operator: $ => prec.dynamic(-1, seq(
  repeat1(seq($._expression, '\\X')),
  $._expression
))

有用!不幸的是,使用这个解决方案,我们打开了潘多拉魔盒:向语言中添加任何额外的中缀运算符变得非常复杂,因为您必须同时处理它们之间的静态优先级以及与此运算符组合的动态优先级。将运算符指定为高优先级的标准左关联运算符进行解析可能更容易,然后在语义级别处理此展平。

于 2021-04-03T14:26:54.113 回答