Rust 编译器如何标记泛型中的“>”与“>>”?
How does the Rust compiler tokenize '>' vs '>>' in generics?
我写过很多简单的分词器和递归下降解析器,所以我熟悉它们工作原理的基本概念。但是当我偶然发现以下 Rust 代码时,我感到很惊讶:
Option<Option<i32>>
我们知道 Rust 有一个 >>
移位运算符,所以我认为天真的标记器会在这里输出一个 >>
标记,解析器会认为这是一个错误(因为它期望两个 >
代币代替)。
但显然 Rust 编译器了解情况并正确处理。这是怎么回事?
- 分词器是否保持某种状态,以某种方式知道它需要关闭尖括号?
- 解析器是否检查
>>
并将其分成两个推回令牌流的令牌?
- 或者完全不同的东西?
实际上有一个问题非常详细地描述了其中的一些内容:#13: "The parser"
现实情况是,Rust 令牌管道(分词器 + 词法分析器)是一个相对简单的递归下降解析器,具有前瞻性(顺便说一句,这解释了当您编写不正确的代码时会遇到的大量语法错误。例如,忘记关闭括号,解析器将卡在那个块中,抱怨块的限制)。摄取每个令牌,在令牌之间保留状态,并查看额外的令牌以进行前瞻。
当 Rust 遇到它应该打开一个单独状态的东西时(比如你的例子),它被保存在状态中以便能够准确地处理这个。由于该语言的构思非常巧妙,除了引用和引用调用之外没有真正的歧义(比如 *variable.call()
- 你是说 (*variable).call()
还是 *(variable.call())
?Rust 让你明确指定) .
当谈到像您所描述的那样的类型定义时,没有歧义,因为根据定义,移位运算符不能在 space 中。 turbofish 运算符也是如此 - ::
先例表明类型将是下一个。
所以,答案是 "something else" - 严格的词法分析器规则和有状态的解析器。
词法分析器并不独立于解析器,所以它有一点上下文。此外,关于你的确切问题,Rust 类型只能在精确的地方找到:
在函数签名中:显然,不能与运算符混淆。
在 :
印记之后:再次不能有任何歧义,因为冒号表示将写入的类型:
let x: Vec<_> = some_iterator.collect();
在涡轮鱼算子中:
let x = some_iterator.collect::<Vec<_>>();
该符号的存在只是为了避免产生歧义。
在特质依赖类型中:
impl trait Foo for Bar {
type Dependent = Vec<u8>;
}
type
关键字明确表示会有类型
如您所见,Rust 团队精心设计了语法,因此语法中不会存在歧义。
您可以查看 Rust 解析库以了解它们是如何处理的。
图书馆比较
毛茸茸的泡菜
这是我编写的解析器,所以我对其中的概念最为熟悉。
标记器是一个简单的逐字节解析器,它贪婪地 consumes the characters >>
to create a DoubleRightAngle
标记。
标记化完成后,所有这些标记都会收集到一个向量中,然后进行第二次解析。在这一遍中,解析位置是一个复杂的索引allows being "split"。这允许解析器在需要时将 >>
分解为两个 >
。特定的解析函数根据正在解析的内容查找 >>
或两个递归 >
。
标记化和解析都是使用 peresil crate 实现的。
同步
Syn 是另一个解析库。在这里,他们使用了一个相关的想法:每个标记都是 composed of multiple spans, one for each character. That is, the Shr
结构,有一个 spans: [Span; 2]
字段。
Rustc
看来编译器允许 "gluing" multiple tokens into a bigger one. During parsing, >>
can be "consumed" and replaced with a >
:
token::BinOp(token::Shr) => {
let span = self.token.span.with_lo(self.token.span.lo() + BytePos(1));
Some(self.bump_with(token::Gt, span))
}
token::BinOpEq(token::Shr) => {
let span = self.token.span.with_lo(self.token.span.lo() + BytePos(1));
Some(self.bump_with(token::Ge, span))
}
token::Ge => {
let span = self.token.span.with_lo(self.token.span.lo() + BytePos(1));
Some(self.bump_with(token::Eq, span))
}
加分
空格周围还有一个问题。解析器应该等效地解析这两种类型:
Option<Option<i32>>
Option < Option < i32 > >
但是,它不应该等效地解析这些表达式:
a >>= 1
a >> = 1
我写过很多简单的分词器和递归下降解析器,所以我熟悉它们工作原理的基本概念。但是当我偶然发现以下 Rust 代码时,我感到很惊讶:
Option<Option<i32>>
我们知道 Rust 有一个 >>
移位运算符,所以我认为天真的标记器会在这里输出一个 >>
标记,解析器会认为这是一个错误(因为它期望两个 >
代币代替)。
但显然 Rust 编译器了解情况并正确处理。这是怎么回事?
- 分词器是否保持某种状态,以某种方式知道它需要关闭尖括号?
- 解析器是否检查
>>
并将其分成两个推回令牌流的令牌? - 或者完全不同的东西?
实际上有一个问题非常详细地描述了其中的一些内容:#13: "The parser"
现实情况是,Rust 令牌管道(分词器 + 词法分析器)是一个相对简单的递归下降解析器,具有前瞻性(顺便说一句,这解释了当您编写不正确的代码时会遇到的大量语法错误。例如,忘记关闭括号,解析器将卡在那个块中,抱怨块的限制)。摄取每个令牌,在令牌之间保留状态,并查看额外的令牌以进行前瞻。
当 Rust 遇到它应该打开一个单独状态的东西时(比如你的例子),它被保存在状态中以便能够准确地处理这个。由于该语言的构思非常巧妙,除了引用和引用调用之外没有真正的歧义(比如 *variable.call()
- 你是说 (*variable).call()
还是 *(variable.call())
?Rust 让你明确指定) .
当谈到像您所描述的那样的类型定义时,没有歧义,因为根据定义,移位运算符不能在 space 中。 turbofish 运算符也是如此 - ::
先例表明类型将是下一个。
所以,答案是 "something else" - 严格的词法分析器规则和有状态的解析器。
词法分析器并不独立于解析器,所以它有一点上下文。此外,关于你的确切问题,Rust 类型只能在精确的地方找到:
在函数签名中:显然,不能与运算符混淆。
在
:
印记之后:再次不能有任何歧义,因为冒号表示将写入的类型:let x: Vec<_> = some_iterator.collect();
在涡轮鱼算子中:
let x = some_iterator.collect::<Vec<_>>();
该符号的存在只是为了避免产生歧义。
在特质依赖类型中:
impl trait Foo for Bar { type Dependent = Vec<u8>; }
type
关键字明确表示会有类型
如您所见,Rust 团队精心设计了语法,因此语法中不会存在歧义。
您可以查看 Rust 解析库以了解它们是如何处理的。
图书馆比较
毛茸茸的泡菜
这是我编写的解析器,所以我对其中的概念最为熟悉。
标记器是一个简单的逐字节解析器,它贪婪地 consumes the characters >>
to create a DoubleRightAngle
标记。
标记化完成后,所有这些标记都会收集到一个向量中,然后进行第二次解析。在这一遍中,解析位置是一个复杂的索引allows being "split"。这允许解析器在需要时将 >>
分解为两个 >
。特定的解析函数根据正在解析的内容查找 >>
或两个递归 >
。
标记化和解析都是使用 peresil crate 实现的。
同步
Syn 是另一个解析库。在这里,他们使用了一个相关的想法:每个标记都是 composed of multiple spans, one for each character. That is, the Shr
结构,有一个 spans: [Span; 2]
字段。
Rustc
看来编译器允许 "gluing" multiple tokens into a bigger one. During parsing, >>
can be "consumed" and replaced with a >
:
token::BinOp(token::Shr) => {
let span = self.token.span.with_lo(self.token.span.lo() + BytePos(1));
Some(self.bump_with(token::Gt, span))
}
token::BinOpEq(token::Shr) => {
let span = self.token.span.with_lo(self.token.span.lo() + BytePos(1));
Some(self.bump_with(token::Ge, span))
}
token::Ge => {
let span = self.token.span.with_lo(self.token.span.lo() + BytePos(1));
Some(self.bump_with(token::Eq, span))
}
加分
空格周围还有一个问题。解析器应该等效地解析这两种类型:
Option<Option<i32>>
Option < Option < i32 > >
但是,它不应该等效地解析这些表达式:
a >>= 1
a >> = 1