【问题标题】:How does the Rust compiler tokenize '>' vs '>>' in generics?Rust 编译器如何标记泛型中的“>”和“>>”?
【发布时间】:2019-09-24 02:05:55
【问题描述】:

我编写了许多简单的标记器和递归下降解析器,因此我熟悉它们如何工作的基本概念。但是当我偶然发现以下 Rust 代码时,我感到很惊讶:

Option<Option<i32>>

我们知道 Rust 有一个 &gt;&gt; 移位运算符,所以我认为一个天真的标记器会在这里输出一个 &gt;&gt; 标记,解析器会将其视为错误(因为它需要两个 &gt; 标记)。

但很明显,Rust 编译器理解这种情况并正确处理它。这是怎么回事?

  • 标记器是否保持某种状态,不知何故知道它需要关闭尖括号?
  • 解析器是否检查 &gt;&gt; 并将其分解为两个令牌,然后推送回令牌流?
  • 或者完全是别的什么?

【问题讨论】:

    标签: parsing compilation rust tokenize lexical-analysis


    【解决方案1】:

    您可以查看 Rust 解析库以了解它们如何处理它。

    库比较

    模糊泡菜

    这是我写的解析器,所以我对概念最熟悉。

    tokenizer 是一个简单的逐字节解析器,它贪婪地 consumes the characters &gt;&gt; to create a DoubleRightAngle 令牌。

    一旦标记化完成,所有这些标记都被收集到一个向量中,并进行第二次解析。在这个过程中,解析位置是一个复杂的索引allows being "split"。这允许解析器在需要时将&gt;&gt; 分解为两个&gt;。具体的解析函数会根据解析的内容查找&gt;&gt; 或两个递归&gt;

    标记化和解析都是使用peresil crate 实现的。

    同步

    Syn 是另一个解析库。在这里,他们使用了一个相关的想法:每个令牌都是composed of multiple spans,每个字符一个。也就是说,Shr 结构有一个spans: [Span; 2] 字段。

    Rustc

    编译器似乎允许"gluing" multiple tokens into a bigger one。解析时&gt;&gt; can be "consumed" and replaced with a &gt;:

    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
    

    【讨论】:

      【解决方案2】:

      实际上有一个问题非常详细地描述了其中的一些:#13: "The parser"

      现实情况是,Rust 令牌管道(tokenizer + lexer)是一个相对简单的递归下降解析器,具有前瞻功能(顺便说一下,它解释了当你编写不正确的代码时会出现大量语法错误。例如,忘记关闭一个括号,解析器将卡在该块中,抱怨该块的限制)。每个令牌都被摄取,状态保持在令牌之间,并且为了前瞻目的而偷看一个额外的令牌。

      当 Rust 遇到它应该打开一个单独的状态(例如你的示例)时,它被保存在状态中以便能够准确地处理这个。由于该语言的构思非常巧妙,因此在引用和引用调用之外没有真正的歧义(例如*variable.call() - 你的意思是(*variable).call() 还是*(variable.call())?Rust 让你明确指定这一点)。

      当涉及到您所描述的类型定义时,没有歧义,因为根据定义,移位运算符不能在该空间中。 turbofish 运算符也是如此 - :: 先例表示将是下一个类型。

      所以,答案是“别的东西”——严格的词法分析器规则和有状态的解析器。

      【讨论】:

        【解决方案3】:

        词法分析器不独立于解析器,所以它有一点上下文。此外,关于您的精确问题,Rust 类型只能在精确的位置找到:

        • 在函数签名中:显然,不能与运算符混淆。

        • : sigil 之后:再次不能有任何歧义,因为冒号表示将写入一个类型:

          let x: Vec<_> = some_iterator.collect();
          
        • 在turbofish运算符中:

          let x = some_iterator.collect::<Vec<_>>();
          

          该符号的存在仅用于不产生歧义。

        • 在特质依赖类型中:

          impl trait Foo for Bar {
              type Dependent = Vec<u8>;
          }
          

          type 关键字清楚地表明会有一个类型。

        如您所见,Rust 团队精心设计了语法,使语法中不存在歧义。

        【讨论】:

        • 这个符号的存在只是为了不产生歧义——有人说要去掉turbofish;这如何改变你的答案?
        • 这并不能真正回答问题。许多传统的解析器都有一个完全独立于语言语义的标记化步骤,并且不知道它是在解析类型还是表达式来产生正确的标记。这似乎是 OP 正在谈论的那种标记器-解析器管道。
        • @mcarton 真的有任何编程语言的词法分析器完全独立于解析器吗?通常需要一些上下文。
        • @Shepmaster 真的吗?虽然我确实看到人们抱怨涡轮鱼的语法,但我还没有看到一个严肃的提议来删除它。我不明白如何做到这一点而不引起它所防止的许多令人头疼的问题。
        • @FrenchBoiethios 可能没有多少现代语言是纯粹的了。但它仍然是课程中常用的方法,词法分析和解析不仅限于编程语言。
        猜你喜欢
        • 1970-01-01
        • 2011-07-17
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-07-03
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多