#parse-error #parser #token #node #syntax #infrastructure #rome

rome_parser

Rome解析器的共享基础设施

2个版本

0.0.1 2023年4月5日
0.0.0 2022年1月26日

#8 in #rome


3 crate中使用

MIT 许可证

475KB
10K SLoC

编写解析规则

这是一份关于使用Rome解析器基础设施实现解析规则的简要或详细指南。

命名

约定是在您的解析规则前缀中使用 parse_,然后使用语法文件中定义的名称。

例如,parse_for_statementparse_expression

签名

大多数解析规则只接受一个参数,即对解析器的 &mut 引用,并返回一个 ParsedSyntax

fn parse_rule_name(&mut: Parser) -> ParsedSyntax {}

如果需要,您可以在函数中添加额外的参数。在极少数情况下,您可能需要考虑返回 ConditionalParsedSyntax,如条件语法中所述。

解析单个节点

假设您想解析JS if 语句

JsIfStatement =
 if
 (
 test: JsAnyExpression
 )
 consequent: JsBlockStatement
 else_clause: JsElseClause?

存在测试

现在,解析函数必须首先测试解析器是否定位在 if 语句上,如果不是,则返回 Absent

if !p.at(T![if]) {
 return ParsedSyntax::Absent;
}

为什么返回 ParsedSyntax::Absent?如果规则无法通过下一个标记(s)预测是否形成预期的节点,则函数必须返回 ParsedSyntax::Absent。这样做允许调用规则决定是否这是错误,并在必要时执行错误恢复。第二个原因是确保规则不返回所有子节点都缺失的节点。

您的规则实现可能需要考虑不仅仅第一个子节点,以确定是否可以解析预期的一些子节点。例如,if语句规则可以测试解析器是否位于else子句,然后创建一个所有子节点都缺失,除了else子句的if语句。

if !p.at(T![if]) && !p.at(T![else]){
  return Absent
}

如果第一个子节点是一个节点而不是标记,您的实现也可以调用另一个解析规则。

let assignment_target = parse_assignment_target(p);

if assignment_target.is_absent() {
  return Absent;
}

let my_node = assignment_target.precede_or_missing();

但请注意调用其他规则。如果您的规则返回 Absent,则规则不得前进解析器 - 意味着它不能在解析过程中前进并消费标记。

解析子节点

解析规则将指导您如何编写实现,解析器基础设施提供了以下便利的API。

  • 可选标记 'ident'?:使用p.eat(token)。如果匹配传入的标记,则消耗下一个标记。
  • 必需标记 'ident':使用p.expect(token)。如果匹配传入的标记,则消耗下一个标记。如果标记不在源代码中,则添加一个Expected 'x' but found 'y'错误和一个缺失标记。
  • 可选节点 body: JsBlockStatement?:使用parse_block_statement(p).or_missing(p)。如果节点在源代码中存在,则解析该块,如果不存在,则添加缺失标记。
  • 必需节点 body: JsBlockStatement:使用parse_block_statement(p).or_missing_with_error(p, error_builder):如果节点在源代码中存在,则解析该块,如果不存在,则添加缺失标记和错误。

使用上述规则,可以得到以下if语句规则的实现。

fn parse_if_statement(p: &mut Parser) -> ParsedSyntax {
 if !p.at(T![if]) {
  return Absent;
 }

 let m = p.start();

 p.expect(T![if]);
 p.expect(T!['(']);
 parse_any_expression(p).or_add_diagnostic(p, js_parse_errors::expeced_if_statement);
 p.expect(T![')']);
 parse_block_statement(p).or_add_diagnostic(p, js_parse_errors::expected_block_statement);
// the else block is optional, handle the marker by using `ok`
 parse_else_clause(p).ok();

 Present(m.complete(p, JS_IF_STATEMENT));
}

