diff options
-rw-r--r-- | .document | 1 | ||||
-rw-r--r-- | .github/workflows/zjit-macos.yml | 13 | ||||
-rw-r--r-- | .github/workflows/zjit-ubuntu.yml | 13 | ||||
-rw-r--r-- | bootstraptest/test_zjit.rb | 33 | ||||
-rw-r--r-- | common.mk | 3 | ||||
-rw-r--r-- | inits.c | 1 | ||||
-rw-r--r-- | test/lib/jit_support.rb | 6 | ||||
-rw-r--r-- | test/ruby/test_zjit.rb | 117 | ||||
-rw-r--r-- | zjit.c | 6 | ||||
-rw-r--r-- | zjit.rb | 6 | ||||
-rw-r--r-- | zjit/src/lib.rs | 16 | ||||
-rw-r--r-- | zjit/src/state.rs | 15 |
12 files changed, 177 insertions, 53 deletions
@@ -30,6 +30,7 @@ thread_sync.rb trace_point.rb warning.rb yjit.rb +zjit.rb # Errno::* known_errors.inc diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml index 6829bbe113..c84fd5a6ac 100644 --- a/.github/workflows/zjit-macos.yml +++ b/.github/workflows/zjit-macos.yml @@ -31,15 +31,9 @@ jobs: - test_task: 'zjit-test' configure: '--enable-zjit=dev' - - test_task: 'btest' - zjit_opts: '--zjit-call-threshold=1' + - test_task: 'test-all' configure: '--enable-zjit=dev' - btests: '../src/bootstraptest/test_zjit.rb' - - - test_task: 'btest' - zjit_opts: '--zjit-call-threshold=2' - configure: '--enable-zjit=dev' - btests: '../src/bootstraptest/test_zjit.rb' + tests: '../src/test/ruby/test_zjit.rb' # Test without ZJIT for now - test_task: 'check' @@ -101,14 +95,13 @@ jobs: make -s ${{ matrix.test_task }} ${TESTS:+TESTS="$TESTS"} RUN_OPTS="$RUN_OPTS" SPECOPTS="$SPECOPTS" - BTESTS="$BTESTS" timeout-minutes: 60 env: RUBY_TESTOPTS: '-q --tty=no' TEST_BUNDLED_GEMS_ALLOW_FAILURES: 'typeprof' SYNTAX_SUGGEST_TIMEOUT: '5' PRECHECK_BUNDLED_GEMS: 'no' - BTESTS: ${{ matrix.btests }} + TESTS: ${{ matrix.tests }} continue-on-error: ${{ matrix.continue-on-test_task || false }} result: diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index 2c72f2edc4..8c8664f846 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -36,15 +36,9 @@ jobs: - test_task: 'zjit-test' configure: '--enable-zjit=dev' - - test_task: 'btest' - zjit_opts: '--zjit-call-threshold=1' + - test_task: 'test-all' configure: '--enable-zjit=dev' - btests: '../src/bootstraptest/test_zjit.rb' - - - test_task: 'btest' - zjit_opts: '--zjit-call-threshold=2' - configure: '--enable-zjit=dev' - btests: '../src/bootstraptest/test_zjit.rb' + tests: '../src/test/ruby/test_zjit.rb' # Test without ZJIT for now - test_task: 'check' @@ -125,7 +119,6 @@ jobs: make -s ${{ matrix.test_task }} ${TESTS:+TESTS="$TESTS"} RUN_OPTS="$RUN_OPTS" MSPECOPT=--debug SPECOPTS="$SPECOPTS" YJIT_BENCH_OPTS="$YJIT_BENCH_OPTS" YJIT_BINDGEN_DIFF_OPTS="$YJIT_BINDGEN_DIFF_OPTS" - BTESTS="$BTESTS" timeout-minutes: 90 env: RUBY_TESTOPTS: '-q --tty=no' @@ -134,7 +127,7 @@ jobs: SYNTAX_SUGGEST_TIMEOUT: '5' YJIT_BINDGEN_DIFF_OPTS: '--exit-code' LIBCLANG_PATH: ${{ matrix.libclang_path }} - BTESTS: ${{ matrix.btests }} + TESTS: ${{ matrix.tests }} continue-on-error: ${{ matrix.continue-on-test_task || false }} result: diff --git a/bootstraptest/test_zjit.rb b/bootstraptest/test_zjit.rb deleted file mode 100644 index 45f4de8f44..0000000000 --- a/bootstraptest/test_zjit.rb +++ /dev/null @@ -1,33 +0,0 @@ -# Tests of Ruby methods that ZJIT can currently compile. -# make btest BTESTS=bootstraptest/test_zjit.rb RUN_OPTS="--zjit" - -assert_equal 'nil', %q{ - def test = nil - test; test.inspect -} - -assert_equal '1', %q{ - def test = 1 - test; test -} - -assert_equal '3', %q{ - def test = 1 + 2 - test; test -} - -assert_equal '[6, 3]', %q{ - def test(a, b) = a + b - [test(2, 4), test(1, 2)] -} - -# Test argument ordering -assert_equal '2', %q{ - def test(a, b) = a - b - test(6, 4) -} - -assert_equal '6', %q{ - def test(a, b, c) = a + b + c - test(1, 2, 3) -} @@ -1223,6 +1223,7 @@ BUILTIN_RB_SRCS = \ $(srcdir)/gem_prelude.rb \ $(srcdir)/yjit.rb \ $(srcdir)/yjit_hook.rb \ + $(srcdir)/zjit.rb \ $(empty) BUILTIN_RB_INCS = $(BUILTIN_RB_SRCS:.rb=.rbinc) @@ -10717,6 +10718,7 @@ miniinit.$(OBJEXT): {$(VPATH)}vm_opts.h miniinit.$(OBJEXT): {$(VPATH)}warning.rb miniinit.$(OBJEXT): {$(VPATH)}yjit.rb miniinit.$(OBJEXT): {$(VPATH)}yjit_hook.rb +miniinit.$(OBJEXT): {$(VPATH)}zjit.rb node.$(OBJEXT): $(CCAN_DIR)/check_type/check_type.h node.$(OBJEXT): $(CCAN_DIR)/container_of/container_of.h node.$(OBJEXT): $(CCAN_DIR)/list/list.h @@ -21179,4 +21181,5 @@ zjit.$(OBJEXT): {$(VPATH)}vm_opts.h zjit.$(OBJEXT): {$(VPATH)}vm_sync.h zjit.$(OBJEXT): {$(VPATH)}zjit.c zjit.$(OBJEXT): {$(VPATH)}zjit.h +zjit.$(OBJEXT): {$(VPATH)}zjit.rbinc # AUTOGENERATED DEPENDENCIES END @@ -103,6 +103,7 @@ rb_call_builtin_inits(void) BUILTIN(thread_sync); BUILTIN(nilclass); BUILTIN(marshal); + BUILTIN(zjit); Init_builtin_prelude(); } #undef CALL diff --git a/test/lib/jit_support.rb b/test/lib/jit_support.rb index 1b15f685a0..79fdbcce48 100644 --- a/test/lib/jit_support.rb +++ b/test/lib/jit_support.rb @@ -16,4 +16,10 @@ module JITSupport def yjit_force_enabled? "#{RbConfig::CONFIG['CFLAGS']} #{RbConfig::CONFIG['CPPFLAGS']}".match?(/(\A|\s)-D ?YJIT_FORCE_ENABLE\b/) end + + def zjit_supported? + return @zjit_supported if defined?(@zjit_supported) + # nil in mswin + @zjit_supported = ![nil, 'no'].include?(RbConfig::CONFIG['ZJIT_SUPPORT']) + end end diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb new file mode 100644 index 0000000000..7365aa9e57 --- /dev/null +++ b/test/ruby/test_zjit.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true +# +# This set of tests can be run with: +# make test-all TESTS=test/ruby/test_zjit.rb + +require 'test/unit' +require 'envutil' +require_relative '../lib/jit_support' +return unless JITSupport.zjit_supported? + +class TestZJIT < Test::Unit::TestCase + def test_nil + assert_compiles nil, %q{ + def test = nil + test + } + end + + def test_putobject + assert_compiles 1, %q{ + def test = 1 + test + } + end + + def test_opt_plus_const + assert_compiles 3, %q{ + def test = 1 + 2 + test # profile opt_plus + test + }, call_threshold: 2 + end + + def test_opt_plus_fixnum + assert_compiles 3, %q{ + def test(a, b) = a + b + test(0, 1) # profile opt_plus + test(1, 2) + }, call_threshold: 2 + end + + def test_opt_plus_chain + assert_compiles 6, %q{ + def test(a, b, c) = a + b + c + test(0, 1, 2) # profile opt_plus + test(1, 2, 3) + }, call_threshold: 2 + end + + # Test argument ordering + def test_opt_minus + omit 'FixnumSub is not implemented yet' + assert_compiles 2, %q{ + def test(a, b) = a - b + test(2, 1) # profile opt_minus + test(6, 4) + }, call_threshold: 2 + end + + private + + # Assert that every method call in `test_script` can be compiled by ZJIT + # at a given call_threshold + def assert_compiles(expected, test_script, call_threshold: 1) + pipe_fd = 3 + + script = <<~RUBY + _test_proc = -> { + RubyVM::ZJIT.assert_compiles + #{test_script} + } + result = _test_proc.call + IO.open(#{pipe_fd}).write(Marshal.dump(result)) + RUBY + + status, out, err, pipe_out = eval_with_jit(script, call_threshold:, pipe_fd:) + + message = "exited with status #{status.to_i}" + message << "\nstdout:\n```\n#{out}```\n" unless out.empty? + message << "\nstderr:\n```\n#{err}```\n" unless err.empty? + assert status.success?, message + + actual = Marshal.load(pipe_out) + assert_equal expected, actual + end + + # Run a Ruby process with ZJIT options and a pipe for writing test results + def eval_with_jit(script, call_threshold: 1, timeout: 1000, pipe_fd:) + args = [ + "--disable-gems", + "--zjit-call-threshold=#{call_threshold}", + ] + args << "-e" << script_shell_encode(script) + pipe_r, pipe_w = IO.pipe + # Separate thread so we don't deadlock when + # the child ruby blocks writing the output to pipe_fd + pipe_out = nil + pipe_reader = Thread.new do + pipe_out = pipe_r.read + pipe_r.close + end + out, err, status = EnvUtil.invoke_ruby(args, '', true, true, rubybin: RbConfig.ruby, timeout: timeout, ios: { pipe_fd => pipe_w }) + pipe_w.close + pipe_reader.join(timeout) + [status, out, err, pipe_out] + ensure + pipe_reader&.kill + pipe_reader&.join(timeout) + pipe_r&.close + pipe_w&.close + end + + def script_shell_encode(s) + # We can't pass utf-8-encoded characters directly in a shell arg. But we can use Ruby \u constants. + s.chars.map { |c| c.ascii_only? ? c : "\\u%x" % c.codepoints[0] }.join + end +end @@ -677,3 +677,9 @@ rb_iseq_set_zjit_payload(const rb_iseq_t *iseq, void *payload) RUBY_ASSERT_ALWAYS(NULL == iseq->body->zjit_payload); iseq->body->zjit_payload = payload; } + +// Primitives used by zjit.rb +VALUE rb_zjit_assert_compiles(rb_execution_context_t *ec, VALUE self); + +// Preprocessed zjit.rb generated during build +#include "zjit.rbinc" diff --git a/zjit.rb b/zjit.rb new file mode 100644 index 0000000000..fd58c1c94a --- /dev/null +++ b/zjit.rb @@ -0,0 +1,6 @@ +module RubyVM::ZJIT + # Assert that any future ZJIT compilation will return a function pointer + def self.assert_compiles + Primitive.rb_zjit_assert_compiles + end +end diff --git a/zjit/src/lib.rs b/zjit/src/lib.rs index 9e93f0f2be..e68ac93fa5 100644 --- a/zjit/src/lib.rs +++ b/zjit/src/lib.rs @@ -89,6 +89,15 @@ fn rb_bug_panic_hook() { /// Generate JIT code for a given ISEQ, which takes EC and CFP as its arguments. #[unsafe(no_mangle)] pub extern "C" fn rb_zjit_iseq_gen_entry_point(iseq: IseqPtr, _ec: EcPtr) -> *const u8 { + let code_ptr = iseq_gen_entry_point(iseq); + if ZJITState::assert_compiles_enabled() && code_ptr == std::ptr::null() { + let iseq_location = iseq_get_location(iseq, 0); + panic!("Failed to compile: {iseq_location}"); + } + code_ptr +} + +fn iseq_gen_entry_point(iseq: IseqPtr) -> *const u8 { // Do not test the JIT code in HIR tests if cfg!(test) { return std::ptr::null(); @@ -116,3 +125,10 @@ pub extern "C" fn rb_zjit_iseq_gen_entry_point(iseq: IseqPtr, _ec: EcPtr) -> *co } }) } + +/// Assert that any future ZJIT compilation will return a function pointer (not fail to compile) +#[unsafe(no_mangle)] +pub extern "C" fn rb_zjit_assert_compiles(_ec: EcPtr, _self: VALUE) -> VALUE { + ZJITState::enable_assert_compiles(); + Qnil +} diff --git a/zjit/src/state.rs b/zjit/src/state.rs index 92efc3a48e..c176d58b20 100644 --- a/zjit/src/state.rs +++ b/zjit/src/state.rs @@ -12,6 +12,9 @@ pub struct ZJITState { /// Assumptions that require invalidation invariants: Invariants, + + /// Assert successful compilation if set to true + assert_compiles: bool, } /// Private singleton instance of the codegen globals @@ -64,6 +67,7 @@ impl ZJITState { code_block: cb, options, invariants: Invariants::default(), + assert_compiles: false, }; unsafe { ZJIT_STATE = Some(zjit_state); } } @@ -92,4 +96,15 @@ impl ZJITState { pub fn get_invariants() -> &'static mut Invariants { &mut ZJITState::get_instance().invariants } + + /// Return true if successful compilation should be asserted + pub fn assert_compiles_enabled() -> bool { + ZJITState::get_instance().assert_compiles + } + + /// Start asserting successful compilation + pub fn enable_assert_compiles() { + let instance = ZJITState::get_instance(); + instance.assert_compiles = true; + } } |