2个版本
0.0.1 | 2023年4月5日 |
---|---|
0.0.0 | 2022年1月26日 |
#8 in #rome
在 3 crate中使用
475KB
10K SLoC
编写解析规则
这是一份关于使用Rome解析器基础设施实现解析规则的简要或详细指南。
命名
约定是在您的解析规则前缀中使用 parse_
,然后使用语法文件中定义的名称。
例如,parse_for_statement
或 parse_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_recover
在parse_array_element
方法返回Absent
时执行错误恢复;源文本中没有数组元素。
恢复将消耗所有标记,直到找到token_set
中指定的标记之一、换行符(如果您调用了enable_recovery_on_line_break
)或文件末尾。
恢复操作不会丢弃令牌,而是将它们包裹在一个JS_BOGUS_EXPRESSION
节点(第一个参数)内部。存在多个BOGUS_*
节点。你必须查阅语法以了解哪种BOGUS*
节点在你的情况下是受支持的。
通常,你希望将结束列表的终结令牌、元素分隔符令牌和语句结束令牌包括在你的恢复集中。
现在,恢复的问题在于它可能会失败,有两个原因
- 解析器到达了文件末尾;
- 下一个令牌是恢复集中指定的令牌之一,这意味着没有可以恢复的内容;
在这些情况下,ParseSeparatedList
和ParseNodeList
将为你恢复解析器。
条件语法
条件语法允许你表达某些语法可能不在所有源文件中有效。一些用例包括
- 仅在严格或宽松模式下受支持的语法:例如,当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_bogus
将ConditionalParsedSyntax
转换为常规的ParsedSyntax
,如果解析器处于严格模式,则将整个解析的with
语句包裹在一个BOGUS
节点中,否则返回未更改的with
语句。
如果没有匹配你的解析规则节点的BOGUS
节点?你必须返回不带or_invalid_to_bogus
恢复的ConditionalParsedSyntax
。然后调用者必须恢复可能无效的语法。
总结
- 解析规则命名为
parse_rule_name
- 解析规则应返回一个
ParsedSyntax
- 如果它消耗任何令牌,则规则必须返回
Present
,因此可以解析节点及其子节点的一部分。 - 否则返回
Absent
,并且必须不进行解析也不添加任何错误。 - 列表必须执行错误恢复以避免无限循环。
- 查阅语法以确定在规则上下文中有效的
BOGUS
节点。
依赖关系
~9–18MB
~219K SLoC