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_lex 从 lexer.rex 中定义的规则生成 Ruby 文件 lexer.rex.rb。
这些规则将正则表达式映射到发出标记的代码。
oedipus_lex 旨在保持简单,生成的文件可读。它在幕后使用 StringScanner。它选择第一个匹配的规则,与许多优先考虑最长匹配的词法分析工具相反。
标记
Lexer 发出的标记类型为
-
语法符号的字符串(例如
'('、'$'、'...') -
其余部分的符号形式为
:tTOKEN_TYPE(例如:tPREDICATE)
标记存储为 [type, value],或者如果发出位置,则存储为 [type, [value, location]]。
生成
使用 rake generate:lexer 从 lexer.rex 文件生成 lexer.rex.rb。这由 rake spec 自动完成。
lexer.rex.rb 不在源代码控制之下,但包含在 gem 中。
|
解析器
与 Lexer 类似,parser.rb 定义了 Parser,并包含解析器工作所需的少量定义。大部分处理都在继承的类 parser.racc.rb 中完成,该类由 racc 从 parser.y 中的规则生成。
节点
Parser 发出 NodePattern::Node,它们类似于 RuboCop 的节点。它们都继承自 parser 的 Parser::AST::Source::Node,并且还共享其他方法。
与 RuboCop 的节点类似,一些节点有专门的类(例如 Sequence),而其他节点直接使用基类(例如 s(:number, 42))。
规则
规则紧密遵循上述定义。特别是 node_pattern_list 和 variadic_pattern_list 之间的区别,前者是节点模式列表(每个项可以匹配单个节点),而后者是更通用的元素列表,其中一些可能是可变的,另一些是简单的节点模式。
生成
与词法分析器类似,使用 rake generate:parser 从 parser.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,它生成可以针对给定节点返回 true 或 false 的 Ruby 代码,或者针对序列头的节点类型返回 true 或 false。
var vs access
子编译器可以调用当前节点,该节点存储在变量中(使用var:关键字参数提供)或通过 Ruby 表达式(例如access: 'current_node.children[2]')。
子编译器不会生成执行此access表达式的代码超过一次或两次。如果它可能访问该节点超过该次数,multiple_access将把结果存储在一个临时变量中(例如union)。
AtomSubcompiler
此子编译器生成被评估为 Ruby 对象的 Ruby 代码。例如"42"、:a_symbol、param1。
一个好的思考方式是当它必须作为参数传递给函数调用时。例如
# 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)中跟踪。
子编译器将在第一个可变参数元素之前和最后一个元素之后使用固定索引。