Newshot
Account

Steadfast 更新 I - 基线

Translation(model translation, not the source's own text)

Steadfast 更新 I - 基线

13·11·2026

我非常认同 Rich Hickey 所说的 Hammock-driven development,这是一个半开玩笑的说法,指的是通过花大量时间思考问题来解决问题。Rich 谈到,花时间把一个问思想透很重要,而不是急着坐到电脑前敲代码;他也正确地指出,软件中的许多大问题其实是误解问题,而这本质上关乎思考。

在我的工作和生活中,我一次又一次看到,行动前缺乏把事情想透会导致糟糕的结果。希伯来语说法“סוֹף מַעֲשֶׂה בְּמַחֲשָׁבָה תְּחִלָּה”,意思是“行动的终点在于最初的思考”,讲的正是这一点——在哲学、精神、伦理乃至政治层面上都是如此。一个明智的人会在头脑中把解决问题的行动计划一直推演到结论,探索其所有细节,权衡所有后果与替代方案,然后再采取行动。就像棋手能够提前推演棋局数十步一样。确实,想象一下,如果我们敬爱的领导者在行动前有能力把事情想透,人类会好多少!😜

不过需要指出的是,我并不怎么花时间躺在吊床里(我们花园里确实有一个,但现在是冬天,不太有用。)但我确实喜欢花时间思考代码,而不是写代码。我可以在洗碗、散步、开车或听音乐时这样做。有时我干脆睡一觉,让大脑在后台任务中处理想法。有时我会把想法写在笔记本里,这也是一个非常有帮助的过程。我很确定,我的大部分工作时间都花在把事情想透上,而编码部分在许多方面只是我所有思考的产物。

这也与我最新的开源项目 Steadfast 有关。这个项目在我脑海中已经存在很长时间了,而真正帮助我推进这个项目的正是这一点——花大量时间不仅去思考,还去勾勒我的想法并尝试它们。这帮助我确定了需要解决哪些问题、实现可能如何运作、API 可能是什么样子,以及界限在哪里。

奠定基线

上周我忙于为这个项目打基础。我从实现将给定的 Prism AST 转换为源代码的功能开始。这项工作在我编写 Papercraft 编译器时已经做过,但我想改进它,以便为后续工作做准备。

目前在 Ruby 中没有办法把 AST 传给 eval——确实,那会很好,但在花了一些时间阅读 Prism 源代码以及 Ruby 运行时源代码(更具体地说,是 eval 的实现)之后,我清楚地认识到,尝试实现这样的功能绝非易事。这主要与以下事实有关:(据我所知)Ruby 层的 Prism AST 节点实际上是从底层的 pm_node C 结构体具体化而来的,但并不由它们支撑。所以,如果我们希望能够 eval 给定的 AST,就需要能够在 C 层把这些 Ruby AST 节点转换回 pm_node 结构体,然后以某种方式将其送入 eval 实现,以便它把 AST 转换为 Ruby VM 字节码。

能够执行 eval(ast) 确实会很棒,也许有一天 Ruby 运行时会获得这种能力。但就目前而言,我们需要能够将给定的 AST 转换为源代码,然后我们可以把它送入 eval。这还进一步意味着,如果我们基于一个合成 AST 生成代码——例如一个由来自不同源文件不同位置解析出的节点组成的 AST——那么当抛出异常时,我们可能会得到完全无意义的回溯!

正是考虑到这一点,我也做了一些工作,以便能够追踪生成源代码中每一小段代码的原始源位置。这之所以复杂,是因为虽然每个 Prism 节点都包含位置信息,说明它在源文件中的位置(起始和结束的行与列),但目前 Ruby AST 节点没有文件名信息。因此我做的一件事,是把该信息添加到 Prism::Source 类中,每个 AST 节点都会引用它的一个实例,然后利用该信息将生成源代码中的每一行映射回其原始源位置。

Steadfast 目前提供以下工具:

  • Steadfast.to_ast(obj) - 这是将给定的 proc 或方法转换为其 AST 的工具。该工具使用 Ruby 4.1 中新的 AST API,即 Proc#syntax_tree 和 Method#syntax_tree,并如上所述,为 AST 节点引用的底层 Prism::Source 实例额外设置一个 __filename__ 属性。我仍在考虑的一件事是,提供一种使用 #syntax_tree 的替代方案,使得可以在较旧版本的 Ruby 中使用 Steadfast,或者在我们出于例如内存使用等原因不想启用 RubyVM.keep_script_lines 时也能使用(更多信息见此)。相关代码已经存在于 steadfast.rb 中(从 Sirop 复制过来),但目前尚未接入。

Steadfast.to_ast(obj) - 这是将给定的 proc 或方法转换为其 AST 的工具。该工具使用 Ruby 4.1 中新的 AST API,即 Proc#syntax_tree 和 Method#syntax_tree,并如上所述,为 AST 节点引用的底层 Prism::Source 实例额外设置一个 __filename__ 属性。

我仍在考虑的一件事是,提供一种使用 #syntax_tree 的替代方案,使得可以在较旧版本的 Ruby 中使用 Steadfast,或者在我们出于例如内存使用等原因不想启用 RubyVM.keep_script_lines 时也能使用(更多信息见此)。相关代码已经存在于 steadfast.rb 中(从 Sirop 复制过来),但目前尚未接入。

  • Steadfast.to_source(obj) - 此工具为给定的 proc、方法或 AST 生成源代码。生成的代码格式并不美观。没有缩进,换行可能看起来随意,但这并不重要。代码生成算法的设计更注重正确性、易于测试,以及能够轻松正确地转换组合 AST 的异常回溯(即包含来自不同源位置的节点的 AST)。有一整套测试确保“sourcifier”能够处理各种 Ruby 语法——大多数测试夹具改编自用于测试 Prism 的那些。
  • Steadfast.translate_backtrace(bt) - 为生成代码转换给定的异常回溯,使每个回溯条目都指向正确的源位置,如上所述。

Steadfast API

当我开发一个新 gem 时,通常从设计 API 开始,尝试弄清楚开发者会如何使用这个 gem。就 Steadfast 而言,我选择提供一个名为 Steadfast 的单一模块,它提供了一组相对较少的模块方法。除此之外,我目前不打算实现任何额外的类或子模块,至少不是那些暴露给该 gem 的“用户”的类或子模块。

除了上面已经实现的方法之外,以下是我目前计划实现的其余 API:

  • Steadfast.mutate(ast, **) / Steadfast.mutate(ast) { } - 通过为特定节点提供替换(第一种形式),或提供一个会为 AST 中每个节点调用并且可以返回替换节点的块,来修改给定的 AST。例如,下面的(愚蠢的)例子将所有字面整数 42 替换为 43:mutated_ast = Steadfast.mutate(ast) { |n| (n in Prism::IntegerNode(value: 42)) ? Steadfast.quote { 43 } : n } 由于 AST 基本上是不可变的,如果块为 AST 中某处的节点返回了任何替换,此方法将返回带有所需更改的给定 AST 的副本。
  • Steadfast.mutate(ast, **) / Steadfast.mutate(ast) { } - 通过为特定节点提供替换(第一种形式),或提供一个会为 AST 中每个节点调用并且可以返回替换节点的块,来修改给定的 AST。例如,下面的(愚蠢的)例子将所有字面整数 42 替换为 43:

        mutated_ast = Steadfast.mutate(ast) { |n|
  (n in Prism::IntegerNode(value: 42)) ? Steadfast.quote { 43 } : n
}
      

由于 AST 基本上是不可变的,如果块为 AST 中某处的节点返回了任何替换,此方法将返回带有所需更改的给定 AST 的副本。

  • Steadfast.walk(ast) { } - 一个用于遍历 AST 的工具,会为 AST 中的每个节点调用块,比 Prism 当前提供的 API 更方便的一种遍历 AST 的方式。
  • Steadfast.find(ast) { } - 一个返回块返回 true 的第一个节点的工具。例如,下面返回对 #foo 的第一次调用:Steadfast.find(ast) { it in Prism::CallNode(receiver: nil, name: :foo) }

Steadfast.find(ast) { } - 一个返回块返回 true 的第一个节点的工具。例如,下面返回对 #foo 的第一次调用:


        Steadfast.find(ast

Steadfast 更新 I - 基线

……在要求代码被优化之前,但这似乎是 Ruby 一个相当小众的特性。然而,使用 Steadfast,只需操作给定方法的 AST 就可以实现尾调用优化。考虑以下阶乘的递归实现:

      

def fact(n, acc = 1) return acc if n <= 1

fact(n - 1, n * acc) # <- tail call end


        
如果没有尾调用优化,用足够大的 n(在我的机器上使用 Ruby 4.0.9 时大于 10061)调用此方法将导致抛出 SystemStackError,而且还有性能成本,因为方法调用的开销,因此尾调用优化的用例,它会将该方法转换为类似以下内容:

      

def fact(n, acc = 1) loop do return acc if n <= 1

n, acc = [n - 1, n * acc] end end


        
我们在这里所做的转换是取递归方法的主体,将其放入一个循环中,并用更新方法参数的局部变量赋值替换尾调用。以下是使用 Steadfast 可能如何做到这一点:

      

@param meth [Method]

def optimize_tail_call(meth) ast = Steadfast.to_ast(meth) params = ast.parameters *body, tail = ast.body.body raise "Not a tail call method!" if !(tail in Prism::CallNode(name: meth.name))

ast2 = Steadfast.mutate(ast, ast.body => Steadfast.quote { loop { unquote(body) unquote { left = [params.requireds.map(&:name), params.optionals.map(&:name)] right = tail.arguments.arguments quote { __.assign(unquote(left), unquote(right)) } } } }) Steadfast.eval_ast(ast2, meth.receiver) end

让我们过一遍这段代码:我们首先获取给定方法的 AST,从 AST 中提取参数节点、排除尾调用的主体以及尾部(方法体中的最后一条语句)。注意我们如何使用 splat 运算符获取主体中除最后一条之外的所有语句,最后一条被赋值给 tail。我们确保尾部确实是尾调用,即递归的,然后我们通过修改方法定义创建一个新的 AST,将方法体替换为 loop {}。我们将原始主体(不包括尾部)注入循环中,随后是一个局部变量赋值,将尾调用参数赋值给方法参数。

注意 quote 内部的 unquote 调用实际上是在“编译时”执行的,即构建 AST 时。unquote 的返回值是一个 AST 节点,被注入到引用的代码中。quote/unquote 工具的工作方式有点像插值字符串——代码在引号中,而 unquote 就像插值运算符,它让你能够逃出引号,以便进行一些计算,这些计算将被注入到引用的代码中。

还要注意 __.assign 调用,它允许我们合成具有任意左侧和右侧的局部变量赋值。这是我打算包含在 Steadfast 中的额外工具之一,它将允许合成任何类型的 AST 节点。这些工具的语法是我仍在研究的东西,所以最终它们可能看起来不同。

Steadfast 的下一步

既然从任意 AST 生成源代码的基线能力已经可用,我可以将注意力转向项目的下一个阶段,该阶段将专注于构建一个健壮的 AST 变异工具。Steadfast.mutate 的原型已经存在于测试代码中(我用它来构建内联 AST,以测试源映射功能)。此外,我还可能研究 walk、find 和 find_all 工具,它们也可能有助于测试。

正如我上面所写,我把大量时间花在思考问题上,而不是写代码。随着我继续思考 Steadfast,我发现新的用例,以及我们可能进行代码生成的新方式。或许有一整层新的更高层工具正等待被发现,它们基于我上面讨论的 Steadfast 基础工具,将让我们能够解决诸如解码和编码 HTML、XML、JSON、Protobufs、SQL 以及其他协议和格式之类的问题。也许还有一整套新技术等待被发现,用于分析和操作 Ruby 源代码,风格类似 Rubocop 等工具。

使用 Steadfast,我们能在 Ruby 中把元编程推进到多远?这就是我打算在未来几个月里弄清楚的事情。

The translation is machine-generated and individual terms may be off. Nothing is rewritten or cut; the original is authoritative.

Published by Lobsters
This site only summarises and aggregates; copyright stays with the original author. Headlines and summaries follow the source — where they differ, the original wins.
Read the original ↗