diff --git a/common.mk b/common.mk index 1c3ad312e587b7..be27c434ab63bf 100644 --- a/common.mk +++ b/common.mk @@ -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) @@ -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 diff --git a/doc/contributing/documentation_guide.md b/doc/contributing/documentation_guide.md index 9945ab57fbf28a..7c73ad1c50b0f2 100644 --- a/doc/contributing/documentation_guide.md +++ b/doc/contributing/documentation_guide.md @@ -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 diff --git a/vm_trace.c b/vm_trace.c index 74bc1fce807b83..42b9991e7141bc 100644 --- a/vm_trace.c +++ b/vm_trace.c @@ -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"); } @@ -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"); } @@ -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"); } @@ -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"); } @@ -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"); } diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 9846a0e70a23f1..b2b1db1cf010d9 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -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), @@ -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); @@ -3081,10 +3084,12 @@ c_callable! { // mutability (Cell) 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. @@ -3092,11 +3097,58 @@ c_callable! { // 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 @@ -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); } @@ -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(); @@ -3466,7 +3518,10 @@ pub struct IseqCall { pub iseq: Cell, /// 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>, @@ -3479,12 +3534,13 @@ pub type IseqCallRef = Rc; 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) } diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index b0a53ec56493f4..c50ba467750811 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -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 } diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index c9f2a45b048adc..60b0f72da5c8db 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -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, diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index 72012b7eba9504..706afea9a896fa 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -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; @@ -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);