summaryrefslogtreecommitdiff
path: root/zjit/src/jit_frame.rs
diff options
context:
space:
mode:
Diffstat (limited to 'zjit/src/jit_frame.rs')
-rw-r--r--zjit/src/jit_frame.rs314
1 files changed, 314 insertions, 0 deletions
diff --git a/zjit/src/jit_frame.rs b/zjit/src/jit_frame.rs
new file mode 100644
index 0000000000..8691833db0
--- /dev/null
+++ b/zjit/src/jit_frame.rs
@@ -0,0 +1,314 @@
+use crate::cruby::{IseqPtr, VALUE, rb_gc_mark_movable, rb_gc_location};
+use crate::cruby::zjit_jit_frame;
+use crate::codegen::iseq_may_write_block_code;
+use crate::state::ZJITState;
+
+/// JITFrame struct is defined in zjit.h (the single source of truth) and
+/// imported into Rust via bindgen. See zjit.h for field documentation.
+pub type JITFrame = zjit_jit_frame;
+
+impl JITFrame {
+ /// Allocate a JITFrame on the heap, register it with ZJITState, and return
+ /// a raw pointer that remains valid for the lifetime of the process.
+ fn alloc(jit_frame: JITFrame) -> *const Self {
+ let raw_ptr = Box::into_raw(Box::new(jit_frame));
+ ZJITState::get_jit_frames().push(raw_ptr);
+ raw_ptr as *const _
+ }
+
+ /// Create a JITFrame for an ISEQ frame.
+ pub fn new_iseq(pc: *const VALUE, iseq: IseqPtr) -> *const Self {
+ let materialize_block_code = !iseq_may_write_block_code(iseq);
+ Self::alloc(JITFrame { pc, iseq, materialize_block_code })
+ }
+
+ /// Mark the iseq pointer for GC. Called from rb_zjit_root_mark.
+ pub fn mark(&self) {
+ if !self.iseq.is_null() {
+ unsafe { rb_gc_mark_movable(VALUE::from(self.iseq)); }
+ }
+ }
+
+ /// Update the iseq pointer after GC compaction.
+ pub fn update_references(&mut self) {
+ if !self.iseq.is_null() {
+ let new_iseq = unsafe { rb_gc_location(VALUE::from(self.iseq)) }.as_iseq();
+ if self.iseq != new_iseq {
+ self.iseq = new_iseq;
+ }
+ }
+ }
+}
+
+/// Update the iseq pointer in an on-stack JITFrame during GC compaction.
+/// Called from rb_execution_context_update in vm.c.
+#[unsafe(no_mangle)]
+pub extern "C" fn rb_zjit_jit_frame_update_references(jit_frame: *mut JITFrame) {
+ unsafe { &mut *jit_frame }.update_references();
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::cruby::{eval, inspect};
+ use insta::assert_snapshot;
+
+ #[test]
+ fn test_jit_frame_entry_first() {
+ eval(r#"
+ def test
+ itself
+ callee
+ end
+
+ def callee
+ caller
+ end
+
+ test
+ "#);
+ assert_snapshot!(inspect("test.first"), @r#""<compiled>:4:in 'Object#test'""#);
+ }
+
+ #[test]
+ fn test_materialize_one_frame() {
+ assert_snapshot!(inspect("
+ def jit_entry
+ raise rescue 1
+ end
+ jit_entry
+ jit_entry
+ "), @"1");
+ }
+
+ #[test]
+ fn test_materialize_two_frames() { // materialize caller frames on raise
+ // At the point of `resuce`, there are two lightweight frames on stack and both need to be
+ // materialized before passing control to interpreter.
+ assert_snapshot!(inspect("
+ def jit_entry = raise_and_rescue
+ def raise_and_rescue
+ raise rescue 1
+ end
+ jit_entry
+ jit_entry
+ "), @"1");
+ }
+
+ // Materialize frames on side exit: a type guard triggers a side exit with
+ // multiple JIT frames on the stack. All frames must be materialized before
+ // the interpreter resumes.
+ #[test]
+ fn test_side_exit_materialize_frames() {
+ assert_snapshot!(inspect("
+ def side_exit(n) = 1 + n
+ def jit_frame(n) = 1 + side_exit(n)
+ def entry(n) = jit_frame(n)
+ entry(2)
+ [entry(2), entry(2.0)]
+ "), @"[4, 4.0]");
+ }
+
+ // BOP invalidation must not overwrite the top-most frame's PC with
+ // jit_frame's PC. After invalidation the interpreter resumes at a new
+ // PC, so a stale jit_frame PC would cause wrong execution.
+ #[test]
+ fn test_bop_invalidation() {
+ assert_snapshot!(inspect(r#"
+ def test
+ eval("class Integer; def +(_) = 100; end")
+ 1 + 2
+ end
+ test
+ test
+ "#), @"100");
+ }
+
+ // Side exit at the very start of a method, before gen_save_pc_for_gc has
+ // updated the entry JITFrame.
+ #[test]
+ fn test_side_exit_before_jit_frame_update() {
+ assert_snapshot!(inspect("
+ def entry(n) = n + 1
+ entry(1)
+ [entry(1), entry(1.0)]
+ "), @"[2, 2.0]");
+ }
+
+ #[test]
+ fn test_caller_iseq() {
+ assert_snapshot!(inspect(r#"
+ def callee = call_caller
+ def test = callee
+
+ def callee2 = call_caller
+ def test2 = callee2
+
+ def call_caller = caller
+
+ test
+ test2
+ test.first
+ "#), @r#""<compiled>:2:in 'Object#callee'""#);
+ }
+
+ // ISEQ must be readable during exception handling so the interpreter
+ // can look up rescue/ensure tables.
+ #[test]
+ fn test_iseq_on_raise() {
+ assert_snapshot!(inspect(r#"
+ def jit_entry(v) = make_range_then_exit(v)
+ def make_range_then_exit(v)
+ range = (v..1)
+ super rescue range
+ end
+ jit_entry(0)
+ jit_entry(0)
+ jit_entry(0/1r)
+ "#), @"(0/1)..1");
+ }
+
+ // Multiple exception raises during keyword argument evaluation: each
+ // raise needs correct ISEQ for catch table lookup.
+ #[test]
+ fn test_iseq_on_raise_on_ensure() {
+ assert_snapshot!(inspect(r#"
+ def raise_a = raise "a"
+ def raise_b = raise "b"
+ def raise_c = raise "c"
+
+ def foo(a: raise_a, b: raise_b, c: raise_c)
+ [a, b, c]
+ end
+
+ def test_a
+ foo(b: 2, c: 3)
+ rescue RuntimeError => e
+ e.message
+ end
+
+ def test_b
+ foo(a: 1, c: 3)
+ rescue RuntimeError => e
+ e.message
+ end
+
+ def test_c
+ foo(a: 1, b: 2)
+ rescue RuntimeError => e
+ e.message
+ end
+
+ def test
+ [test_a, test_b, test_c]
+ end
+
+ test
+ test
+ "#), @r#"["a", "b", "c"]"#);
+ }
+
+ // Send fallback (e.g. method_missing) calls into the interpreter, which
+ // reads cfp->iseq via GET_ISEQ(). gen_prepare_non_leaf_call writes the
+ // iseq to JITFrame, but GET_ISEQ reads cfp->iseq directly. This test
+ // ensures the interpreter can resolve the caller iseq for backtraces.
+ #[test]
+ fn test_send_fallback_caller_location() {
+ assert_snapshot!(inspect(r#"
+ def callee = caller_locations(1, 1)[0].label
+ def test = callee
+ test
+ test
+ "#), @r#""Object#test""#);
+ }
+
+ // A send fallback may throw (e.g. via method_missing raising). The
+ // interpreter must be able to find the correct rescue handler in the
+ // caller's ISEQ catch table. This exercises throw through send fallback.
+ #[test]
+ fn test_send_fallback_throw() {
+ assert_snapshot!(inspect(r#"
+ class Foo
+ def method_missing(name, *) = raise("no #{name}")
+ end
+ def test
+ Foo.new.bar
+ rescue RuntimeError => e
+ e.message
+ end
+ test
+ test
+ "#), @r#""no bar""#);
+ }
+
+ // Proc.new inside a block passed via invokeblock captures the caller's
+ // block_code. When the JIT compiles the caller, block_code must be
+ // correctly available for the proc to work.
+ #[test]
+ fn test_proc_from_invokeblock() {
+ assert_snapshot!(inspect("
+ def capture_block(&blk) = blk
+ def test = capture_block { 42 }
+ test
+ test.call
+ "), @"42");
+ }
+
+ // binding() called from a JIT-compiled callee must see the correct
+ // source location (iseq + pc) of the caller frame.
+ #[test]
+ fn test_binding_source_location() {
+ assert_snapshot!(inspect(r#"
+ def callee = binding
+ def test = callee
+ test
+ b = test
+ b.source_location[1] > 0
+ "#), @"true");
+ }
+
+ // $~ (Regexp special variable) is stored via svar which walks the EP
+ // chain to find the LEP. rb_vm_svar_lep uses rb_zjit_cfp_has_iseq to
+ // skip C frames, so it must work correctly with JITFrame.
+ #[test]
+ fn test_svar_regexp_match() {
+ assert_snapshot!(inspect(r#"
+ def test(s)
+ s =~ /hello/
+ $~
+ end
+ test("hello world")
+ test("hello world").to_s
+ "#), @r#""hello""#);
+ }
+
+ // C function calls with rb_block_call (like Array#each, Enumerable#map)
+ // write an ifunc to cfp->block_code after the JIT pushes the C frame.
+ // GC must mark and relocate this ifunc. This test exercises the code
+ // path fixed by "Fix ZJIT segfault: write block_code for C frames and
+ // fix GC marking".
+ #[test]
+ fn test_cfunc_block_code_gc() {
+ assert_snapshot!(inspect("
+ def test
+ # Use a cfunc that calls back into Ruby with a block (rb_block_call)
+ [1, 2, 3].map { |x| x.to_s }
+ end
+ test
+ test
+ "), @r#"["1", "2", "3"]"#);
+ }
+
+ // Multiple levels of cfunc-with-block: a JIT-compiled method calls a
+ // cfunc that yields, and the block itself calls another cfunc that
+ // yields. Each C frame's block_code must be properly initialized.
+ #[test]
+ fn test_nested_cfunc_with_block() {
+ assert_snapshot!(inspect("
+ def test
+ [1, 2].flat_map { |x| [x, x + 10].map { |y| y * 2 } }
+ end
+ test
+ test
+ "), @"[2, 22, 4, 24]");
+ }
+}