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
2 changes: 1 addition & 1 deletion gems/bundled_gems
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ matrix 0.4.3 https://github.com/ruby/matrix
prime 0.1.4 https://github.com/ruby/prime
rbs 4.0.2 https://github.com/ruby/rbs
typeprof 0.31.1 https://github.com/ruby/typeprof
debug 1.11.1 https://github.com/ruby/debug
debug 1.11.1 https://github.com/ruby/debug 2897edad6d2c2eeb49ffe915192c54572dbe6c82
racc 1.8.1 https://github.com/ruby/racc
mutex_m 0.3.0 https://github.com/ruby/mutex_m
getoptlong 0.2.1 https://github.com/ruby/getoptlong
Expand Down
2 changes: 1 addition & 1 deletion io.c
Original file line number Diff line number Diff line change
Expand Up @@ -15113,7 +15113,7 @@ set_LAST_READ_LINE(VALUE val, ID _x, VALUE *_y)
* - +:binmode+: If a truthy value, specifies the mode as binary, text-only otherwise.
* - +:autoclose+: If a truthy value, specifies that the +fd+ will close
* when the stream closes; otherwise it remains open.
* - +:path:+ If a string value is provided, it is used in #inspect and is available as
* - +:path+: If a string value is provided, it is used in #inspect and is available as
* #path method.
*
* Also available are the options offered in String#encode,
Expand Down
1 change: 0 additions & 1 deletion lib/pathname.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
#
# For documentation, see class Pathname.
#

class Pathname # * Find *
#
# Iterates over the directory tree in a depth first manner, yielding a
Expand Down
128 changes: 74 additions & 54 deletions pathname_builtin.rb
Original file line number Diff line number Diff line change
@@ -1,76 +1,96 @@
# frozen_string_literal: true
#
# = pathname.rb
# A \Pathname object contains a string directory path or filepath;
# it does not represent a corresponding actual file or directory
# -- which in fact may or may not exist.
#
# Object-Oriented Pathname Class
# A \Pathname object is immutable (except for method #freeze).
#
# Author:: Tanaka Akira <akr@m17n.org>
# Documentation:: Author and Gavin Sinclair
# A pathname may be relative or absolute:
#
# For documentation, see class Pathname.
# Pathname.new('lib') # => #<Pathname:lib>
# Pathname.new('/usr/local/bin') # => #<Pathname:/usr/local/bin>
#

