在 tree-sitter 语法中,如何以非关联方式匹配非分隔值列表?

问题描述

例如,考虑以下语法:

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,$._expression,...

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

解决方法

答案在于 tree-sitter 的 conflicts 字段;来自the docs

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
))

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