Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
66 changes: 42 additions & 24 deletions doc/syntax/refinements.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code>/</code> is defined in a
refinement for Numeric <code>1 / 2</code> invokes the original Integer#/
because Integer is a subclass of Numeric and is searched before the refinements
for the superclass Numeric. Since the method <code>/</code> 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, <code>1.foo</code>
Note that methods in a earlier ancestor have priority over refinements in a
later ancestor. For example, if the method <code>/</code> is defined in a
refinement for Numeric <code>1 / 2</code> 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, <code>1.foo</code>
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

Expand Down
14 changes: 7 additions & 7 deletions timev.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 <tt>struct tm</tt>.
# Integer and Rational have no range limit, but localtime and
# gmtime have range limits due to the C types +time_t+ and <tt>struct tm</tt>.
# 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).
#
# <tt>struct tm</tt> has _tm_year_ member to represent years.
# (<tt>tm_year = 0</tt> 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
Expand Down
8 changes: 4 additions & 4 deletions vm.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
75 changes: 56 additions & 19 deletions vm_eval.c
Original file line number Diff line number Diff line change
Expand Up @@ -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. <code>module_eval</code> 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+:
*
* <em>produces:</em>
* 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 <tt>__FILE__</tt> and errors).
* Otherwise, it will default to <tt>(eval at __FILE__:__LINE__)</tt>
* where <tt>__FILE__</tt> and <tt>__LINE__</tt> 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 <tt>__LINE__</tt> 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
Expand Down
9 changes: 9 additions & 0 deletions zjit.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 26 additions & 11 deletions zjit.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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) {}
Expand All @@ -62,40 +78,39 @@ 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)

// 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;
}

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;
}
Expand Down
1 change: 1 addition & 0 deletions zjit/bindgen/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
7 changes: 5 additions & 2 deletions zjit/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions zjit/src/cruby_bindings.inc.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 0 additions & 5 deletions zjit/src/jit_frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down