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
5 changes: 5 additions & 0 deletions common.mk
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ INSTALL_DOC_OPTS = --rdoc-output="$(RDOCOUT)" --html-output="$(HTMLOUT)"
RDOC_GEN_OPTS = --no-force-update \
--exclude '^lib/rubygems/core_ext/kernel_require\.rb$$' \
$(empty)
RDOC_SERVER_PORT = 4000

INITOBJS = dmyext.$(OBJEXT) dmyenc.$(OBJEXT)
NORMALMAINOBJ = main.$(OBJEXT)
Expand Down Expand Up @@ -630,6 +631,10 @@ html: PHONY $(RDOC_DEPENDS) $(RBCONFIG)
@echo Generating RDoc HTML files
$(Q) $(RDOC) --op "$(HTMLOUT)" $(RDOC_GEN_OPTS) $(RDOCFLAGS) .

html-server: PHONY $(RDOC_DEPENDS) $(RBCONFIG)
@echo Starting RDoc server with live reload
$(Q) $(RDOC) --server=$(RDOC_SERVER_PORT) $(RDOC_GEN_OPTS) $(RDOCFLAGS) .

RDOC_COVERAGE_EXCLUDES = -x ^ext/json -x ^ext/openssl -x ^ext/psych \
-x ^lib/bundler -x ^lib/rubygems \
-x ^lib/did_you_mean -x ^lib/error_highlight -x ^lib/syntax_suggest
Expand Down
13 changes: 10 additions & 3 deletions doc/contributing/documentation_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,19 @@ build directory:
make html
```

Or, to start a live-reloading server that automatically refreshes
the browser when you edit source files:

```sh
make html-server
```

Then visit http://localhost:4000 in your browser.
To use a different port: `make html-server RDOC_SERVER_PORT=8080`.

If you don't have a build directory, follow the [quick start
guide](building_ruby.md#label-Quick+start+guide) up to step 4.

Then you can preview your changes by opening
`{build folder}/.ext/html/index.html` file in your browser.

## Goal

The goal of Ruby documentation is to impart the most important
Expand Down
43 changes: 13 additions & 30 deletions vm_trace.c
Original file line number Diff line number Diff line change
Expand Up @@ -1117,15 +1117,18 @@ rb_tracearg_self(rb_trace_arg_t *trace_arg)
return trace_arg->self;
}

VALUE
rb_tracearg_return_value(rb_trace_arg_t *trace_arg)
static void
check_event_support(rb_trace_arg_t *trace_arg, rb_event_flag_t supported)
{
if (trace_arg->event & (RUBY_EVENT_RETURN | RUBY_EVENT_C_RETURN | RUBY_EVENT_B_RETURN)) {
/* ok */
}
else {
if (!(trace_arg->event & supported)) {
rb_raise(rb_eRuntimeError, "not supported by this event");
}
}

VALUE
rb_tracearg_return_value(rb_trace_arg_t *trace_arg)
{
check_event_support(trace_arg, RUBY_EVENT_RETURN | RUBY_EVENT_C_RETURN | RUBY_EVENT_B_RETURN);
if (UNDEF_P(trace_arg->data)) {
rb_bug("rb_tracearg_return_value: unreachable");
}
Expand All @@ -1135,12 +1138,7 @@ rb_tracearg_return_value(rb_trace_arg_t *trace_arg)
VALUE
rb_tracearg_raised_exception(rb_trace_arg_t *trace_arg)
{
if (trace_arg->event & (RUBY_EVENT_RAISE | RUBY_EVENT_RESCUE)) {
/* ok */
}
else {
rb_raise(rb_eRuntimeError, "not supported by this event");
}
check_event_support(trace_arg, RUBY_EVENT_RAISE | RUBY_EVENT_RESCUE);
if (UNDEF_P(trace_arg->data)) {
rb_bug("rb_tracearg_raised_exception: unreachable");
}
Expand All @@ -1152,12 +1150,7 @@ rb_tracearg_eval_script(rb_trace_arg_t *trace_arg)
{
VALUE data = trace_arg->data;

if (trace_arg->event & (RUBY_EVENT_SCRIPT_COMPILED)) {
/* ok */
}
else {
rb_raise(rb_eRuntimeError, "not supported by this event");
}
check_event_support(trace_arg, RUBY_EVENT_SCRIPT_COMPILED);
if (UNDEF_P(data)) {
rb_bug("rb_tracearg_eval_script: unreachable");
}
Expand All @@ -1176,12 +1169,7 @@ rb_tracearg_instruction_sequence(rb_trace_arg_t *trace_arg)
{
VALUE data = trace_arg->data;

if (trace_arg->event & (RUBY_EVENT_SCRIPT_COMPILED)) {
/* ok */
}
else {
rb_raise(rb_eRuntimeError, "not supported by this event");
}
check_event_support(trace_arg, RUBY_EVENT_SCRIPT_COMPILED);
if (UNDEF_P(data)) {
rb_bug("rb_tracearg_instruction_sequence: unreachable");
}
Expand All @@ -1201,12 +1189,7 @@ rb_tracearg_instruction_sequence(rb_trace_arg_t *trace_arg)
VALUE
rb_tracearg_object(rb_trace_arg_t *trace_arg)
{
if (trace_arg->event & (RUBY_INTERNAL_EVENT_NEWOBJ | RUBY_INTERNAL_EVENT_FREEOBJ)) {
/* ok */
}
else {
rb_raise(rb_eRuntimeError, "not supported by this event");
}
check_event_support(trace_arg, RUBY_INTERNAL_EVENT_NEWOBJ | RUBY_INTERNAL_EVENT_FREEOBJ);
if (UNDEF_P(trace_arg->data)) {
rb_bug("rb_tracearg_object: unreachable");
}
Expand Down
78 changes: 67 additions & 11 deletions zjit/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,10 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
&Insn::Send { cd, block: Some(BlockHandler::BlockIseq(blockiseq)), state, reason, .. } => gen_send(jit, asm, cd, blockiseq, &function.frame_state(state), reason),
&Insn::Send { cd, block: Some(BlockHandler::BlockArg), state, reason, .. } => gen_send(jit, asm, cd, std::ptr::null(), &function.frame_state(state), reason),
&Insn::SendForward { cd, blockiseq, state, reason, .. } => gen_send_forward(jit, asm, cd, blockiseq, &function.frame_state(state), reason),
Insn::SendDirect { cme, iseq, recv, args, kw_bits, block, state, .. } => gen_send_iseq_direct(cb, jit, asm, *cme, *iseq, opnd!(recv), opnds!(args), *kw_bits, &function.frame_state(*state), *block),
&Insn::SendDirect { cme, iseq, recv, ref args, kw_bits, block, state, .. } => gen_send_iseq_direct(
cb, jit, asm, cme, iseq, opnd!(recv), opnds!(args),
kw_bits, &function.frame_state(state), block,
),
&Insn::InvokeSuper { cd, blockiseq, state, reason, .. } => gen_invokesuper(jit, asm, cd, blockiseq, &function.frame_state(state), reason),
&Insn::InvokeSuperForward { cd, blockiseq, state, reason, .. } => gen_invokesuperforward(jit, asm, cd, blockiseq, &function.frame_state(state), reason),
&Insn::InvokeBlock { cd, state, reason, .. } => gen_invokeblock(jit, asm, cd, &function.frame_state(state), reason),
Expand Down Expand Up @@ -1669,7 +1672,7 @@ fn gen_send_iseq_direct(
};

// Make a method call. The target address will be rewritten once compiled.
let iseq_call = IseqCall::new(iseq, num_optionals_passed);
let iseq_call = IseqCall::new(iseq, num_optionals_passed.try_into().expect("checked in HIR"), args.len().try_into().expect("checked in HIR"));
let dummy_ptr = cb.get_write_ptr().raw_ptr(cb);
jit.iseq_calls.push(iseq_call.clone());
let ret = asm.ccall_with_iseq_call(dummy_ptr, c_args, &iseq_call);
Expand Down Expand Up @@ -3081,22 +3084,71 @@ c_callable! {
// mutability (Cell<IseqPtr>) requires exclusive access.
let iseq_call = unsafe { Rc::from_raw(iseq_call_ptr as *const IseqCall) };
let iseq = iseq_call.iseq.get();
let argc = iseq_call.argc;
let num_opts_filled = iseq_call.jit_entry_idx;

// JIT-to-JIT calls don't eagerly fill nils to non-parameter locals.
// If we side-exit from function_stub_hit (before JIT code runs), we need to set them here.
fn prepare_for_exit(iseq: IseqPtr, cfp: CfpPtr, sp: *mut VALUE, compile_error: &CompileError) {
fn prepare_for_exit(iseq: IseqPtr, cfp: CfpPtr, sp: *mut VALUE, argc: u16, num_opts_filled: u16, compile_error: &CompileError) {
unsafe {
// Caller frames are materialized by jit_exec() after the entry trampoline returns.
// The current frame's pc and iseq are already set by function_stub_hit before this point.

// Set SP which gen_push_frame() doesn't set
rb_set_cfp_sp(cfp, sp);

// Fill nils to uninitialized (non-argument) locals
let local_size = get_iseq_body_local_table_size(iseq).to_usize();
let num_params = iseq.params().size.to_usize();
let base = sp.offset(-local_size_and_idx_to_bp_offset(local_size, num_params) as isize);
slice::from_raw_parts_mut(base, local_size - num_params).fill(Qnil);
let params = iseq.params();
let params_size = params.size.to_usize();
let frame_base = sp.offset(-local_size_and_idx_to_bp_offset(local_size, 0) as isize);
let locals = slice::from_raw_parts_mut(frame_base, local_size);
// Fill nils to uninitialized (non-parameter) locals
locals.get_mut(params_size..).unwrap_or_default().fill(Qnil);

// SendDirect packs args without gaps for unfilled optionals.
// When we exit to the interpreter, we need to shift args right
// to create the gap and nil-fill the unfilled optional slots.
//
// Example: def target(req, a = a, b = b, kw:); target(1, kw: 2)
// lead_num=1, opt_num=2, opts_filled=0, argc=2
//
// locals[] as placed by SendDirect (argc=2, no gaps):
// [req, kw_val, ?, ?, ?, ...]
// 0 1
// ^----caller's args----^
//
// locals[] expected by interpreter (params_size=4):
// [req, a, b, kw_val, ?, ...]
// 0 1 2 3
// ^nil ^nil^--moved--^
//
// gap_start = lead_num + opts_filled = 1
// gap_end = lead_num + opt_num = 3
// We move locals[gap_start..argc] to locals[gap_end..], then
// nil-fill locals[gap_start..gap_end].
let opt_num: usize = params.opt_num.try_into().expect("ISEQ opt_num should be non-negative");
let opts_filled = num_opts_filled.to_usize();
let opts_unfilled = opt_num.saturating_sub(opts_filled);
if opts_unfilled > 0 {
let argc = argc.to_usize();
let lead_num: usize = params.lead_num.try_into().expect("ISEQ lead_num should be non-negative");
let param_locals = &mut locals[..params_size];
// Gap of unspecified optional parameters
let gap_start = lead_num + opts_filled;
let gap_end = lead_num + opt_num;
// When there are arguments in the gap, shift them past the gap
let args_overlapping_gap = gap_start..argc;
if !args_overlapping_gap.is_empty() {
assert!(
gap_end.checked_add(args_overlapping_gap.len())
.is_some_and(|new_end| new_end <= param_locals.len()) ,
"shift past gap out-of-bounds. params={params:#?} args_overlapping_gap={args_overlapping_gap:?}"
);
param_locals.copy_within(args_overlapping_gap, gap_end);
}
// Nil-fill the now-vacant optional parameter slots
param_locals[gap_start..gap_end].fill(Qnil);
}
}

// Increment a compile error counter for --zjit-stats
Expand Down Expand Up @@ -3126,7 +3178,7 @@ c_callable! {
// We'll use this Rc again, so increment the ref count decremented by from_raw.
unsafe { Rc::increment_strong_count(iseq_call_ptr as *const IseqCall); }

prepare_for_exit(iseq, cfp, sp, compile_error);
prepare_for_exit(iseq, cfp, sp, argc, num_opts_filled, compile_error);
return ZJITState::get_exit_trampoline_with_counter().raw_ptr(cb);
}

Expand All @@ -3141,7 +3193,7 @@ c_callable! {
// We'll use this Rc again, so increment the ref count decremented by from_raw.
unsafe { Rc::increment_strong_count(iseq_call_ptr as *const IseqCall); }

prepare_for_exit(iseq, cfp, sp, &compile_error);
prepare_for_exit(iseq, cfp, sp, argc, num_opts_filled, &compile_error);
ZJITState::get_exit_trampoline_with_counter()
});
cb.mark_all_executable();
Expand Down Expand Up @@ -3466,7 +3518,10 @@ pub struct IseqCall {
pub iseq: Cell<IseqPtr>,

/// Index that corresponds to [crate::hir::jit_entry_insns]
jit_entry_idx: u32,
jit_entry_idx: u16,

/// Argument count passing to the HIR function
argc: u16,

/// Position where the call instruction starts
start_addr: Cell<Option<CodePtr>>,
Expand All @@ -3479,12 +3534,13 @@ pub type IseqCallRef = Rc<IseqCall>;

impl IseqCall {
/// Allocate a new IseqCall
fn new(iseq: IseqPtr, jit_entry_idx: u32) -> IseqCallRef {
fn new(iseq: IseqPtr, jit_entry_idx: u16, argc: u16) -> IseqCallRef {
let iseq_call = IseqCall {
iseq: Cell::new(iseq),
start_addr: Cell::new(None),
end_addr: Cell::new(None),
jit_entry_idx,
argc,
};
Rc::new(iseq_call)
}
Expand Down
6 changes: 6 additions & 0 deletions zjit/src/hir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2417,6 +2417,12 @@ fn can_direct_send(function: &mut Function, block: BlockId, iseq: *const rb_iseq
return false;
}

// IseqCall stores num_optionals_passed and argc as u16
if u16::try_from(args.len()).is_err() {
function.set_dynamic_send_reason(send_insn, TooManyArgsForLir);
return false;
}

can_send
}

Expand Down
45 changes: 45 additions & 0 deletions zjit/src/hir/opt_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14960,6 +14960,51 @@ mod hir_opt_tests {
");
}

#[test]
fn test_exit_from_function_stub_for_opt_keyword_callee() {
// We have a SendDirect to a callee that fails to compile,
// so the function stub has to take care of exiting to
// interpreter.
eval("
def target(a = binding.local_variable_get(:a), b: nil)
::RubyVM::ZJIT.induce_compile_failure!
[a, b]
end

def entry = target(b: -1)

raise 'wrong' unless [nil, -1] == entry
raise 'wrong' unless [nil, -1] == entry
");

crate::hir::tests::hir_build_tests::assert_compile_fails("target", ParseError::DirectiveInduced);
let hir = hir_string("entry");
assert!(hir.contains("SendDirect"), "{hir}");
}

#[test]
fn test_exit_from_function_stub_for_lead_opt() {
// We have a SendDirect to a callee that fails to compile,
// so the function stub has to take care of exiting to
// interpreter.
let result = eval("
def target(_required, a = a, b = b)
::RubyVM::ZJIT.induce_compile_failure!
a
end

def entry = target(1)

entry
entry
");
assert_eq!(Qnil, result);

crate::hir::tests::hir_build_tests::assert_compile_fails("target", ParseError::DirectiveInduced);
let hir = hir_string("entry");
assert!(hir.contains("SendDirect"), "{hir}");
}

#[test]
fn test_recompile_no_profile_send() {
// Test the SideExit → recompile flow: a no-profile send becomes a SideExit,
Expand Down
4 changes: 2 additions & 2 deletions zjit/src/hir/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ mod snapshot_tests {
}

#[cfg(test)]
pub mod hir_build_tests {
pub(crate) mod hir_build_tests {
use super::*;
use crate::options::set_call_threshold;
use insta::assert_snapshot;
Expand Down Expand Up @@ -275,7 +275,7 @@ pub mod hir_build_tests {
}

#[track_caller]
fn assert_compile_fails(method: &str, reason: ParseError) {
pub fn assert_compile_fails(method: &str, reason: ParseError) {
let iseq = crate::cruby::with_rubyvm(|| get_method_iseq("self", method));
unsafe { crate::cruby::rb_zjit_profile_disable(iseq) };
let result = iseq_to_hir(iseq);
Expand Down