Skip to content
27 changes: 27 additions & 0 deletions doc/zjit.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,33 @@ To run code snippets with ZJIT:
You can also try https://www.rubyexplorer.xyz/ to view Ruby YARV disasm output with syntax highlighting
in a way that can be easily shared with other team members.

## Understanding Ruby Stacks

Ruby execution involves three distinct stacks and understanding them will help you understand ZJIT's implementation:

### 1. Native Stack

- **Purpose**: Return addresses and saved registers. ZJIT also uses it for some C functions' argument arrays
- **Management**: OS-managed, one per native thread
- **Growth**: Downward from high addresses
- **Constants**: `NATIVE_STACK_PTR`, `NATIVE_BASE_PTR`

### 2. Ruby VM Stack

The Ruby VM uses a single contiguous memory region (`ec->vm_stack`) containing two sub-stacks that grow toward each other. When they meet, stack overflow occurs.

**Control Frame Stack:**

- **Stores**: Frame metadata (`rb_control_frame_t` structures)
- **Growth**: Downward from `vm_stack + size` (high addresses)
- **Constants**: `CFP`

**Value Stack:**

- **Stores**: YARV bytecode operands (self, arguments, locals, temporaries)
- **Growth**: Upward from `vm_stack` (low addresses)
- **Constants**: `SP`

## ZJIT Glossary

This glossary contains terms that are helpful for understanding ZJIT.
Expand Down
2 changes: 1 addition & 1 deletion lib/error_highlight/core_ext.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module ErrorHighlight
module CoreExt
private def generate_snippet
if ArgumentError === self && message =~ /\A(?:wrong number of arguments|missing keyword|unknown keyword|no keywords accepted)\b/
if ArgumentError === self && message =~ /\A(?:wrong number of arguments|missing keyword[s]?|unknown keyword[s]?|no keywords accepted)\b/
locs = self.backtrace_locations
return "" if locs.size < 2
callee_loc, caller_loc = locs
Expand Down
44 changes: 43 additions & 1 deletion test/error_highlight/test_error_highlight.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def preprocess(msg)
def assert_error_message(klass, expected_msg, &blk)
omit unless klass < ErrorHighlight::CoreExt
err = assert_raise(klass, &blk)
unless klass == ArgumentError && err.message =~ /\A(?:wrong number of arguments|missing keyword|unknown keyword|no keywords accepted)\b/
unless klass == ArgumentError && err.message =~ /\A(?:wrong number of arguments|missing keyword[s]?|unknown keyword[s]?|no keywords accepted)\b/
spot = ErrorHighlight.spot(err)
if spot
assert_kind_of(Integer, spot[:first_lineno])
Expand Down Expand Up @@ -1502,6 +1502,27 @@ def test_missing_keyword
end
end

def test_missing_keywords # multiple missing keywords
lineno = __LINE__
assert_error_message(ArgumentError, <<~END) do
missing keywords: :kw2, :kw3 (ArgumentError)

caller: #{ __FILE__ }:#{ lineno + 16 }
| keyword_test(kw1: 1)
^^^^^^^^^^^^
callee: #{ __FILE__ }:#{ KEYWORD_TEST_LINENO }
#{
MethodDefLocationSupported ?
"| def keyword_test(kw1:, kw2:, kw3:)
^^^^^^^^^^^^" :
"(cannot highlight method definition; try Ruby 3.5 or later)"
}
END

keyword_test(kw1: 1)
end
end

def test_unknown_keyword
lineno = __LINE__
assert_error_message(ArgumentError, <<~END) do
Expand All @@ -1523,6 +1544,27 @@ def test_unknown_keyword
end
end

def test_unknown_keywords
lineno = __LINE__
assert_error_message(ArgumentError, <<~END) do
unknown keywords: :kw4, :kw5 (ArgumentError)

