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
189 changes: 134 additions & 55 deletions prism_compile.c

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions prism_compile.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ typedef struct pm_scope_node {
* the instructions pertaining to BEGIN{} nodes.
*/
struct iseq_link_anchor *pre_execution_anchor;

/**
* Cached line hint for line offset list lookups. Since the compiler walks
* the AST roughly in source order, consecutive lookups tend to be for
* nearby byte offsets. This avoids repeated binary searches.
*/
size_t last_line;
} pm_scope_node_t;

void pm_scope_node_init(const pm_node_t *node, pm_scope_node_t *scope, pm_scope_node_t *previous);
Expand Down
1 change: 1 addition & 0 deletions zjit.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include "vm_insnhelper.h"
#include "probes.h"
#include "probes_helper.h"
#include "constant.h"
#include "iseq.h"
#include "ruby/debug.h"
#include "internal/cont.h"
Expand Down
14 changes: 14 additions & 0 deletions zjit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@ def trace_exit_locations_enabled?
Primitive.rb_zjit_trace_exit_locations_enabled_p
end

# A directive for the compiler to fail to compile the call to this method.
# To show this to ZJIT, say `::RubyVM::ZJIT.induce_compile_failure!` verbatim.
# Other forms are too dynamic to detect during compilation.
#
# Actually running this method does nothing, whether ZJIT sees the call or not.
def induce_compile_failure! = nil

# A directive for the compiler to exit out of compiled code at the call site of this method.
# To show this to ZJIT, say `::RubyVM::ZJIT.induce_side_exit!` verbatim.
# Other forms are too dynamic to detect during compilation.
#
# Actually running this method does nothing, whether ZJIT sees the call or not.
def induce_side_exit! = nil