# == Convenience Methods
#
# The class provides *all* functionality from class File and module FileTest,
# along with some functionality from class Dir and module FileUtils.
#
# Here's an example string path and corresponding \Pathname object:
#
# path = 'lib/fileutils.rb'
# pn = Pathname.new(path) # => #<Pathname:lib/fileutils.rb>
#
# Each of these method pairs (\Pathname vs. \File) gives exactly the same result:
#
# pn.size # => 83777
# File.size(path) # => 83777
#
# pn.directory? # => false
# File.directory?(path) # => false
#
# pn.read.size # => 81074
# File.read(path).size# # => 81074
#
# Each of these method pairs gives similar results,
# but each \Pathname method returns a more versatile \Pathname object,
# instead of a string:
#
# pn.dirname # => #<Pathname:lib>
# File.dirname(path) # => "lib"
#
# Pathname represents the name of a file or directory on the filesystem,
# but not the file itself.
# pn.basename # => #<Pathname:fileutils.rb>
# File.basename(path) # => "fileutils.rb"
#
# The pathname depends on the Operating System: Unix, Windows, etc.
# This library works with pathnames of local OS, however non-Unix pathnames
# are supported experimentally.
# pn.split # => [#<Pathname:lib>, #<Pathname:fileutils.rb>]
# File.split(path) # => ["lib", "fileutils.rb"]
#
# A Pathname can be relative or absolute. It's not until you try to
# reference the file that it even matters whether the file exists or not.
# Each of these methods takes a block:
#
# Pathname is immutable. It has no method for destructive update.
# pn.open do |file|
# p file
# end
# File.open(path) do |file|
# p file
# end
#
# The goal of this class is to manipulate file path information in a neater
# way than standard Ruby provides. The examples below demonstrate the
# difference.
# The outputs for each:
#
# *All* functionality from File, FileTest, and some from Dir and FileUtils is
# included, in an unsurprising way. It is essentially a facade for all of
# these, and more.
# #<File:lib/fileutils.rb (closed)>
# #<File:lib/fileutils.rb (closed)>
#
# == Examples
# Each of these methods takes a block:
#
# === Example 1: Using Pathname
# pn.each_line do |line|
# p line
# break
# end
# File.foreach(path) do |line|
# p line
# break
# end
#
# require 'pathname'
# pn = Pathname.new("/usr/bin/ruby")
# size = pn.size # 27662
# isdir = pn.directory? # false
# dir = pn.dirname # Pathname:/usr/bin
# base = pn.basename # Pathname:ruby
# dir, base = pn.split # [Pathname:/usr/bin, Pathname:ruby]
# data = pn.read
# pn.open { |f| _ }
# pn.each_line { |line| _ }
# The outputs for each:
#
# === Example 2: Using standard Ruby
# "# frozen_string_literal: true\n"
# "# frozen_string_literal: true\n"
#
# pn = "/usr/bin/ruby"
# size = File.size(pn) # 27662
# isdir = File.directory?(pn) # false
# dir = File.dirname(pn) # "/usr/bin"
# base = File.basename(pn) # "ruby"
# dir, base = File.split(pn) # ["/usr/bin", "ruby"]
# data = File.read(pn)
# File.open(pn) { |f| _ }
# File.foreach(pn) { |line| _ }
# == More Methods
#
# === Example 3: Special features
# Here is a sampling of other available methods:
#
# p1 = Pathname.new("/usr/lib") # Pathname:/usr/lib
# p2 = p1 + "ruby/1.8" # Pathname:/usr/lib/ruby/1.8
# p3 = p1.parent # Pathname:/usr
# p4 = p2.relative_path_from(p3) # Pathname:lib/ruby/1.8
# pwd = Pathname.pwd # Pathname:/home/gavin
# pwd.absolute? # true
# p5 = Pathname.new "." # Pathname:.
# p5 = p5 + "music/../articles" # Pathname:music/../articles
# p5.cleanpath # Pathname:articles
# p5.realpath # Pathname:/home/gavin/articles
# p5.children # [Pathname:/home/gavin/articles/linux, ...]
# p1 = Pathname.new('/usr/lib') # => #<Pathname:/usr/lib>
# p1.absolute? # => true
# p2 = p1 + 'ruby/4.0' # => #<Pathname:/usr/lib/ruby/4.0>
# p3 = p1.parent # => #<Pathname:/usr>
# p4 = p2.relative_path_from(p3) # => #<Pathname:lib/ruby/4.0>
# p4.absolute? # => false
# p5 = Pathname.new('.') # => #<Pathname:.>
# p6 = p5 + 'usr/../var' # => #<Pathname:usr/../var>
# p6.cleanpath # => #<Pathname:var>
# p6.realpath # => #<Pathname:/var>
# p6.children.take(2)
# # => [#<Pathname:usr/../var/local>, #<Pathname:usr/../var/spool>]
#
# == Breakdown of functionality
#
Expand Down
3 changes: 3 additions & 0 deletions test/.excludes-zjit/TestOpenURIProxy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
if RUBY_PLATFORM =~ /linux/
exclude(/test_/, 'randomly fails with SystemStackError (Shopify/ruby#964)')
end
29 changes: 20 additions & 9 deletions zjit/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,15 @@ use crate::hir_type::{types, Type};
use crate::options::{get_option, PerfMap};
use crate::cast::IntoUsize;

/// At the moment, we support recompiling each ISEQ only once.
pub const MAX_ISEQ_VERSIONS: usize = 2;
/// Default maximum number of compiled versions per ISEQ.
const DEFAULT_MAX_VERSIONS: usize = 2;

/// Maximum number of compiled versions per ISEQ.
/// Configurable via --zjit-max-versions (default: 2).
pub fn max_iseq_versions() -> usize {
unsafe { crate::options::OPTIONS.as_ref() }
.map_or(DEFAULT_MAX_VERSIONS, |opts| opts.max_versions)
}