caller: #{ __FILE__ }:#{ lineno + 16 }
| keyword_test(kw1: 1, kw2: 2, kw3: 3, kw4: 4, kw5: 5)
^^^^^^^^^^^^
callee: #{ __FILE__ }:#{ KEYWORD_TEST_LINENO }
#{
MethodDefLocationSupported ?
"| def keyword_test(kw1:, kw2:, kw3:)
^^^^^^^^^^^^" :
"(cannot highlight method definition; try Ruby 3.5 or later)"
}
END

keyword_test(kw1: 1, kw2: 2, kw3: 3, kw4: 4, kw5: 5)
end
end

WRONG_NUBMER_OF_ARGUMENTS_TEST2_LINENO = __LINE__ + 1
def wrong_number_of_arguments_test2(
long_argument_name_x,
Expand Down
30 changes: 30 additions & 0 deletions test/ruby/test_string.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3777,6 +3777,36 @@ class MyError < StandardError; end
end
end

def test_encode_fallback_too_big_memory_leak
{
"hash" => <<~RUBY,
fallback = Hash.new { "\\uffee" }
RUBY
"proc" => <<~RUBY,
fallback = proc { "\\uffee" }
RUBY
"method" => <<~RUBY,
def my_method(_str) = "\\uffee"
fallback = method(:my_method)
RUBY
"aref" => <<~RUBY,
fallback = Object.new
def fallback.[](_str) = "\\uffee"
RUBY
}.each do |type, code|
assert_no_memory_leak([], '', <<~RUBY, "fallback type is #{type}", rss: true)
class MyError < StandardError; end

#{code}

100_000.times do |i|
"\\ufffd".encode(Encoding::US_ASCII, fallback:)
rescue ArgumentError
end
RUBY
end
end

private

def assert_bytesplice_result(expected, s, *args)
Expand Down
1 change: 1 addition & 0 deletions transcode.c
Original file line number Diff line number Diff line change
Expand Up @@ -2432,6 +2432,7 @@ transcode_loop(const unsigned char **in_pos, unsigned char **out_pos,
ret = rb_econv_insert_output(ec, (const unsigned char *)RSTRING_PTR(rep),
RSTRING_LEN(rep), rb_enc_name(rb_enc_get(rep)));
if ((int)ret == -1) {
rb_econv_close(ec);
rb_raise(rb_eArgError, "too big fallback string");
}
goto resume;
Expand Down
29 changes: 18 additions & 11 deletions zjit/src/backend/arm64/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,7 @@ impl Assembler {

/// Split instructions using scratch registers. To maximize the use of the register pool for
/// VRegs, most splits should happen in [`Self::arm64_split`]. However, some instructions
/// need to be split with registers after `alloc_regs`, e.g. for `compile_side_exits`, so this
/// need to be split with registers after `alloc_regs`, e.g. for `compile_exits`, so this
/// splits them and uses scratch registers for it.
fn arm64_scratch_split(self) -> Assembler {
let mut asm = Assembler::new_with_asm(&self);
Expand All @@ -706,7 +706,7 @@ impl Assembler {
asm.push_insn(Insn::RShift { out: SCRATCH0_OPND, opnd: out, shift: Opnd::UImm(63) });
}
}
// For compile_side_exits, support splitting simple C arguments here
// For compile_exits, support splitting simple C arguments here
Insn::CCall { opnds, .. } if !opnds.is_empty() => {
for (i, opnd) in opnds.iter().enumerate() {
asm.load_into(C_ARG_OPNDS[i], *opnd);
Expand All @@ -716,7 +716,7 @@ impl Assembler {
}
&mut Insn::Lea { opnd, out } => {
match (opnd, out) {
// Split here for compile_side_exits
// Split here for compile_exits
(Opnd::Mem(_), Opnd::Mem(_)) => {
asm.lea_into(SCRATCH0_OPND, opnd);
asm.store(out, SCRATCH0_OPND);
Expand All @@ -728,7 +728,7 @@ impl Assembler {
}
&mut Insn::IncrCounter { mem, value } => {
// Convert Opnd::const_ptr into Opnd::Mem.
// It's split here to support IncrCounter in compile_side_exits.
// It's split here to support IncrCounter in compile_exits.
assert!(matches!(mem, Opnd::UImm(_)));
asm.load_into(SCRATCH0_OPND, mem);
asm.lea_into(SCRATCH0_OPND, Opnd::mem(64, SCRATCH0_OPND, 0));
Expand Down Expand Up @@ -872,7 +872,7 @@ impl Assembler {
});
},
Target::SideExit { .. } => {
unreachable!("Target::SideExit should have been compiled by compile_side_exits")
unreachable!("Target::SideExit should have been compiled by compile_exits")
},
};
}
Expand Down Expand Up @@ -974,11 +974,14 @@ impl Assembler {
// The write_pos for the last Insn::PatchPoint, if any
let mut last_patch_pos: Option<usize> = None;

// Install a panic hook to dump Assembler with insn_idx on dev builds
let (_hook, mut hook_insn_idx) = AssemblerPanicHook::new(self, 0);

// For each instruction
let mut insn_idx: usize = 0;
while let Some(insn) = self.insns.get(insn_idx) {
// Dump Assembler with insn_idx if --zjit-dump-lir=panic is given
let _hook = AssemblerPanicHook::new(self, insn_idx);
// Update insn_idx that is shown on panic
hook_insn_idx.as_mut().map(|idx| idx.lock().map(|mut idx| *idx = insn_idx).unwrap());

match insn {
Insn::Comment(text) => {
Expand Down Expand Up @@ -1346,7 +1349,7 @@ impl Assembler {
});
},
Target::SideExit { .. } => {
unreachable!("Target::SideExit should have been compiled by compile_side_exits")
unreachable!("Target::SideExit should have been compiled by compile_exits")
},
};
},
Expand Down Expand Up @@ -1468,9 +1471,9 @@ impl Assembler {
let mut asm = asm.alloc_regs(regs)?;
asm_dump!(asm, alloc_regs);

// We put compile_side_exits after alloc_regs to avoid extending live ranges for VRegs spilled on side exits.
asm.compile_side_exits();
asm_dump!(asm, compile_side_exits);
// We put compile_exits after alloc_regs to avoid extending live ranges for VRegs spilled on side exits.
asm.compile_exits();
asm_dump!(asm, compile_exits);

if use_scratch_reg {
asm = asm.arm64_scratch_split();
Expand Down Expand Up @@ -1561,9 +1564,11 @@ mod tests {
asm.store(Opnd::mem(64, SP, 0x10), val64);
let side_exit = Target::SideExit { reason: SideExitReason::Interrupt, pc: 0 as _, stack: vec![], locals: vec![], label: None };
asm.push_insn(Insn::Joz(val64, side_exit));
asm.parallel_mov(vec![(C_ARG_OPNDS[0], C_RET_OPND.with_num_bits(32)), (C_ARG_OPNDS[1], Opnd::mem(64, SP, -8))]);

let val32 = asm.sub(Opnd::Value(Qtrue), Opnd::Imm(1));
asm.store(Opnd::mem(64, EC, 0x10).with_num_bits(32), val32.with_num_bits(32));
asm.je(label);
asm.cret(val64);

asm.frame_teardown(JIT_PRESERVED_REGS);
Expand All @@ -1574,8 +1579,10 @@ mod tests {
v0 = Add x19, 0x40
Store [x21 + 0x10], v0
Joz Exit(Interrupt), v0
ParallelMov x0 <- w0, x1 <- [x21 - 8]
v1 = Sub Value(0x14), Imm(1)
Store Mem32[x20 + 0x10], VReg32(v1)
Je bb0
CRet v0
FrameTeardown x19, x21, x20
");
Expand Down
Loading