diff --git a/doc/syntax/refinements.rdoc b/doc/syntax/refinements.rdoc
index 4095977284cf55..80595eb4455ab6 100644
--- a/doc/syntax/refinements.rdoc
+++ b/doc/syntax/refinements.rdoc
@@ -210,40 +210,58 @@ all refinements from the same module are active when a refined method
== Method Lookup
-When looking up a method for an instance of class +C+ Ruby checks:
+Method lookup in Ruby is based on the ancestor chain. You can see the
+ancestor chain for any object in Ruby by doing:
-* The refinements of +C+, in reverse order of activation
-* The prepended modules of +C+
-* +C+
-* The included modules of +C+
+ object.singleton_class.ancestors
+ # or, if the object does not support a singleton class:
+ object.class.ancestors
-If no method was found at any point this repeats with the superclass of +C+.
+The ancestor chain is constructed as follows:
-Note that methods in a subclass have priority over refinements in a
-superclass. For example, if the method / is defined in a
-refinement for Numeric 1 / 2 invokes the original Integer#/
-because Integer is a subclass of Numeric and is searched before the refinements
-for the superclass Numeric. Since the method / is also present
-in child +Integer+, the method lookup does not move up to the superclass.
+* Subclasses are before superclasses in the ancestor chain
+* Prepended modules are before the class they prepend in the ancestor
+ chain, in reverse order in which they were prepended.
+* Included modules are after the class they are included in in the
+ ancestor chain, in reverse order in which they were included.
+
+When looking up a method for an object, Ruby goes through each ancestor:
+
+* If the class/module has been refined, Ruby will consider the refinements
+ activated at the point the method was called, in reverse order of
+ activation.
+* Otherwise, Ruby will check the methods of the class/module itself.
+
+If no method was found at either point this repeats with the next
+ancestor.
-However, if a method +foo+ is defined on Numeric in a refinement, 1.foo
+Note that methods in a earlier ancestor have priority over refinements in a
+later ancestor. For example, if the method / is defined in a
+refinement for Numeric 1 / 2 invokes the original Integer#/
+because Integer is a comes before Numeric in the ancestor chain. However,
+if a method +foo+ is defined on Numeric in a refinement, 1.foo
invokes that method since +foo+ does not exist on Integer.
== +super+
-When +super+ is invoked method lookup checks:
+When +super+ is invoked, method lookup starts:
+
+* If the method is in a refinement, at the refined class or module
+* Otherwise, at the next ancestor
+
+Method lookup then proceeds as described in the Method Lookup section
+above.
-* The included modules of the current class. Note that the current class may
- be a refinement.
-* If the current class is a refinement, the method lookup proceeds as in the
- Method Lookup section above.
-* If the current class has a direct superclass, the method proceeds as in the
- Method Lookup section above using the superclass.
+Refinements activated at the call site of a refinement method do not
+affect +super+ inside that method. Only refinements activated at the
+point +super+ was called affect method lookup for that +super+ call.
+You cannot use refinements to insert into the middle of a method
+lookup chain, only to insert at the start of a method lookup chain,
+unless you control the +super+ call sites.
-Note that +super+ in a method of a refinement invokes the method in the
-refined class even if there is another refinement which has been activated in
-the same context. This is only true for +super+ in a method of a refinement, it
-does not apply to +super+ in a method in a module that is included in a refinement.
+Note that if you refine a module, the refinement method can call +super+
+to call the method in the module, but the method in the module cannot
+call +super+ to continue the method lookup process to further ancestors.
== Methods Introspection
diff --git a/timev.rb b/timev.rb
index 005c3d481a0ebf..4500b8f1699c5d 100644
--- a/timev.rb
+++ b/timev.rb
@@ -57,7 +57,7 @@
# Other calendars, such as Julian calendar, are not supported.
#
# The implementation uses a signed 63 bit integer, Integer (Bignum) object or
-# Ratoinal object to represent a rational value.
+# Rational object to represent a rational value.
# (The signed 63 bit integer is used regardless of 32 and 64 bit environments.)
# The value represents the number of nanoseconds from _Epoch_.
# The signed 63 bit integer can represent 1823-11-12 to 2116-02-20.
@@ -68,23 +68,23 @@
# and 6-tuple (year,month,day,hour,minute,second).
# +localtime+ is used for local time and +gmtime+ is used for UTC.
#
-# Integer and Rational has no range limit, but the localtime and
-# gmtime has range limits due to the C types +time_t+ and struct tm.
+# Integer and Rational have no range limit, but localtime and
+# gmtime have range limits due to the C types +time_t+ and struct tm.
# If that limit is exceeded, Ruby extrapolates the localtime function.
#
# +time_t+ can represent 1901-12-14 to 2038-01-19 if it is 32 bit signed integer,
# -292277022657-01-27 to 292277026596-12-05 if it is 64 bit signed integer.
-# However +localtime+ on some platforms doesn't supports negative +time_t+ (before 1970).
+# However +localtime+ on some platforms doesn't support negative +time_t+ (before 1970).
#
# struct tm has _tm_year_ member to represent years.
# (tm_year = 0 means the year 1900.)
# It is defined as +int+ in the C standard.
# _tm_year_ can represent years between -2147481748 to 2147485547 if +int+ is 32 bit.
#
-# Ruby supports leap seconds as far as if the C function +localtime+ and
-# +gmtime+ supports it.
+# Ruby supports leap seconds as far as the C functions +localtime+ and
+# +gmtime+ support them.
# They use the tz database in most Unix systems.
-# The tz database has timezones which supports leap seconds.
+# The tz database has timezones which support leap seconds.
# For example, "Asia/Tokyo" doesn't support leap seconds but
# "right/Asia/Tokyo" supports leap seconds.
# So, Ruby supports leap seconds if the TZ environment variable is
diff --git a/vm.c b/vm.c
index 4ec30928ed7f08..393ad4d4b84292 100644
--- a/vm.c
+++ b/vm.c
@@ -2847,8 +2847,8 @@ zjit_materialize_frames(rb_control_frame_t *cfp)
if (!rb_zjit_enabled_p) return;
while (true) {
- if (CFP_ZJIT_FRAME(cfp)) {
- const zjit_jit_frame_t *jit_frame = (const zjit_jit_frame_t *)cfp->jit_return;
+ if (CFP_ZJIT_FRAME_P(cfp)) {
+ const zjit_jit_frame_t *jit_frame = CFP_ZJIT_FRAME(cfp);
cfp->pc = jit_frame->pc;
cfp->_iseq = (rb_iseq_t *)jit_frame->iseq;
if (jit_frame->materialize_block_code) {
@@ -3665,8 +3665,8 @@ rb_execution_context_update(rb_execution_context_t *ec)
while (cfp != limit_cfp) {
const VALUE *ep = cfp->ep;
cfp->self = rb_gc_location(cfp->self);
- if (CFP_ZJIT_FRAME(cfp)) {
- rb_zjit_jit_frame_update_references((zjit_jit_frame_t *)cfp->jit_return);
+ if (CFP_ZJIT_FRAME_P(cfp)) {
+ rb_zjit_jit_frame_update_references((zjit_jit_frame_t *)CFP_ZJIT_FRAME(cfp));
// block_code must always be relocated. For ISEQ frames, the JIT caller
// may have written it (gen_block_handler_specval) for passing blocks.
// For C frames, rb_iterate0 may have written an ifunc to block_code
diff --git a/vm_eval.c b/vm_eval.c
index 2beef569b0aae6..84bb40ba8034ac 100644
--- a/vm_eval.c
+++ b/vm_eval.c
@@ -2392,29 +2392,66 @@ rb_obj_instance_exec(int argc, const VALUE *argv, VALUE self)
/*
* call-seq:
- * mod.class_eval(string [, filename [, lineno]]) -> obj
- * mod.class_eval {|mod| block } -> obj
- * mod.module_eval(string [, filename [, lineno]]) -> obj
- * mod.module_eval {|mod| block } -> obj
+ * class_eval(string, filename = nil, lineno = 1) -> obj
+ * class_eval { |mod| ... } -> obj
+ * module_eval(string, filename = nil, lineno = 1) -> obj
+ * module_eval { |mod| ... } -> obj
*
- * Evaluates the string or block in the context of _mod_, except that when
- * a block is given, constant/class variable lookup is not affected. This
- * can be used to add methods to a class. module_eval returns
- * the result of evaluating its argument. The optional _filename_ and
- * _lineno_ parameters set the text for error messages.
+ * Evaluates the +string+ or block in the context of +self+.
+ * Returns the result of the last expression.
*
- * class Thing
- * end
- * a = %q{def hello() "Hello there!" end}
- * Thing.module_eval(a)
- * puts Thing.new.hello()
- * Thing.module_eval("invalid code", "dummy", 123)
+ * When +string+ is given, evaluates the given string in the
+ * context of +self+:
*
- * produces:
+ * class Foo; end
*
- * Hello there!
- * dummy:123:in `module_eval': undefined local variable
- * or method `code' for Thing:Class
+ * Foo.module_eval("def greeting = puts 'hello'")
+ *
+ * Foo.new.greeting # => "hello"
+ *
+ * If the optional +filename+ is given, it will be used as the
+ * filename of the evaluation (for __FILE__ and errors).
+ * Otherwise, it will default to (eval at __FILE__:__LINE__)
+ * where __FILE__ and __LINE__ are the filename and
+ * line number of the caller, respectively:
+ *
+ * class Foo; end
+ *
+ * Foo.module_eval("puts __FILE__") # => "(eval at ../test.rb:3)"
+ * Foo.module_eval("puts __FILE__", "foobar.rb") # => "foobar.rb"
+ *
+ * If the optional +lineno+ is given, it will be used as the
+ * line number of the evaluation (for __LINE__ and errors).
+ * Otherwise, it will default to 1:
+ *
+ * class Foo; end
+ *
+ * Foo.module_eval("puts __LINE__") # => 1
+ * Foo.module_eval("puts __FILE__", nil, 10) # => 10
+ *
+ * When a block is given, evaluates the block in the context
+ * of +self+:
+ *
+ * class Foo; end
+ *
+ * Foo.module_eval do
+ * def greeting = puts "hello"
+ * end
+ *
+ * Foo.new.greeting
+ *
+ * However, constant and class variable lookup differs between
+ * +string+ and block. When +string+ is given, contant and class
+ * variables are looked up in the context of +self+. When a block
+ * is given, the context of the lookup is not changed:
+ *
+ * class Foo
+ * GREETING = "hello"
+ * end
+ *
+ * Foo.module_eval("puts GREETING") # => "hello"
+ *
+ * Foo.module_eval { puts GREETING } # => NameError: uninitialized constant GREETING
*/
static VALUE
diff --git a/zjit.c b/zjit.c
index fdebc922f0fc85..f1a02864af48a4 100644
--- a/zjit.c
+++ b/zjit.c
@@ -32,6 +32,15 @@ enum zjit_struct_offsets {
ISEQ_BODY_OFFSET_PARAM = offsetof(struct rb_iseq_constant_body, param)
};
+// Special JITFrame used by all C method calls. We don't control the native
+// stack layout for C frames, so cfp->jit_return points at this static frame
+// via the ZJIT_JIT_RETURN_C_FRAME sentinel instead of a per-call allocation.
+const zjit_jit_frame_t rb_zjit_c_frame = (zjit_jit_frame_t) {
+ .pc = 0,
+ .iseq = 0,
+ .materialize_block_code = false,
+};
+
void rb_zjit_profile_disable(const rb_iseq_t *iseq);
void
diff --git a/zjit.h b/zjit.h
index 031561ec54bdc4..f8bffb19caf340 100644
--- a/zjit.h
+++ b/zjit.h
@@ -27,6 +27,7 @@ typedef struct zjit_jit_frame {
#if USE_ZJIT
extern void *rb_zjit_entry;
+extern const zjit_jit_frame_t rb_zjit_c_frame;
extern uint64_t rb_zjit_call_threshold;
extern uint64_t rb_zjit_profile_threshold;
void rb_zjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit_exception);
@@ -48,6 +49,21 @@ void rb_zjit_tracing_invalidate_all(void);
void rb_zjit_invalidate_no_singleton_class(VALUE klass);
void rb_zjit_invalidate_root_box(void);
void rb_zjit_jit_frame_update_references(zjit_jit_frame_t *jit_frame);
+
+// Special value for cfp->jit_return that means "this is a C method frame, use
+// rb_zjit_c_frame as the JITFrame". We don't control the native stack layout
+// for C frames, so there's no per-call JITFrame storage; we set this sentinel
+// instead of a heap-allocated JITFrame pointer.
+#define ZJIT_JIT_RETURN_C_FRAME 0x1
+
+static inline const zjit_jit_frame_t *
+CFP_ZJIT_FRAME(const rb_control_frame_t *cfp)
+{
+ if ((VALUE)cfp->jit_return == ZJIT_JIT_RETURN_C_FRAME) {
+ return &rb_zjit_c_frame;
+ }
+ return (const zjit_jit_frame_t *)cfp->jit_return;
+}
#else
#define rb_zjit_entry 0
static inline void rb_zjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit_exception) {}
@@ -62,6 +78,7 @@ static inline void rb_zjit_tracing_invalidate_all(void) {}
static inline void rb_zjit_invalidate_no_singleton_class(VALUE klass) {}
static inline void rb_zjit_invalidate_root_box(void) {}
static inline void rb_zjit_jit_frame_update_references(zjit_jit_frame_t *jit_frame) {}
+static inline const zjit_jit_frame_t *CFP_ZJIT_FRAME(const rb_control_frame_t *cfp) { return NULL; }
#endif // #if USE_ZJIT
#define rb_zjit_enabled_p (rb_zjit_entry != 0)
@@ -69,24 +86,22 @@ static inline void rb_zjit_jit_frame_update_references(zjit_jit_frame_t *jit_fra
// BADFrame. The high bit is set, so likely SEGV on linux and darwin if dereferenced.
#define ZJIT_JIT_RETURN_POISON 0xbadfbadfbadfbadfULL
-// Return the JITFrame pointer from cfp->jit_return, or NULL if not present.
-// YJIT also uses jit_return (as a return address), so this must only return
-// non-NULL when ZJIT is enabled and has set jit_return to a JITFrame pointer.
-static inline void *
-CFP_ZJIT_FRAME(const rb_control_frame_t *cfp)
+// Return true if a given CFP has ZJIT's JITFrame.
+static inline bool
+CFP_ZJIT_FRAME_P(const rb_control_frame_t *cfp)
{
- if (!rb_zjit_enabled_p) return NULL;
+ if (!rb_zjit_enabled_p) return false;
#if USE_ZJIT
RUBY_ASSERT((unsigned long long)cfp->jit_return != ZJIT_JIT_RETURN_POISON);
#endif
- return cfp->jit_return;
+ return cfp->jit_return != NULL;
}
static inline const VALUE*
CFP_PC(const rb_control_frame_t *cfp)
{
- if (CFP_ZJIT_FRAME(cfp)) {
- return ((const zjit_jit_frame_t *)cfp->jit_return)->pc;
+ if (CFP_ZJIT_FRAME_P(cfp)) {
+ return CFP_ZJIT_FRAME(cfp)->pc;
}
return cfp->pc;
}
@@ -94,8 +109,8 @@ CFP_PC(const rb_control_frame_t *cfp)
static inline const rb_iseq_t*
CFP_ISEQ(const rb_control_frame_t *cfp)
{
- if (CFP_ZJIT_FRAME(cfp)) {
- return ((const zjit_jit_frame_t *)cfp->jit_return)->iseq;
+ if (CFP_ZJIT_FRAME_P(cfp)) {
+ return CFP_ZJIT_FRAME(cfp)->iseq;
}
return cfp->_iseq;
}
diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs
index f85839a5a2fb0d..573eb37a72f59d 100644
--- a/zjit/bindgen/src/main.rs
+++ b/zjit/bindgen/src/main.rs
@@ -315,6 +315,7 @@ fn main() {
.allowlist_type("jit_bindgen_constants")
.allowlist_type("zjit_struct_offsets")
.allowlist_var("ZJIT_JIT_RETURN_POISON")
+ .allowlist_var("ZJIT_JIT_RETURN_C_FRAME")
.allowlist_function("rb_assert_holding_vm_lock")
.allowlist_function("rb_jit_shape_complex_p")
.allowlist_function("rb_jit_multi_ractor_p")
diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs
index bbed16803f06fa..efcc11a5e43300 100644
--- a/zjit/src/codegen.rs
+++ b/zjit/src/codegen.rs
@@ -2865,8 +2865,11 @@ fn gen_push_frame(asm: &mut Assembler, argc: usize, state: &FrameState, frame: C
// Without this, stale data from a previous frame occupying this CFP slot
// can be used as an ifunc pointer, causing a segfault.
asm.mov(cfp_opnd(RUBY_OFFSET_CFP_BLOCK_CODE), 0.into());
- let jit_frame = JITFrame::new_cfunc();
- asm.mov(cfp_opnd(RUBY_OFFSET_CFP_JIT_RETURN), Opnd::const_ptr(jit_frame));
+ // C frames share a single static JITFrame (rb_zjit_c_frame). Setting
+ // cfp->jit_return to the ZJIT_JIT_RETURN_C_FRAME sentinel tells
+ // CFP_ZJIT_FRAME() to use that shared frame, so we don't need to
+ // allocate a per-call JITFrame for C method pushes.
+ asm.mov(cfp_opnd(RUBY_OFFSET_CFP_JIT_RETURN), (ZJIT_JIT_RETURN_C_FRAME as usize).into());
}
asm.mov(cfp_opnd(RUBY_OFFSET_CFP_SELF), frame.recv);
diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs
index 559134a9790525..1194a590df144b 100644
--- a/zjit/src/cruby_bindings.inc.rs
+++ b/zjit/src/cruby_bindings.inc.rs
@@ -234,6 +234,7 @@ pub const VM_ENV_DATA_INDEX_SPECVAL: i32 = -1;
pub const VM_ENV_DATA_INDEX_FLAGS: u32 = 0;
pub const VM_BLOCK_HANDLER_NONE: u32 = 0;
pub const SHAPE_ID_NUM_BITS: u32 = 32;
+pub const ZJIT_JIT_RETURN_C_FRAME: u32 = 1;
pub const ZJIT_JIT_RETURN_POISON: i64 = -4981057192772781345;
pub type rb_alloc_func_t = ::std::option::Option VALUE>;
pub const RUBY_Qfalse: ruby_special_consts = 0;
diff --git a/zjit/src/jit_frame.rs b/zjit/src/jit_frame.rs
index f7133daab2ee79..b434d0a8ed1dfd 100644
--- a/zjit/src/jit_frame.rs
+++ b/zjit/src/jit_frame.rs
@@ -20,11 +20,6 @@ impl JITFrame {
Self::alloc(JITFrame { pc, iseq, materialize_block_code })
}
- /// Create a JITFrame for a C frame (no PC, no ISEQ).
- pub fn new_cfunc() -> *const Self {
- Self::alloc(JITFrame { pc: std::ptr::null(), iseq: std::ptr::null(), materialize_block_code: false })
- }
-
/// Mark the iseq pointer for GC. Called from rb_zjit_root_mark.
pub fn mark(&self) {
if !self.iseq.is_null() {