Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
da7be2e
ZJIT: Optimize codegen (#16426)
tenderlove Mar 20, 2026
8836334
ZJIT: Halve metadata memory usage (#16414)
tekknolagi Mar 20, 2026
cb15888
Fix make-snapshot: build dump_ast for mk_builtin_loader.rb (#16482)
k0kubun Mar 20, 2026
58e5355
Revert "ZJIT: Prepare frame state before getivar calls"
k0kubun Mar 20, 2026
a53eb6b
Skip dump_ast dependency for .rbinc when BASERUBY is unavailable
k0kubun Mar 20, 2026
defbf49
[ruby/prism] Swich identifiers to byte[]
headius Mar 19, 2026
32c5f01
[ruby/prism] Tweaks for byte[] identifiers
headius Mar 20, 2026
6446aca
[ruby/prism] Revert "Switch identifiers to byte[]"
kddnewton Mar 20, 2026
5b4642b
[ruby/prism] Prism.find
kddnewton Mar 20, 2026
f64f4b3
[ruby/prism] Ensure Source#offsets is set correctly in all cases
eregon Mar 16, 2026
f55d9e1
ZJIT: Add codegen for ArrayMax (#16411)
tekknolagi Mar 20, 2026
e69e633
ZJIT: Clean up getivar specialization code (#16475)
tekknolagi Mar 21, 2026
6b2f209
ZJIT: Unbox fixnum constants at compile time. (#16484)
tenderlove Mar 21, 2026
229a7ae
[DOC] Tweaks for Pathname#+
BurdetteLamar Mar 16, 2026
e6cf66a
[PRISM] Remove checked-in generated file
kddnewton Mar 20, 2026
e10f252
[PRISM] Use a flat lookup table for locals instead of an st_table
kddnewton Mar 20, 2026
7bb8f3d
[DOC] Fix links in extension.rdoc
BurdetteLamar Mar 16, 2026
ffd69d0
[DOC] Doc for Pathname#==
BurdetteLamar Mar 17, 2026
7397bbf
[DOC] Doc for Pathname#==
BurdetteLamar Mar 17, 2026
8c53dd3
[DOC] Doc for pathname#absolute?
BurdetteLamar Mar 17, 2026
4860f35
[ruby/prism] Clean up types on node find
kddnewton Mar 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ lcov*.info
/prism/api_node.c
/prism/ast.h
/prism/diagnostic.c
/prism/json.c
/prism/node.c
/prism/prettyprint.c
/prism/serialize.c
Expand Down
3 changes: 3 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ AC_ARG_WITH(dump-ast,
AS_HELP_STRING([--with-dump-ast=DUMP_AST], [use DUMP_AST as dump_ast; for cross-compiling with a host-built dump_ast]),
[DUMP_AST=$withval DUMP_AST_TARGET='$(empty)'],
[DUMP_AST='./dump_ast$(EXEEXT)' DUMP_AST_TARGET='$(DUMP_AST)'])
dnl Without baseruby, .rbinc files cannot be regenerated, so clear the
dnl dependency on dump_ast to avoid rebuilding pre-generated .rbinc files.
AS_IF([test "$HAVE_BASERUBY" = no], [DUMP_AST_TARGET='$(empty)'])
AC_SUBST(X_DUMP_AST, "${DUMP_AST}")
AC_SUBST(X_DUMP_AST_TARGET, "${DUMP_AST_TARGET}")

Expand Down
4 changes: 2 additions & 2 deletions doc/extension.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -317,11 +317,11 @@ rb_ary_aref(int argc, const VALUE *argv, VALUE ary) ::

rb_ary_entry(VALUE ary, long offset) ::

\ary[offset]
ary\[offset]

rb_ary_store(VALUE ary, long offset, VALUE obj) ::

\ary[offset] = obj
ary\[offset] = obj

rb_ary_subseq(VALUE ary, long beg, long len) ::

Expand Down
88 changes: 53 additions & 35 deletions iseq.c
Original file line number Diff line number Diff line change
Expand Up @@ -1096,41 +1096,15 @@ rb_iseq_new_with_opt(VALUE ast_value, VALUE name, VALUE path, VALUE realpath,
return iseq_translate(iseq);
}

struct pm_iseq_new_with_opt_data {
rb_iseq_t *iseq;
pm_scope_node_t *node;
};

VALUE
pm_iseq_new_with_opt_try(VALUE d)
{
struct pm_iseq_new_with_opt_data *data = (struct pm_iseq_new_with_opt_data *)d;

// This can compile child iseqs, which can raise syntax errors
pm_iseq_compile_node(data->iseq, data->node);

// This raises an exception if there is a syntax error
finish_iseq_build(data->iseq);

return Qundef;
}

/**
* This is a step in the prism compiler that is called once all of the various
* options have been established. It is called from one of the pm_iseq_new_*
* functions or from the RubyVM::InstructionSequence APIs. It is responsible for
* allocating the instruction sequence, calling into the compiler, and returning
* the built instruction sequence.
*
* Importantly, this is also the function where the compiler is re-entered to
* compile child instruction sequences. A child instruction sequence is always
* compiled using a scope node, which is why we cast it explicitly to that here
* in the parameters (as opposed to accepting a generic pm_node_t *).
* Core implementation for building a prism iseq. This does not use rb_protect,
* so any exceptions (e.g. from finish_iseq_build) propagate normally up the
* call stack — matching the parse.y compiler's behavior.
*/
rb_iseq_t *
pm_iseq_new_with_opt(pm_scope_node_t *node, VALUE name, VALUE path, VALUE realpath,
int first_lineno, const rb_iseq_t *parent, int isolated_depth,
enum rb_iseq_type type, const rb_compile_option_t *option, int *error_state)
pm_iseq_build(pm_scope_node_t *node, VALUE name, VALUE path, VALUE realpath,
int first_lineno, const rb_iseq_t *parent, int isolated_depth,
enum rb_iseq_type type, const rb_compile_option_t *option)
{
rb_iseq_t *iseq = iseq_alloc();
ISEQ_BODY(iseq)->prism = true;
Expand All @@ -1157,15 +1131,59 @@ pm_iseq_new_with_opt(pm_scope_node_t *node, VALUE name, VALUE path, VALUE realpa
prepare_iseq_build(iseq, name, path, realpath, first_lineno, &code_location, node->ast_node->node_id,
parent, isolated_depth, type, node->script_lines == NULL ? Qnil : *node->script_lines, option);

pm_iseq_compile_node(iseq, node);
finish_iseq_build(iseq);

return iseq_translate(iseq);
}

struct pm_iseq_new_with_opt_data {
rb_iseq_t *iseq;
pm_scope_node_t *node;
VALUE name, path, realpath;
int first_lineno, isolated_depth;
const rb_iseq_t *parent;
enum rb_iseq_type type;
const rb_compile_option_t *option;
};

static VALUE
pm_iseq_new_with_opt_try(VALUE d)
{
struct pm_iseq_new_with_opt_data *data = (struct pm_iseq_new_with_opt_data *)d;
data->iseq = pm_iseq_build(data->node, data->name, data->path, data->realpath,
data->first_lineno, data->parent, data->isolated_depth,
data->type, data->option);
return Qundef;
}

/**
* This is a step in the prism compiler that is called once all of the various
* options have been established. It is called from one of the pm_iseq_new_*
* functions or from the RubyVM::InstructionSequence APIs.
*
* This function uses rb_protect to catch exceptions, storing the error state
* in the provided out parameter. This is only needed at top-level entry points
* where the caller wants to handle errors gracefully. Child iseqs compiled
* during the compilation process do NOT go through this function — they use
* pm_iseq_build directly, letting exceptions propagate naturally (matching
* the parse.y compiler's behavior).
*/
rb_iseq_t *
pm_iseq_new_with_opt(pm_scope_node_t *node, VALUE name, VALUE path, VALUE realpath,
int first_lineno, const rb_iseq_t *parent, int isolated_depth,
enum rb_iseq_type type, const rb_compile_option_t *option, int *error_state)
{
struct pm_iseq_new_with_opt_data data = {
.iseq = iseq,
.node = node
.node = node, .name = name, .path = path, .realpath = realpath,
.first_lineno = first_lineno, .parent = parent,
.isolated_depth = isolated_depth, .type = type, .option = option
};
rb_protect(pm_iseq_new_with_opt_try, (VALUE)&data, error_state);

if (*error_state) return NULL;

return iseq_translate(iseq);
return data.iseq;
}

rb_iseq_t *
Expand Down
16 changes: 15 additions & 1 deletion lib/prism.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ module Prism
autoload :InspectVisitor, "prism/inspect_visitor"
autoload :LexCompat, "prism/lex_compat"
autoload :MutationCompiler, "prism/mutation_compiler"
autoload :NodeFind, "prism/node_find"
autoload :Pattern, "prism/pattern"
autoload :Reflection, "prism/reflection"
autoload :Relocation, "prism/relocation"
Expand All @@ -34,7 +35,10 @@ module Prism
# Some of these constants are not meant to be exposed, so marking them as
# private here.

private_constant :LexCompat
if RUBY_ENGINE != "jruby"
private_constant :LexCompat
private_constant :NodeFind
end

# Raised when requested to parse as the currently running Ruby version but Prism has no support for it.
class CurrentVersionError < ArgumentError
Expand Down Expand Up @@ -81,6 +85,16 @@ def self.load(source, serialized, freeze = false)
Serialize.load_parse(source, serialized, freeze)
end

# Given a Method, UnboundMethod, Proc, or Thread::Backtrace::Location,
# returns the Prism node representing it. On CRuby, this uses node_id for
# an exact match. On other implementations, it falls back to best-effort
# matching by source location line number.
#--
#: (Method | UnboundMethod | Proc | Thread::Backtrace::Location callable, ?rubyvm: bool) -> Node?
def self.find(callable, rubyvm: !!defined?(RubyVM))
NodeFind.find(callable, rubyvm)
end

# @rbs!
# VERSION: String
# BACKEND: :CEXT | :FFI
Expand Down
185 changes: 185 additions & 0 deletions lib/prism/node_find.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# frozen_string_literal: true
# :markup: markdown
#--
# rbs_inline: enabled

module Prism
# Finds the Prism AST node corresponding to a given Method, UnboundMethod,
# Proc, or Thread::Backtrace::Location. On CRuby, uses node_id from the
# instruction sequence for an exact match. On other implementations, falls
# back to best-effort matching by source location line number.
#
# This module is autoloaded so that programs that don't use Prism.find don't
# pay for its definition.
module NodeFind # :nodoc:
# Find the node for the given callable or backtrace location.
#--
#: (Method | UnboundMethod | Proc | Thread::Backtrace::Location callable, bool rubyvm) -> Node?
def self.find(callable, rubyvm)
case callable
when Proc
if rubyvm
RubyVMCallableFind.new.find(callable)
elsif callable.lambda?
LineLambdaFind.new.find(callable)
else
LineProcFind.new.find(callable)
end
when Method, UnboundMethod
if rubyvm
RubyVMCallableFind.new.find(callable)
else
LineMethodFind.new.find(callable)
end
when Thread::Backtrace::Location
if rubyvm
RubyVMBacktraceLocationFind.new.find(callable)
else
LineBacktraceLocationFind.new.find(callable)
end
else
raise ArgumentError, "Expected a Method, UnboundMethod, Proc, or Thread::Backtrace::Location, got #{callable.class}"
end
end

# Base class that handles parsing a file.
class Find
private

# Parse the given file path, returning a ParseResult or nil.
#--
#: (String? file) -> ParseResult?
def parse_file(file)
return unless file && File.readable?(file)
result = Prism.parse_file(file)
result if result.success?
end
end

# Finds the AST node for a Method, UnboundMethod, or Proc using the node_id
# from the instruction sequence.
class RubyVMCallableFind < Find
# Find the node for the given callable using the ISeq node_id.
#--
#: (Method | UnboundMethod | Proc callable) -> Node?
def find(callable)
return unless (source_location = callable.source_location)
return unless (result = parse_file(source_location[0]))
return unless (iseq = RubyVM::InstructionSequence.of(callable))

header = iseq.to_a[4]
return unless header[:parser] == :prism

result.value.find { |node| node.node_id == header[:node_id] }
end
end

# Finds the AST node for a Thread::Backtrace::Location using the node_id
# from the backtrace location.
class RubyVMBacktraceLocationFind < Find
# Find the node for the given backtrace location using node_id.
#--
#: (Thread::Backtrace::Location location) -> Node?
def find(location)
file = location.absolute_path || location.path
return unless (result = parse_file(file))
return unless RubyVM::AbstractSyntaxTree.respond_to?(:node_id_for_backtrace_location)

node_id = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(location)

result.value.find { |node| node.node_id == node_id }
end
end

# Finds the AST node for a Method or UnboundMethod using best-effort line
# matching. Used on non-CRuby implementations.
class LineMethodFind < Find
# Find the node for the given method by matching on name and line.
#--
#: (Method | UnboundMethod callable) -> Node?
def find(callable)
return unless (source_location = callable.source_location)
return unless (result = parse_file(source_location[0]))

name = callable.name
start_line = source_location[1]

result.value.find do |node|
case node
when DefNode
node.name == name && node.location.start_line == start_line
when CallNode
node.block.is_a?(BlockNode) && node.location.start_line == start_line
else
false
end
end
end
end

# Finds the AST node for a lambda using best-effort line matching. Used
# on non-CRuby implementations.
class LineLambdaFind < Find
# Find the node for the given lambda by matching on line.
#--
#: (Proc callable) -> Node?
def find(callable)
return unless (source_location = callable.source_location)
return unless (result = parse_file(source_location[0]))

start_line = source_location[1]

result.value.find do |node|
case node
when LambdaNode
node.location.start_line == start_line
when CallNode
node.block.is_a?(BlockNode) && node.location.start_line == start_line
else
false
end
end
end
end

# Finds the AST node for a non-lambda Proc using best-effort line
# matching. Used on non-CRuby implementations.
class LineProcFind < Find
# Find the node for the given proc by matching on line.
#--
#: (Proc callable) -> Node?
def find(callable)
return unless (source_location = callable.source_location)
return unless (result = parse_file(source_location[0]))

start_line = source_location[1]

result.value.find do |node|
case node
when ForNode
node.location.start_line == start_line
when CallNode
node.block.is_a?(BlockNode) && node.location.start_line == start_line
else
false
end
end
end
end

# Finds the AST node for a Thread::Backtrace::Location using best-effort
# line matching. Used on non-CRuby implementations.
class LineBacktraceLocationFind < Find
# Find the node for the given backtrace location by matching on line.
#--
#: (Thread::Backtrace::Location location) -> Node?
def find(location)
file = location.absolute_path || location.path
return unless (result = parse_file(file))

start_line = location.lineno
result.value.find { |node| node.location.start_line == start_line }
end
end
end
end
3 changes: 2 additions & 1 deletion lib/prism/parse_result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ class Source
# source is a subset of a larger source or if this is an eval. offsets is an
# array of byte offsets for the start of each line in the source code, which
# can be calculated by iterating through the source code and recording the
# byte offset whenever a newline character is encountered.
# byte offset whenever a newline character is encountered. The first
# element is always 0 to mark the first line.
#--
#: (String source, Integer start_line, Array[Integer] offsets) -> Source
def self.for(source, start_line, offsets)
Expand Down
Loading