/// Sentinel program counter stored in C frames when runtime checks are enabled.
const PC_POISON: Option<*const VALUE> = if cfg!(feature = "runtime_checks") {
Expand Down Expand Up @@ -216,7 +223,7 @@ fn gen_iseq_entry_point(cb: &mut CodeBlock, iseq: IseqPtr, jit_exception: bool)
pub fn invalidate_iseq_version(cb: &mut CodeBlock, iseq: IseqPtr, version: &mut IseqVersionRef) {
let payload = get_or_create_iseq_payload(iseq);
if unsafe { version.as_ref() }.status != IseqStatus::Invalidated
&& payload.versions.len() < MAX_ISEQ_VERSIONS
&& payload.versions.len() < max_iseq_versions()
{
unsafe { version.as_mut() }.status = IseqStatus::Invalidated;
unsafe { rb_iseq_reset_jit_func(iseq) };
Expand Down Expand Up @@ -300,8 +307,8 @@ fn gen_iseq(cb: &mut CodeBlock, iseq: IseqPtr, function: Option<&Function>) -> R
Some(IseqStatus::CantCompile(err)) => return Err(err.clone()),
_ => {},
}
// If the ISEQ already hax MAX_ISEQ_VERSIONS, do not compile a new version.
if payload.versions.len() == MAX_ISEQ_VERSIONS {
// If the ISEQ already has max versions, do not compile a new version.
if payload.versions.len() >= max_iseq_versions() {
return Err(CompileError::IseqVersionLimitReached);
}

Expand Down Expand Up @@ -3069,11 +3076,15 @@ c_callable! {
unsafe { Rc::increment_strong_count(iseq_call_ptr as *const IseqCall); }
let iseq_call = unsafe { Rc::from_raw(iseq_call_ptr as *const IseqCall) };
let iseq = iseq_call.iseq.get();
let entry_insn_idxs = crate::hir::jit_entry_insns(iseq);
let params = unsafe { iseq.params() };
let entry_idx = iseq_call.jit_entry_idx.to_usize();
let entry_insn_idx = params.opt_table_slice().get(entry_idx)
.unwrap_or_else(|| panic!("function_stub: opt_table out of bounds. {params:#?}, entry_idx={entry_idx}"))
.as_u32();
// gen_push_frame() doesn't set PC or ISEQ, so we need to set them before exit.
// function_stub_hit_body() may allocate and call gc_validate_pc(), so we always set PC and ISEQ.
// Clear jit_return so the interpreter reads cfp->pc and cfp->iseq directly.
let pc = unsafe { rb_iseq_pc_at_idx(iseq, entry_insn_idxs[iseq_call.jit_entry_idx.to_usize()]) };
let pc = unsafe { rb_iseq_pc_at_idx(iseq, entry_insn_idx) };
unsafe { rb_set_cfp_pc(cfp, pc) };
unsafe { (*cfp)._iseq = iseq };
unsafe { (*cfp).jit_return = std::ptr::null_mut() };
Expand Down Expand Up @@ -3494,7 +3505,7 @@ impl Assembler {

/// Store info about a JIT entry point
pub struct JITEntry {
/// Index that corresponds to [crate::hir::jit_entry_insns]
/// Index that corresponds to an entry in [crate::cruby::IseqParameters::opt_table_slice]
jit_entry_idx: usize,
/// Position where the entry point starts
start_addr: Cell<Option<CodePtr>>,
Expand All @@ -3517,7 +3528,7 @@ pub struct IseqCall {
/// Callee ISEQ that start_addr jumps to
pub iseq: Cell<IseqPtr>,

/// Index that corresponds to [crate::hir::jit_entry_insns]
/// Index that corresponds to an entry in [crate::cruby::IseqParameters::opt_table_slice]
jit_entry_idx: u16,

/// Argument count passing to the HIR function
Expand Down
54 changes: 33 additions & 21 deletions zjit/src/codegen_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use super::{gen_insn, JITState};
use crate::asm::CodeBlock;
use crate::backend::lir::Assembler;
use crate::codegen::MAX_ISEQ_VERSIONS;
use crate::codegen::max_iseq_versions;
use crate::cruby::*;
use crate::hir::{Insn, iseq_to_hir};
use crate::options::{rb_zjit_prepare_options, set_call_threshold};
Expand Down Expand Up @@ -420,6 +420,33 @@ fn test_getblockparamproxy() {
assert_snapshot!(assert_compiles("test { 1 }"), @"1");
}

#[test]
fn test_getblockparamproxy_modified() {
eval("
def test(&block)
b = block
0.then(&block)
end
test { 1 }
");
assert_contains_opcode("test", YARVINSN_getblockparamproxy);
assert_snapshot!(inspect("test { 1 }"), @"1");
}

#[test]
fn test_getblockparamproxy_modified_nested_block() {
eval("
def test(&block)
proc do
b = block
0.then(&block)
end
end
test { 1 }.call
");
assert_snapshot!(inspect("test { 1 }.call"), @"1");
}

#[test]
fn test_getblockparam() {
eval("
Expand Down Expand Up @@ -462,9 +489,7 @@ fn test_setblockparam_nested_block() {
}

#[test]
fn test_setblockparam_side_exit() {
// This pattern side exits because `block.call` goes through
// getblockparamproxy's modified-block-parameter case.
fn test_getblockparamproxy_after_setblockparam() {
eval("
def test(&block)
block = proc { 3 }
Expand All @@ -473,21 +498,7 @@ fn test_setblockparam_side_exit() {
test { 1 }
");
assert_contains_opcode("test", YARVINSN_setblockparam);
assert_snapshot!(inspect("test { 1 }"), @"3");
}

#[test]
fn test_getblockparam_proxy_side_exit_restores_block_local() {
eval("
def test(&block)
b = block
raise \"test\" unless block
b ? 2 : 3
end
test {}
");
assert_contains_opcode("test", YARVINSN_getblockparam);
assert_snapshot!(assert_compiles("test {}"), @"2");
assert_snapshot!(assert_compiles("test { 1 }"), @"3");
}

#[test]
Expand Down Expand Up @@ -4926,13 +4937,14 @@ fn test_invokesuper_with_local_written_by_blockiseq() {

#[test]
fn test_max_iseq_versions() {
let max_versions = max_iseq_versions();
eval(&format!("
TEST = -1
def test = TEST

# compile and invalidate MAX+1 times
i = 0
while i < {MAX_ISEQ_VERSIONS} + 1
while i < {max_versions} + 1
test; test # compile a version

Object.send(:remove_const, :TEST)
Expand All @@ -4945,7 +4957,7 @@ fn test_max_iseq_versions() {
// It should not exceed MAX_ISEQ_VERSIONS
let iseq = get_method_iseq("self", "test");
let payload = get_or_create_iseq_payload(iseq);
assert_eq!(payload.versions.len(), MAX_ISEQ_VERSIONS);
assert_eq!(payload.versions.len(), max_iseq_versions());

// The last call should not discard the JIT code
assert!(matches!(unsafe { payload.versions.last().unwrap().as_ref() }.status, IseqStatus::Compiled(_)));
Expand Down
18 changes: 18 additions & 0 deletions zjit/src/cruby.rs
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,24 @@ impl IseqAccess for IseqPtr {
}
}

impl IseqParameters {
/// The `opt_table` is a mapping where `opt_table[number_of_optional_parameters_filled]`
/// gives the YARV entry point of ISeq as an index of the iseq_encoded array.
/// This method gives over the table that additionally works when `opt_num==0`,
/// when the table is stored as `NULL` and implicit.
/// The table stores the indexes as raw VALUE integers; they are not tagged as fixnum.
pub fn opt_table_slice(&self) -> &[VALUE] {
let opt_num: usize = self.opt_num.try_into().expect("ISeq opt_num should always >=0");
if opt_num > 0 {
// The table has size=opt_num+1 because opt_table[opt_num] is valid (all optionals filled)
unsafe { std::slice::from_raw_parts(self.opt_table, opt_num + 1) }
} else {
// The ISeq entry point is index 0 when there are no optional parameters
&[VALUE(0)]
}
}
}

impl From<IseqPtr> for VALUE {
/// For `.into()` convenience
fn from(iseq: IseqPtr) -> Self {
Expand Down
Loading