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
50 changes: 50 additions & 0 deletions test/ruby/test_zjit_cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,56 @@ def array.itself = :not_itself
RUBY
end

def test_send_forwarded_block_arg_nil_then_non_nil
# Regression test: when a forwarded &block arg is profiled as nil, the nil
# block optimization must update the frame state to match the stripped args.
# Otherwise the saved SP is off by one, causing a stack consistency error
# when the guard side-exits for a non-nil block.
assert_runs ':ok', <<~RUBY, call_threshold: 2
def inner(callable = nil, &block)
callable || block
end

def outer(&block)
inner(&block)
end

100.times { outer }
result = outer { |x| x }
result.is_a?(Proc) ? :ok : :fail
RUBY
end

def test_send_forwarded_nil_block_arg_with_polymorphic_receiver
# Regression test: the nil block optimization strips the block arg from the
# frame state used to set up the callee frame, but the pre-call guards
# (receiver GuardType, method-redefinition PatchPoint) must keep using the
# original frame state that still has the block arg on the stack. Otherwise a
# guard side-exit re-executes the send with a stack that is missing the block
# arg slot, corrupting the pushed frame's EP (VM_ENV_FLAGS assertion failure).
# A polymorphic receiver forces the receiver GuardType to side-exit.
assert_runs ':ok', <<~RUBY, call_threshold: 2
class Base
def self.inner(model, name, &block)
block ? block.call : model
end
def self.outer(model, name, &block)
inner(model, name, &block)
end
end
class A < Base; end
class B < Base; end
class C < Base; end
class D < Base; end

1000.times do |i|
klass = [A, B, C, D][i % 4]
klass.outer(i, :n)
end
:ok
RUBY
end

private

# Assert that every method call in `test_script` can be compiled by ZJIT
Expand Down
9 changes: 7 additions & 2 deletions tool/mk_builtin_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ def visit_call_node(source, node, name, locals, requires, bs, inlines)
# an inline function, and then continue the visit.
raise "argc (#{argc}) of inline! should be 1" if argc != 1

text = extract_string_literal(args[0]).rstrip
# The string literal keeps the line endings of the source file; normalize
# them so the generated code does not depend on how it was checked out.
text = extract_string_literal(args[0]).encode(universal_newline: true).rstrip
lineno = line_number(source, node)

case primitive_macro
Expand Down Expand Up @@ -184,7 +186,10 @@ def collect_builtins(dump_ast, file)
exit(1)
end

source = File.read(file)
# dump_ast reports byte offsets against the raw file bytes, so the source
# must be read in binary mode; text mode would convert CRLF to LF on Windows
# and shift every offset.
source = File.binread(file)
root = JSON.parse(stdout)
visit_node(source, root, "top", nil, requires = [], builtins = [], inlines = [])