# If --zjit-trace-exits is enabled parse the hashes from
# Primitive.rb_zjit_get_exit_locations into a format readable
# by Stackprof. This will allow us to find the exact location of a
Expand Down
2 changes: 2 additions & 0 deletions zjit/bindgen/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ fn main() {
.allowlist_function("rb_gc_location")
.allowlist_function("rb_gc_writebarrier")
.allowlist_function("rb_gc_writebarrier_remember")
.allowlist_function("rb_gc_register_mark_object")
.allowlist_function("rb_zjit_writebarrier_check_immediate")

// VALUE variables for Ruby class objects
Expand Down Expand Up @@ -432,6 +433,7 @@ fn main() {
.allowlist_function("rb_vm_base_ptr")
.allowlist_function("rb_ec_stack_check")
.allowlist_function("rb_vm_top_self")
.allowlist_function("rb_const_lookup")

// We define these manually, don't import them
.blocklist_type("VALUE")
Expand Down
6 changes: 5 additions & 1 deletion zjit/src/cruby.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1544,14 +1544,18 @@ pub(crate) mod ids {
name: RUBY_FL_FREEZE
name: RUBY_ELTS_SHARED
name: VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM
name: RubyVM
name: ZJIT
name: induce_side_exit_bang content: b"induce_side_exit!"
name: induce_compile_failure_bang content: b"induce_compile_failure!"
}

/// Get an CRuby `ID` to an interned string, e.g. a particular method name.
macro_rules! ID {
($id_name:ident) => {{
let id = $crate::cruby::ids::$id_name.load(std::sync::atomic::Ordering::Relaxed);
debug_assert_ne!(0, id, "ids module should be initialized");
ID(id)
$crate::cruby::ID(id)
}}
}
pub(crate) use ID;
Expand Down
16 changes: 16 additions & 0 deletions zjit/src/cruby_bindings.inc.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

65 changes: 61 additions & 4 deletions zjit/src/hir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
#![allow(clippy::match_like_matches_macro)]
use crate::{
backend::lir::C_ARG_OPNDS,
cast::IntoUsize, codegen::local_idx_to_ep_offset, cruby::*, invariants::{self, has_singleton_class_of}, payload::{get_or_create_iseq_payload, IseqPayload}, options::{debug, get_option, DumpHIR}, state::ZJITState, json::Json
cast::IntoUsize, codegen::local_idx_to_ep_offset, cruby::*, invariants::{self, has_singleton_class_of}, payload::{get_or_create_iseq_payload, IseqPayload}, options::{debug, get_option, DumpHIR}, state::ZJITState, json::Json,
state,
};
use std::{
cell::RefCell, collections::{BTreeSet, HashMap, HashSet, VecDeque}, ffi::{c_void, c_uint, c_int, CStr}, fmt::Display, mem::{align_of, size_of}, ptr, slice::Iter
cell::RefCell, collections::{BTreeSet, HashMap, HashSet, VecDeque}, ffi::{c_void, c_uint, c_int, CStr}, fmt::Display, mem::{align_of, size_of}, ptr, slice::Iter,
sync::atomic::Ordering,
};
use crate::hir_type::{Type, types};
use crate::hir_effect::{Effect, abstract_heaps, effects};
Expand Down Expand Up @@ -529,6 +531,7 @@ pub enum SideExitReason {
SplatKwNotNilOrHash,
SplatKwPolymorphic,
SplatKwNotProfiled,
DirectiveInduced,
}

#[derive(Debug, Clone, Copy)]
Expand Down Expand Up @@ -6590,6 +6593,7 @@ pub enum ParseError {
MalformedIseq(u32), // insn_idx into iseq_encoded
Validation(ValidationError),
NotAllowed,
DirectiveInduced,
}

/// Return the number of locals in the current ISEQ (includes parameters)
Expand Down Expand Up @@ -7120,7 +7124,32 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
}
YARVINSN_opt_getconstant_path => {
let ic = get_arg(pc, 0).as_ptr();
state.stack_push(fun.push_insn(block, Insn::GetConstantPath { ic, state: exit_id }));
let get_const_path = fun.push_insn(block, Insn::GetConstantPath { ic, state: exit_id });
state.stack_push(get_const_path);

// Check for `::RubyVM::ZJIT` for directives
unsafe {
let mut current_segment = (*ic).segments;
let mut segments = [ID(0); 4 /* expected segment length */];
for segment in segments.iter_mut() {
*segment = current_segment.read();
if *segment == ID(0) {
break;
}
current_segment = current_segment.add(1);
}
if [ID!(NULL), ID!(RubyVM), ID!(ZJIT), ID(0)] == segments {
debug_assert_ne!(ID!(NULL), ID(0));
let ruby_vm_mod = rb_const_lookup(rb_cObject, ID!(RubyVM));
if !ruby_vm_mod.is_null() && (*ruby_vm_mod).value == rb_cRubyVM {
let zjit_module = VALUE(state::ZJIT_MODULE.load(Ordering::Relaxed));
let lookedup_module = rb_const_lookup(rb_cRubyVM, ID!(ZJIT));
if !lookedup_module.is_null() && (*lookedup_module).value == zjit_module {
fun.insn_types[get_const_path.0] = Type::from_value(zjit_module);
}
}
}
}
}
YARVINSN_branchunless | YARVINSN_branchunless_without_ints => {
if opcode == YARVINSN_branchunless {
Expand Down Expand Up @@ -7581,6 +7610,28 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
break; // End the block
}
let argc = unsafe { vm_ci_argc((*cd).ci) };
let mid = unsafe { rb_vm_ci_mid(call_info) };

// Check for calls to directives
if argc == 0
&& (mid == ID!(induce_side_exit_bang) || mid == ID!(induce_compile_failure_bang))
&& fun.type_of(state.stack_top()?)
.ruby_object()
.is_some_and(|obj| obj == VALUE(state::ZJIT_MODULE.load(Ordering::Relaxed)))
{

if mid == ID!(induce_side_exit_bang)
&& state::zjit_module_method_match_serial(ID!(induce_side_exit_bang), &state::INDUCE_SIDE_EXIT_SERIAL)
{
fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::DirectiveInduced });
break; // End the block
}
if mid == ID!(induce_compile_failure_bang)
&& state::zjit_module_method_match_serial(ID!(induce_compile_failure_bang), &state::INDUCE_COMPILE_FAILURE_SERIAL)
{
return Err(ParseError::DirectiveInduced);
}
}

{
fn new_branch_block(
Expand Down Expand Up @@ -7619,10 +7670,16 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
let entry_args = state.as_args(self_param);
if let Some(summary) = fun.polymorphic_summary(&profiles, recv, exit_state.insn_idx) {
let join_block = insn_idx_to_block.get(&insn_idx).copied().unwrap_or_else(|| fun.new_block(insn_idx));
// TODO(max): Only iterate over unique classes, not unique (class, shape) pairs.
// Dedup by expected type so immediate/heap variants
// under the same Ruby class can still get separate branches.
let mut seen_types = Vec::with_capacity(summary.buckets().len());
for &profiled_type in summary.buckets() {
if profiled_type.is_empty() { break; }
let expected = Type::from_profiled_type(profiled_type);
if seen_types.iter().any(|ty: &Type| ty.bit_equal(expected)) {
continue;
}
seen_types.push(expected);
let has_type = fun.push_insn(block, Insn::HasType { val: recv, expected });
let iftrue_block =
new_branch_block(&mut fun, cd, argc as usize, opcode, expected, branch_insn_idx, &exit_state, locals_count, stack_count, join_block);
Expand Down
Loading