等等,这些缺失标记是什么?Rome的AST外观使用固定偏移量从节点中检索特定的子节点。例如,if语句的第三个子节点是条件。然而,如果源文本中没有开括号(,则条件将成为第二个元素。这就是缺失元素发挥作用的地方。

解析列表与错误恢复

解析列表与解析具有固定子集的单个元素不同,因为它需要循环直到解析器到达终止标记(或文件末尾)。

您可能还记得,parse_*方法在返回Absent时不应继续解析。在while循环中,不继续解析解析器是问题所在,因为它不可避免地会导致无限循环。

这就是为什么在解析列表时必须进行错误恢复。幸运的是,解析器提供了使错误恢复变得容易的基础设施。解析列表的一般结构是(是的,这是解析器基础设施应该为您提供的)

让我们尝试解析一个数组

[ 1, 3, 6 ]

我们将使用ParseSeparatedList来实现这一点

struct ArrayElementsList;

impl ParseSeparatedList for ArrayElementsList {
    type ParsedElement = CompletedMarker;

    fn parse_element(&mut self, p: &mut Parser) -> ParsedSyntax<Self::ParsedElement> {
        parse_array_element(p)
    }

    fn is_at_list_end(&self, p: &mut Parser) -> bool {
        p.at_ts(token_set![T![default], T![case], T!['}']])
    }

    fn recover(
        &mut self,
        p: &mut Parser,
        parsed_element: ParsedSyntax<Self::ParsedElement>,
    ) -> parser::RecoveryResult {
        parsed_element.or_recover(
            p,
            &ParseRecovery::new(JS_BOGUS_STATEMENT, STMT_RECOVERY_SET),
            js_parse_error::expected_case,
        )
    }
};

让我们一步一步地来做

parsed_element.or_recover(
    p,
    &ParseRecovery::new(JS_BOGUS_STATEMENT, STMT_RECOVERY_SET),
    js_parse_error::expected_case,
)

or_recoverparse_array_element方法返回Absent时执行错误恢复;源文本中没有数组元素。

恢复将消耗所有标记,直到找到token_set中指定的标记之一、换行符(如果您调用了enable_recovery_on_line_break)或文件末尾。

恢复操作不会丢弃令牌,而是将它们包裹在一个JS_BOGUS_EXPRESSION节点(第一个参数)内部。存在多个BOGUS_*节点。你必须查阅语法以了解哪种BOGUS*节点在你的情况下是受支持的。

通常,你希望将结束列表的终结令牌、元素分隔符令牌和语句结束令牌包括在你的恢复集中。

现在,恢复的问题在于它可能会失败,有两个原因

  • 解析器到达了文件末尾;
  • 下一个令牌是恢复集中指定的令牌之一,这意味着没有可以恢复的内容;

在这些情况下,ParseSeparatedListParseNodeList将为你恢复解析器。

条件语法

条件语法允许你表达某些语法可能不在所有源文件中有效。一些用例包括

  • 仅在严格或宽松模式下受支持的语法:例如,当JavaScript文件使用"use strict"或是一个模块时,with语句是不有效的;
  • 仅在特定文件类型中受支持的语法:TypeScript、JSX、模块;
  • 仅在特定语言版本中可用的语法:实验性特性、语言的不同版本,例如(JavaScript的ECMA版本);

想法是解析器始终解析语法,无论它是否在该特定文件或上下文中受支持。这样做的主要动机是,这为我们提供了完美的错误恢复,并允许我们无论语法是否受支持,都能使用相同的代码。

然而,由于我们希望在当前文件不受支持时添加诊断,并且必须将解析的令牌附加到某个地方,因此必须处理条件语法。

让我们看看仅允许在宽松/宽松模式下的with语句

fn parse_with_statement(p: &mut Parser) -> ParsedSyntax {
 if !p.at(T![with]) {
  return Absent;
 }

 let m = p.start();
 p.bump(T![with]); // with
 parenthesized_expression(p).or_add_diagnostic(p, js_errors::expected_parenthesized_expression);
 parse_statement(p).or_add_diagnostic(p, js_error::expected_statement);
 let with_stmt = m.complete(p, JS_WITH_STATEMENT);

 let conditional = StrictMode.excluding_syntax(p, with_stmt, |p, marker| {
  p.err_builder("`with` statements are not allowed in strict mode", marker.range(p))
 });


}

规则的开始与任何其他规则相同。有趣的部分从以下开始

let conditional = StrictMode.excluding_syntax(p, with_stmt, |p, marker| {
 p.err_builder("`with` statements are not allowed in strict mode", marker.range(p))
});

StrictMode.excluding_syntax将解析的语法转换为无效节点,并在功能不受支持时使用诊断构建器创建诊断。

你可以通过调用or_invalid_to_bogusConditionalParsedSyntax转换为常规的ParsedSyntax,如果解析器处于严格模式,则将整个解析的with语句包裹在一个BOGUS节点中,否则返回未更改的with语句。

如果没有匹配你的解析规则节点的BOGUS节点?你必须返回不带or_invalid_to_bogus恢复的ConditionalParsedSyntax。然后调用者必须恢复可能无效的语法。

总结

  • 解析规则命名为parse_rule_name
  • 解析规则应返回一个ParsedSyntax
  • 如果它消耗任何令牌,则规则必须返回Present,因此可以解析节点及其子节点的一部分。
  • 否则返回Absent,并且必须不进行解析也不添加任何错误。
  • 列表必须执行错误恢复以避免无限循环。
  • 查阅语法以确定在规则上下文中有效的BOGUS节点。

依赖关系

~9–18MB
~219K SLoC