Expand Down
24 changes: 4 additions & 20 deletions zjit/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -648,11 +648,11 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
&Insn::Send { cd, block: Some(BlockHandler::BlockArg), state, reason, .. } => gen_send(jit, asm, function, cd, std::ptr::null(), &function.frame_state(state), reason),
&Insn::SendForward { cd, blockiseq, state, reason, .. } => gen_send_forward(jit, asm, function, cd, blockiseq, &function.frame_state(state), reason),
Insn::SendDirect(insn) => {
let SendDirectData { cme, iseq, recv, args, kw_bits, block, state, .. } = &**insn;
let SendDirectData { cme, iseq, recv, args, kw_bits, jit_entry_idx, block, state, .. } = &**insn;
gen_send_iseq_direct(
cb, jit, asm,
function, *cme, *iseq, opnd!(recv), opnds!(args),
*kw_bits, &function.frame_state(*state), *block,
*kw_bits, *jit_entry_idx, &function.frame_state(*state), *block,
)
}
Insn::PushInlineFrame { cme, iseq, recv, args, blockiseq, state, .. } => {
Expand Down Expand Up @@ -1685,6 +1685,7 @@ fn gen_send_iseq_direct(
recv: Opnd,
args: Vec<Opnd>,
kw_bits: u32,
jit_entry_idx: u16,
state: &FrameState,
block: Option<BlockHandler>,
) -> lir::Opnd {
Expand Down Expand Up @@ -1788,25 +1789,8 @@ fn gen_send_iseq_direct(
}
}

let num_optionals_passed = if params.flags.has_opt() != 0 {
// See vm_call_iseq_setup_normal_opt_start in vm_inshelper.c
let lead_num = params.lead_num as u32;
let opt_num = params.opt_num as u32;
let post_num = params.post_num as u32;
let keyword = params.keyword;
let kw_total_num = if keyword.is_null() { 0 } else { unsafe { (*keyword).num } } as u32;
assert!(args.len() as u32 <= lead_num + opt_num + post_num + kw_total_num);
// For computing optional positional entry point, only count positional args
// and exclude the always-present lead and post slots.
let positional_argc = args.len() as u32 - kw_total_num;
let num_optionals_passed = positional_argc.saturating_sub(lead_num + post_num);
num_optionals_passed
} else {
0
};

// Make a method call. The target address will be rewritten once compiled.
let iseq_call = IseqCall::new(iseq, num_optionals_passed.try_into().expect("checked in HIR"), args.len().try_into().expect("checked in HIR"));
let iseq_call = IseqCall::new(iseq, jit_entry_idx, 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
113 changes: 113 additions & 0 deletions zjit/src/codegen_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,96 @@ fn test_send_optional_arguments() {
"), @"[[1, 2], [3, 4]]");
}

#[test]
fn test_send_rest_arguments() {
eval("
def test(*args) = args
def entry = test(1, 2, 3)
entry
");
assert_snapshot!(assert_compiles("entry"), @"[1, 2, 3]");
}

#[test]
fn test_send_many_rest_arguments() {
eval("
def test(*args) = args.length
def entry = test(1, 2, 3, 4, 5, 6, 7)
entry
");
assert_snapshot!(assert_compiles("entry"), @"7");
}

#[test]
fn test_send_rest_arguments_with_post() {
eval("
def test(a, *args, z) = [a, args, z]
def entry = test(1, 2, 3, 4)
entry
");
assert_snapshot!(assert_compiles("entry"), @"[1, [2, 3], 4]");
}

#[test]
fn test_send_rest_arguments_with_keyword() {
eval("
def test(*args, k:) = [args, k]
def entry = test(1, 2, k: 40)
entry
");
assert_snapshot!(assert_compiles("entry"), @"[[1, 2], 40]");
}

#[test]
fn test_send_rest_arguments_with_optional_keyword_default() {
eval("
def test(*args, k: 40) = [args, k]
def entry = test(1, 2)
entry
");
assert_snapshot!(assert_compiles("entry"), @"[[1, 2], 40]");
}

#[test]
fn test_send_optional_and_rest_arguments() {
eval("
def test(a, b = 2, *rest) = [a, b, rest]
def entry = [test(1), test(3, 4), test(5, 6, 7, 8)]
entry
");
assert_snapshot!(assert_compiles("entry"), @"[[1, 2, []], [3, 4, []], [5, 6, [7, 8]]]");
}

#[test]
fn test_send_rest_arguments_with_keyword_to_positional_hash() {
eval("
def test(*args) = args
def entry = test(k: 1)
entry
");
assert_snapshot!(assert_compiles("entry"), @"[{k: 1}]");
}

#[test]
fn test_send_rest_arguments_with_block_literal() {
eval("
def test(*args) = yield args.length
def entry = test(1, 2, 3) { |n| n + 4 }
entry
");
assert_snapshot!(assert_compiles("entry"), @"7");
}

#[test]
fn test_send_rest_arguments_with_block_param() {
eval("
def test(*args, &block) = block.call(args.length)
def entry = test(1, 2, 3) { |n| n + 5 }
entry
");
assert_snapshot!(assert_compiles("entry"), @"8");
}

#[test]
fn test_send_nil_block_arg() {
assert_snapshot!(inspect("
Expand Down Expand Up @@ -4169,6 +4259,29 @@ fn test_setinstancevariable() {
"), @"1");
}

#[test]
fn test_polymorphic_setinstancevariable_with_shape_transitions() {
set_call_threshold(3);
assert_snapshot!(inspect(r#"
class C
def set(value) = @a = value
end

normal = C.new
with_b = C.new
with_b.instance_variable_set(:@b, true)
normal.set(:profile_normal)
with_b.set(:profile_with_b)

normal = C.new
with_b = C.new
with_b.instance_variable_set(:@b, true)
results = [normal.set(:normal), with_b.set(:with_b)]
results << normal.instance_variable_get(:@a)
results << with_b.instance_variable_get(:@a)
"#), @"[:normal, :with_b, :normal, :with_b]");
}

#[test]
fn test_getclassvariable() {
assert_snapshot!(inspect("
Expand Down
Loading