Project

General

Profile

Actions

Feature #20999

closed

Add RubyVM object source support

Added by bkuhlmann (Brooke Kuhlmann) 5 days ago. Updated 2 days ago.

Status:
Rejected
Assignee:
-
Target version:
-
[ruby-core:120467]

Description

Hello. 👋

I'd like to propose adding the ability to acquire the source of any object within memory via the RubyVM. A couple use cases come to mind:

  • This would simplify the Method Source gem implementation and possibly eliminate the need for the gem.
  • Another use case is this allows DSLs, like Initable, to elegantly acquire the source code of objects and/or functions (in my case, I'm most interested in the lazy evaluation of function bodies).

⚠️ I'm also aware that the RubyVM documentation clearly stats this isn't meant for production use:

This module is for very limited purposes, such as debugging, prototyping, and research. Normal users must not use it. This module is not portable between Ruby implementations.

...but I'd like to entertain this proposed feature request, regardless. Here's an example, using the aforementioned Initable gem, where I use the RubyVM to obtain the source of a Proc:

class Demo
  include Initable[%i[req name], [:key, :default, proc { Object.new }]]
end

puts Demo.new("demo").inspect
#<Demo:0x000000014349a400 @name="demo", @default=#<Object:0x000000014349a360>>

With the above, I'm lazily obtaining the source code of the Proc in order to dynamically define the #initialize method (essentially a module_eval on Demo, simply speaking) using a nested array as specified by Method#parameters because I don't want an instance of Object until initialization is necessary.

Context

Prior to the release of Ruby 3.4.0, you could do this:

function = proc { Object.new }
ast = RubyVM::AbstractSyntaxTree.of function
ast.children.last.source

# "Object.new"

Unfortunately, with the release of Ruby 3.4.0 -- which defaults to the Prism parser -- the ability to acquire source code is a bit more complicated. For example, to achieve what is shown above, you have to do this:

function = proc { Object.new }
RubyVM::InstructionSequence.of(function).script_lines

# [
#   "function = proc { Object.new }\n",
#   "RubyVM::InstructionSequence.of(function).script_lines\n",
#   "\n",
#   ""
# ]

Definitely doable, but now requires more work to pluck "Object.new" from the body of the Proc. One solution is to use a regular expression to find and extract the first line of the result. Example:

/
  proc          # Proc statement.
  \s*           # Optional space.
  \{            # Block open.
  (?<body>.*?)  # Source code body.
  \}            # Block close.
/x

Definitely doesn't account for all use cases (like when a Proc spans multiple lines or uses do...end syntax) but will get you close.

How

I think there are a couple of paths that might be nice to support this use case.

Option A

Teach RubyVM::InstructionSequence to respond to #source which would be similar to what was possible prior to Ruby 3.4.0. Example:

function = proc { Object.new }
RubyVM::InstructionSequence.of(function).source

# "Object.new"

Option B

This is something that Samuel Williams mentioned in Feature 6012 which would be to provide a Source object as answered by Method#source and Proc#source. Example (using a Proc):

# Implementation
# Method#source (i.e. Source.new path, line_number, line_count, body)

# Usage:

function = proc { Object.new }

method.source.code      # "Object.new"
method.source.path      # "$HOME/demo.rb"
method.source.location  # [2, 0, 3, 3]

Option C

It could be nice to support both Option A and B.


Related issues 1 (1 open0 closed)

Related to Ruby master - Feature #21005: Update the source location method to include line start/stop and column start/stop detailsOpenActions
Actions

Also available in: Atom PDF

Like1
Like0Like0Like0Like0Like0Like0Like0