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() {