NodePattern 编译器黑客指南

本文档面向希望了解/修改 NodePattern 编译器的任何人。它假设您对 NodePattern 语法以及 parser gem 生成的 AST 有所了解。

高级视图

NodePattern 编译器使用与 parser gem 相同的技术

  • 一个 Lexer 将源代码分解成标记

  • 一个 Parser 使用标记和 Builder 来生成 AST

  • 一个 Compiler 将此 AST 转换为 Ruby 代码

示例

  • 模式:(send nil? {:puts :p} $...)

  • 标记:'(', [:tNODE_TYPE, :send], [:tPREDICATE, :nil?], '{', ...

  • AST:s(:sequence, s(:node_type, :send), s(:predicate, :nil?), s(:union, ...

  • Ruby 代码

    node.is_a?(::RuboCop::AST::Node) && node.children.size >= 2 &&
    node.send_type? &&
    node.children[0].nil?() &&
    (union2 = node.children[1]; ...

以下描述了各个部分

词汇表

“节点模式”:可以与单个AST::Node匹配的内容。虽然(int 42)#is_fun?都对应于节点模式,但...(没有括号)不是节点模式。

“序列”:描述节点子节点序列(及其类型)的节点模式:(type first_child second_child ...)

“可变参数”:序列中可以匹配可变数量子节点的元素。(send _ int* ...) 有两个可变参数元素(int*...)。(send _ :name) 不包含可变参数元素。请注意,序列本身永远不会是可变参数的。

“原子”:与简单 Ruby 对象对应的模式元素。(send nil? :puts (str 'hello')) 有两个原子::puts'hello'

词法分析器

lexer.rb 定义了 Lexer,并包含词法分析器工作所需的少量定义。大部分处理都在继承的类中完成,该类由 oedipus_lex 生成。

规则

oedipus_lexlexer.rex 中定义的规则生成 Ruby 文件 lexer.rex.rb

这些规则将正则表达式映射到发出标记的代码。

oedipus_lex 旨在保持简单,生成的文件可读。它在幕后使用 StringScanner。它选择第一个匹配的规则,与许多优先考虑最长匹配的词法分析工具相反。

标记

Lexer 发出的标记类型为

  • 语法符号的字符串(例如 '(''$''...'

  • 其余部分的符号形式为 :tTOKEN_TYPE(例如 :tPREDICATE

标记存储为 [type, value],或者如果发出位置,则存储为 [type, [value, location]]

生成

使用 rake generate:lexerlexer.rex 文件生成 lexer.rex.rb。这由 rake spec 自动完成。

lexer.rex.rb 不在源代码控制之下,但包含在 gem 中。

解析器

Lexer 类似,parser.rb 定义了 Parser,并包含解析器工作所需的少量定义。大部分处理都在继承的类 parser.racc.rb 中完成,该类由 raccparser.y 中的规则生成。

节点

Parser 发出 NodePattern::Node,它们类似于 RuboCop 的节点。它们都继承自 parserParser::AST::Source::Node,并且还共享其他方法。

与 RuboCop 的节点类似,一些节点有专门的类(例如 Sequence),而其他节点直接使用基类(例如 s(:number, 42))。

规则

规则紧密遵循上述定义。特别是 node_pattern_listvariadic_pattern_list 之间的区别,前者是节点模式列表(每个项可以匹配单个节点),而后者是更通用的元素列表,其中一些可能是可变的,另一些是简单的节点模式。

生成

与词法分析器类似,使用 rake generate:parserparser.y 文件生成 parser.racc.rb。这由 rake spec 自动完成。

parser.racc.rb 不在源代码管理下,但包含在 gem 中。

编译器

编译器的核心是 Compiler 类。它保存全局状态(例如对命名参数的引用)。编译器的目标是生成 matching_code,即可以针对 AST::Node 或任何 Ruby 对象运行的 Ruby 代码。

matching_code 打包到 lambda 或方法 def 的代码中由 MethodDefiner 模块单独处理。

编译本身由三个子编译器处理

  • NodePatternSubcompiler

  • AtomSubcompiler

  • SequenceSubcompiler

访问者

子编译器使用 访问者模式

visit_ 开头的函数用于处理不同类型的节点。对于类型为 :capture 的节点,将调用 visit_capture 函数,如果没有定义,则将调用 visit_other_type 函数。

不传递任何参数,因为访问的节点可以通过 node 属性读取器访问。

NodePatternSubcompiler

对于任何 NodePattern::Node,它生成可以针对给定节点返回 truefalse 的 Ruby 代码,或者针对序列头的节点类型返回 truefalse

var vs access

子编译器可以调用当前节点,该节点存储在变量中(使用var:关键字参数提供)或通过 Ruby 表达式(例如access: 'current_node.children[2]')。

子编译器不会生成执行此access表达式的代码超过一次或两次。如果它可能访问该节点超过该次数,multiple_access将把结果存储在一个临时变量中(例如union)。

序列

序列是最难处理的元素,并被推迟到SequenceSubcompiler

原子

原子使用visit_other_type处理,该方法推迟到AtomSubcompiler并将该结果转换为节点模式,方法是附加=== cur_node(或在序列头部时附加=== cur_node.type)。

这样,(_ #func?(%1) %2)中的两个参数将以不同的方式编译;%1将被编译为param1,而%2将被编译为param2 === node.children[1]

优先级

生成的代码具有高于或等于&&的优先级,以便使链接变得方便。

AtomSubcompiler

此子编译器生成被评估为 Ruby 对象的 Ruby 代码。例如"42":a_symbolparam1

一个好的思考方式是当它必须作为参数传递给函数调用时。例如

# Pattern '#func(42, %1)' compiles to
func(node, 42, param1)

请注意,任何节点模式都可以由此子编译器输出,但那些不对应于 Ruby 字面量的节点模式将被输出为 lambda,以便它们可以组合。例如

# Pattern '#func(int)' compiles to
func(node, ->(compare) { compare.is_a?(::RuboCop::AST::Node) && compare.int_type? })

SequenceSubcompiler

子编译器依次编译序列的项,跟踪AST::Node的哪些子节点正在匹配。

可变参数项

复杂性来自可变参数元素,这些元素具有复杂的处理并且可能在编译时无法知道哪些子节点与后续项匹配。

示例(没有可变参数项)

(_type int _ str)

第一个子节点必须匹配int,第三个子节点必须匹配str。子编译器将使用children[0]children[2]

示例(一个可变参数项)

(_type int _* str)

第一个子节点必须匹配int最后一个子节点必须匹配str。子编译器将使用children[0]children[-1]

示例(多个可变参数项)

(_type int+ sym str+)

子编译器不能使用任何整数和children[]来匹配sym。这必须在运行时在一个变量(cur_index)中跟踪。

子编译器将在第一个可变参数元素之前和最后一个元素之后使用固定索引。

节点模式项

节点模式项委托给NodePatternSubcompiler

在模式(:sym :sym)中,两个:sym将被不同地编译,因为第一个:sym位于“序列头”中::sym === node.type:sym == node.children[0]。子编译器指示模式是否位于“序列头”中,因此NodePatternSubcompiler可以生成正确的代码。

可变参数元素可能(目前)不覆盖序列头。为了方便起见,(...)被理解为(_ ...)。其他类型的节点将引发错误(例如(<will not compile>);参见Node#in_sequence_head

优先级

与节点模式子编译器一样,它生成优先级高于或等于&&的代码,以便于链式操作。

变体:WithMeta

这些解析器/构建器/词法分析器的变体为 AST 节点生成location信息(与parser gem 完全相同),以及带有其位置的注释(与parser gem 相同)。

由于此信息通常在只想定义方法时不使用,因此默认情况下不会加载它。

变体:Debug

这些编译器/子编译器的变体通过在编译NodePatternSubcompilerSequenceSubcompiler之前和之后添加跟踪代码来工作。每个节点都分配了一个唯一的 ID,跟踪代码在表达式即将被评估时切换相应的开关,并在之后(与&&连接,因此只有在节点匹配时才切换开关)。原子不会被不同地编译,因为它们实际上不可匹配(当不作为节点模式编译时)。