diff --git a/configure.ac b/configure.ac index da5afc00185f95..b035e1688a8461 100644 --- a/configure.ac +++ b/configure.ac @@ -3974,7 +3974,7 @@ AS_CASE(["${ZJIT_SUPPORT}"], [yes], [ ], [dev], [ - rb_cargo_features="$rb_cargo_features,disasm" + rb_cargo_features="$rb_cargo_features,disasm,runtime_checks" JIT_CARGO_SUPPORT=dev AC_DEFINE(RUBY_DEBUG, 1) ], diff --git a/lib/erb.rb b/lib/erb.rb index 61d577d18be6f5..bb2baf089f739b 100644 --- a/lib/erb.rb +++ b/lib/erb.rb @@ -135,18 +135,111 @@ # # ## Bindings # -# The first example above passed no argument to method `result`; -# the second example passed argument `binding`. +# A call to method #result, which produces the formatted result string, +# requires a [Binding object][binding object] as its argument. # -# Here's why: +# The binding object provides the bindings for expressions in [expression tags][expression tags]. # -# - The first example has tag `<%= Time.now %>`, -# which cites *globally-defined* constant `Time`; -# the default `binding` (details not needed here) includes the binding of global constant `Time` to its value. -# - The second example has tag `<%= magic_word %>`, -# which cites *locally-defined* variable `magic_word`; -# the passed argument `binding` (which is simply a call to method [Kernel#binding][kernel#binding]) -# includes the binding of local variable `magic_word` to its value. +# There are three ways to provide the required binding: +# +# - [Default binding][default binding]. +# - [Local binding][local binding]. +# - [Augmented binding][augmented binding] +# +# ### Default Binding +# +# When you pass no `binding` argument to method #result, +# the method uses its default binding: the one returned by method #new_toplevel. +# This binding has the bindings defined by Ruby itself, +# which are those for Ruby's constants and variables. +# +# That binding is sufficient for an expression tag that refers only to Ruby's constants and variables; +# these expression tags refer only to Ruby's global constant `RUBY_COPYRIGHT` and global variable `$0`: +# +# ``` +# s = <. +# The current process is <%= $0 %>. +# EOT +# puts ERB.new(s).result +# The Ruby copyright is "ruby - Copyright (C) 1993-2025 Yukihiro Matsumoto". +# The current process is irb. +# ``` +# +# (The current process is `irb` because that's where we're doing these examples!) +# +# +# ### Local Binding +# +# The default binding is *not* sufficient for an expression +# that refers to a a constant or variable that is not defined there: +# +# ``` +# Foo = 1 # Defines local constant Foo. +# foo = 2 # Defines local variable foo. +# s = <. +# The current value of variable foo is <%= foo %>. +# The Ruby copyright is <%= RUBY_COPYRIGHT.inspect %>. +# The current process is <%= $0 %>. +# EOT +# ``` +# +# This call raises `NameError` because although `Foo` and `foo` are defined locally, +# they are not defined in the default binding: +# +# ``` +# ERB.new(s).result # Raises NameError. +# ``` +# +# To make the locally-defined constants and variables available, +# you can call #result with the local binding: +# +# ``` +# puts ERB.new(s).result(binding) +# The current value of constant Foo is 1. +# The current value of variable foo is 2. +# The Ruby copyright is "ruby - Copyright (C) 1993-2025 Yukihiro Matsumoto". +# The current process is irb. +# ``` +# +# ### Augmented Binding +# +# Another way to make variable bindings (but not constant bindings) available +# is to use method #result_with_hash(hash); +# the passed hash has name/value pairs that are to be used to define and assign variables +# in a copy of the default binding: +# +# ``` +# s = <. +# The current value of variable baz is <%= baz %>. +# The Ruby copyright is <%= RUBY_COPYRIGHT.inspect %>. +# The current process is <%= $0 %>. +# ``` +# +# Both of these calls raise `NameError`, because `bar` and `baz` +# are not defined in either the default binding or the local binding. +# +# ``` +# puts ERB.new(s).result # Raises NameError. +# puts ERB.new(s).result(binding) # Raises NameError. +# ``` +# +# This call passes a hash that causes `bar` and `baz` to be defined +# in a new binding (derived from #new_toplevel): +# +# ``` +# hash = {bar: 3, baz: 4} +# # => {bar: 3, baz: 4} +# ERB.new(s).result_with_hash(hash) +# puts ERB.new(s).result_with_hash(variables) +# The current value of variable bar is 3. +# The current value of variable baz is 4. +# The Ruby copyright is "ruby - Copyright (C) 1993-2025 Yukihiro Matsumoto". +# The current process is irb. +# EOT +# ``` # # ## Tags # @@ -608,13 +701,16 @@ # Other popular template processors may found in the [Template Engines][template engines] page # of the Ruby Toolbox. # +# [%q literals]: https://docs.ruby-lang.org/en/master/syntax/literals_rdoc.html#label-25q-3A+Non-Interpolable+String+Literals +# [augmented binding]: rdoc-ref:ERB@Augmented+Binding # [binding object]: https://docs.ruby-lang.org/en/master/Binding.html # [comment tags]: rdoc-ref:ERB@Comment+Tags +# [default binding]: rdoc-ref:ERB@Default+Binding # [encoding]: https://docs.ruby-lang.org/en/master/Encoding.html # [execution tags]: rdoc-ref:ERB@Execution+Tags # [expression tags]: rdoc-ref:ERB@Expression+Tags # [kernel#binding]: https://docs.ruby-lang.org/en/master/Kernel.html#method-i-binding -# [%q literals]: https://docs.ruby-lang.org/en/master/syntax/literals_rdoc.html#label-25q-3A+Non-Interpolable+String+Literals +# [local binding]: rdoc-ref:ERB@Local+Binding # [magic comments]: https://docs.ruby-lang.org/en/master/syntax/comments_rdoc.html#label-Magic+Comments # [rdoc]: https://ruby.github.io/rdoc # [sprintf]: https://docs.ruby-lang.org/en/master/Kernel.html#method-i-sprintf @@ -785,27 +881,20 @@ def run(b=new_toplevel) # :markup: markdown # # :call-seq: - # result(binding = top_level) -> string + # result(binding = new_toplevel) -> new_string # - # Formats the string stored in template `self`; - # returns the string result: + # Returns the new string formed by processing \ERB tags found in the stored string in `self`. # - # - Each [expression tag][expression tags] is replaced by the value of the embedded expression. - # - Each [execution tag][execution tags] is removed, and its embedded code is executed. - # - Each [comment tag][comment tags] is removed. + # With no argument given, uses the default binding; + # see [Default Binding][default binding]. # - # See examples at the links. + # With argument `binding` given, uses the local binding; + # see [Local Binding][local binding]. # - # Argument `binding` is a [binding object], - # which should contain the bindings needed for all expression tags; - # the default is #top_level. - # See [Bindings][bindings]. + # See also #result_with_hash. # - # [binding object]: https://docs.ruby-lang.org/en/master/Binding.html - # [bindings]: rdoc-ref:ERB@Bindings - # [comment tags]: rdoc-ref:ERB@Comment+Tags - # [execution tags]: rdoc-ref:ERB@Execution+Tags - # [expression tags]: rdoc-ref:ERB@Expression+Tags + # [default binding]: rdoc-ref:ERB@Default+Binding + # [local binding]: rdoc-ref:ERB@Local+Binding # def result(b=new_toplevel) unless @_init.equal?(self.class.singleton_class) @@ -814,8 +903,18 @@ def result(b=new_toplevel) eval(@src, b, (@filename || '(erb)'), @lineno) end - # Render a template on a new toplevel binding with local variables specified - # by a Hash object. + # :markup: markdown + # + # :call-seq: + # result_with_hash(hash) -> string + # + # Returns the new string formed by processing \ERB tags found in the stored string in `self`; + # see [Augmented Binding][augmented binding]. + # + # See also #result. + # + # [augmented binding]: rdoc-ref:ERB@Augmented+Binding + # def result_with_hash(hash) b = new_toplevel(hash.keys) hash.each_pair do |key, value| diff --git a/zjit.c b/zjit.c index 565a362a8a6aef..6bbe508f241a67 100644 --- a/zjit.c +++ b/zjit.c @@ -164,6 +164,12 @@ rb_zjit_insn_leaf(int insn, const VALUE *opes) return insn_leaf(insn, opes); } +ID +rb_zjit_local_id(const rb_iseq_t *iseq, unsigned idx) +{ + return ISEQ_BODY(iseq)->local_table[idx]; +} + // Primitives used by zjit.rb. Don't put other functions below, which wouldn't use them. VALUE rb_zjit_assert_compiles(rb_execution_context_t *ec, VALUE self); VALUE rb_zjit_stats(rb_execution_context_t *ec, VALUE self, VALUE target_key); diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index 7d66bf0ecf3f16..c6f02be415b83a 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -271,6 +271,7 @@ fn main() { .allowlist_function("rb_zjit_iseq_builtin_attrs") .allowlist_function("rb_zjit_iseq_inspect") .allowlist_function("rb_zjit_iseq_insn_set") + .allowlist_function("rb_zjit_local_id") .allowlist_function("rb_set_cfp_(pc|sp)") .allowlist_function("rb_c_method_tracing_currently_enabled") .allowlist_function("rb_full_cfunc_return") diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index 5f96d0718a9d9f..21adc42cd1c753 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::fmt; use std::mem::take; use crate::codegen::local_size_and_idx_to_ep_offset; -use crate::cruby::{Qundef, RUBY_OFFSET_CFP_PC, RUBY_OFFSET_CFP_SP, SIZEOF_VALUE_I32}; +use crate::cruby::{Qundef, RUBY_OFFSET_CFP_PC, RUBY_OFFSET_CFP_SP, SIZEOF_VALUE_I32, vm_stack_canary}; use crate::hir::SideExitReason; use crate::options::{debug, get_option}; use crate::cruby::VALUE; @@ -1171,6 +1171,9 @@ pub struct Assembler { /// Names of labels pub(super) label_names: Vec, + + /// If Some, the next ccall should verify its leafness + leaf_ccall_stack_size: Option } impl Assembler @@ -1190,9 +1193,32 @@ impl Assembler insns: Vec::with_capacity(ASSEMBLER_INSNS_CAPACITY), live_ranges, label_names, + leaf_ccall_stack_size: None, } } + pub fn expect_leaf_ccall(&mut self, stack_size: usize) { + self.leaf_ccall_stack_size = Some(stack_size); + } + + fn set_stack_canary(&mut self) -> Option { + if cfg!(feature = "runtime_checks") { + if let Some(stack_size) = self.leaf_ccall_stack_size.take() { + let canary_addr = self.lea(Opnd::mem(64, SP, (stack_size as i32) * SIZEOF_VALUE_I32)); + let canary_opnd = Opnd::mem(64, canary_addr, 0); + self.mov(canary_opnd, vm_stack_canary().into()); + return Some(canary_opnd) + } + } + None + } + + fn clear_stack_canary(&mut self, canary_opnd: Option){ + if let Some(canary_opnd) = canary_opnd { + self.store(canary_opnd, 0.into()); + }; + } + /// Build an Opnd::VReg and initialize its LiveRange pub(super) fn new_vreg(&mut self, num_bits: u8) -> Opnd { let vreg = Opnd::VReg { idx: self.live_ranges.len(), num_bits }; @@ -1657,8 +1683,10 @@ impl Assembler { /// Call a C function without PosMarkers pub fn ccall(&mut self, fptr: *const u8, opnds: Vec) -> Opnd { + let canary_opnd = self.set_stack_canary(); let out = self.new_vreg(Opnd::match_num_bits(&opnds)); self.push_insn(Insn::CCall { fptr, opnds, start_marker: None, end_marker: None, out }); + self.clear_stack_canary(canary_opnd); out } diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 1f71ad9db3b2b8..12bd6d4aa3f104 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -350,7 +350,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::NewRange { low, high, flag, state } => gen_new_range(jit, asm, opnd!(low), opnd!(high), *flag, &function.frame_state(*state)), Insn::NewRangeFixnum { low, high, flag, state } => gen_new_range_fixnum(asm, opnd!(low), opnd!(high), *flag, &function.frame_state(*state)), Insn::ArrayDup { val, state } => gen_array_dup(asm, opnd!(val), &function.frame_state(*state)), - Insn::ObjectAlloc { val, state } => gen_object_alloc(asm, opnd!(val), &function.frame_state(*state)), + Insn::ObjectAlloc { val, state } => gen_object_alloc(jit, asm, opnd!(val), &function.frame_state(*state)), Insn::StringCopy { val, chilled, state } => gen_string_copy(asm, opnd!(val), *chilled, &function.frame_state(*state)), // concatstrings shouldn't have 0 strings // If it happens we abort the compilation for now @@ -660,7 +660,7 @@ fn gen_getglobal(jit: &mut JITState, asm: &mut Assembler, id: ID, state: &FrameS /// Intern a string fn gen_intern(asm: &mut Assembler, val: Opnd, state: &FrameState) -> Opnd { - gen_prepare_call_with_gc(asm, state); + gen_prepare_leaf_call_with_gc(asm, state); asm_ccall!(asm, rb_str_intern, val) } @@ -713,7 +713,7 @@ fn gen_getspecial_number(asm: &mut Assembler, nth: u64, state: &FrameState) -> O let backref = asm_ccall!(asm, rb_backref_get,); - gen_prepare_call_with_gc(asm, state); + gen_prepare_leaf_call_with_gc(asm, state); asm_ccall!(asm, rb_reg_nth_match, Opnd::Imm((nth >> 1).try_into().unwrap()), backref) } @@ -730,12 +730,12 @@ fn gen_check_interrupts(jit: &mut JITState, asm: &mut Assembler, state: &FrameSt } fn gen_hash_dup(asm: &mut Assembler, val: Opnd, state: &FrameState) -> lir::Opnd { - gen_prepare_call_with_gc(asm, state); + gen_prepare_leaf_call_with_gc(asm, state); asm_ccall!(asm, rb_hash_resurrect, val) } fn gen_array_push(asm: &mut Assembler, array: Opnd, val: Opnd, state: &FrameState) { - gen_prepare_call_with_gc(asm, state); + gen_prepare_leaf_call_with_gc(asm, state); asm_ccall!(asm, rb_ary_push, array, val); } @@ -983,14 +983,7 @@ fn gen_send( gen_incr_counter(asm, Counter::dynamic_send_count); gen_incr_counter(asm, Counter::dynamic_send_type_send); - // Save PC and SP - gen_prepare_call_with_gc(asm, state); - gen_save_sp(asm, state.stack().len()); - - // Spill locals and stack - gen_spill_locals(jit, asm, state); - gen_spill_stack(jit, asm, state); - + gen_prepare_non_leaf_call(jit, asm, state); asm_comment!(asm, "call #{} with dynamic dispatch", ruby_call_method_name(cd)); unsafe extern "C" { fn rb_vm_send(ec: EcPtr, cfp: CfpPtr, cd: VALUE, blockiseq: IseqPtr) -> VALUE; @@ -1010,20 +1003,7 @@ fn gen_send_without_block( ) -> lir::Opnd { gen_incr_counter(asm, Counter::dynamic_send_count); gen_incr_counter(asm, Counter::dynamic_send_type_send_without_block); - - // Note that it's incorrect to use this frame state to side exit because - // the state might not be on the boundary of an interpreter instruction. - // For example, `opt_str_uminus` pushes to the stack and then sends. - asm_comment!(asm, "spill frame state"); - - // Save PC and SP - gen_prepare_call_with_gc(asm, state); - gen_save_sp(asm, state.stack().len()); - - // Spill locals and stack - gen_spill_locals(jit, asm, state); - gen_spill_stack(jit, asm, state); - + gen_prepare_non_leaf_call(jit, asm, state); asm_comment!(asm, "call #{} with dynamic dispatch", ruby_call_method_name(cd)); unsafe extern "C" { fn rb_vm_opt_send_without_block(ec: EcPtr, cfp: CfpPtr, cd: VALUE) -> VALUE; @@ -1059,7 +1039,8 @@ fn gen_send_without_block_direct( asm.jbe(side_exit(jit, state, StackOverflow)); // Save cfp->pc and cfp->sp for the caller frame - gen_prepare_call_with_gc(asm, state); + gen_prepare_call_with_gc(asm, state, false); + // Special SP math. Can't use gen_prepare_non_leaf_call gen_save_sp(asm, state.stack().len() - args.len() - 1); // -1 for receiver gen_spill_locals(jit, asm, state); @@ -1119,11 +1100,7 @@ fn gen_invokeblock( gen_incr_counter(asm, Counter::dynamic_send_count); gen_incr_counter(asm, Counter::dynamic_send_type_invokeblock); - // Save PC and SP, spill locals and stack - gen_prepare_call_with_gc(asm, state); - gen_save_sp(asm, state.stack().len()); - gen_spill_locals(jit, asm, state); - gen_spill_stack(jit, asm, state); + gen_prepare_non_leaf_call(jit, asm, state); asm_comment!(asm, "call invokeblock"); unsafe extern "C" { @@ -1146,14 +1123,7 @@ fn gen_invokesuper( gen_incr_counter(asm, Counter::dynamic_send_count); gen_incr_counter(asm, Counter::dynamic_send_type_invokesuper); - // Save PC and SP - gen_prepare_call_with_gc(asm, state); - gen_save_sp(asm, state.stack().len()); - - // Spill locals and stack - gen_spill_locals(jit, asm, state); - gen_spill_stack(jit, asm, state); - + gen_prepare_non_leaf_call(jit, asm, state); asm_comment!(asm, "call super with dynamic dispatch"); unsafe extern "C" { fn rb_vm_invokesuper(ec: EcPtr, cfp: CfpPtr, cd: VALUE, blockiseq: IseqPtr) -> VALUE; @@ -1167,7 +1137,7 @@ fn gen_invokesuper( /// Compile a string resurrection fn gen_string_copy(asm: &mut Assembler, recv: Opnd, chilled: bool, state: &FrameState) -> Opnd { // TODO: split rb_ec_str_resurrect into separate functions - gen_prepare_call_with_gc(asm, state); + gen_prepare_leaf_call_with_gc(asm, state); let chilled = if chilled { Opnd::Imm(1) } else { Opnd::Imm(0) }; asm_ccall!(asm, rb_ec_str_resurrect, EC, recv, chilled) } @@ -1178,7 +1148,7 @@ fn gen_array_dup( val: lir::Opnd, state: &FrameState, ) -> lir::Opnd { - gen_prepare_call_with_gc(asm, state); + gen_prepare_leaf_call_with_gc(asm, state); asm_ccall!(asm, rb_ary_resurrect, val) } @@ -1189,7 +1159,7 @@ fn gen_new_array( elements: Vec, state: &FrameState, ) -> lir::Opnd { - gen_prepare_call_with_gc(asm, state); + gen_prepare_leaf_call_with_gc(asm, state); let length: c_long = elements.len().try_into().expect("Unable to fit length of elements into c_long"); @@ -1256,12 +1226,14 @@ fn gen_new_range_fixnum( flag: RangeType, state: &FrameState, ) -> lir::Opnd { - gen_prepare_call_with_gc(asm, state); + gen_prepare_leaf_call_with_gc(asm, state); asm_ccall!(asm, rb_range_new, low, high, (flag as i64).into()) } -fn gen_object_alloc(asm: &mut Assembler, val: lir::Opnd, state: &FrameState) -> lir::Opnd { - gen_prepare_call_with_gc(asm, state); +fn gen_object_alloc(jit: &JITState, asm: &mut Assembler, val: lir::Opnd, state: &FrameState) -> lir::Opnd { + // TODO: this is leaf in the vast majority of cases, + // Should specialize to avoid `gen_prepare_non_leaf_call` (Shopify#747) + gen_prepare_non_leaf_call(jit, asm, state); asm_ccall!(asm, rb_obj_alloc, val) } @@ -1375,7 +1347,7 @@ fn gen_is_method_cfunc(jit: &JITState, asm: &mut Assembler, val: lir::Opnd, cd: } fn gen_anytostring(asm: &mut Assembler, val: lir::Opnd, str: lir::Opnd, state: &FrameState) -> lir::Opnd { - gen_prepare_call_with_gc(asm, state); + gen_prepare_leaf_call_with_gc(asm, state); asm_ccall!(asm, rb_obj_as_string_result, str, val) } @@ -1532,12 +1504,20 @@ fn gen_incr_counter(asm: &mut Assembler, counter: Counter) { /// /// Unlike YJIT, we don't need to save the stack slots to protect them from GC /// because the backend spills all live registers onto the C stack on CCall. -fn gen_prepare_call_with_gc(asm: &mut Assembler, state: &FrameState) { +fn gen_prepare_call_with_gc(asm: &mut Assembler, state: &FrameState, leaf: bool) { let opcode: usize = state.get_opcode().try_into().unwrap(); let next_pc: *const VALUE = unsafe { state.pc.offset(insn_len(opcode) as isize) }; asm_comment!(asm, "save PC to CFP"); asm.mov(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_PC), Opnd::const_ptr(next_pc)); + + if leaf { + asm.expect_leaf_ccall(state.stack_size()); + } +} + +fn gen_prepare_leaf_call_with_gc(asm: &mut Assembler, state: &FrameState) { + gen_prepare_call_with_gc(asm, state, true); } /// Save the current SP on the CFP @@ -1572,11 +1552,11 @@ fn gen_spill_stack(jit: &JITState, asm: &mut Assembler, state: &FrameState) { } /// Prepare for calling a C function that may call an arbitrary method. -/// Use gen_prepare_call_with_gc() if the method is leaf but allocates objects. +/// Use gen_prepare_leaf_call_with_gc() if the method is leaf but allocates objects. fn gen_prepare_non_leaf_call(jit: &JITState, asm: &mut Assembler, state: &FrameState) { // TODO: Lazily materialize caller frames when needed // Save PC for backtraces and allocation tracing - gen_prepare_call_with_gc(asm, state); + gen_prepare_call_with_gc(asm, state, false); // Save SP and spill the virtual stack in case it raises an exception // and the interpreter uses the stack for handling the exception diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 9ee4b1bb743958..4bb1c3dffd4b06 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -936,6 +936,7 @@ unsafe extern "C" { pub fn rb_zjit_singleton_class_p(klass: VALUE) -> bool; pub fn rb_zjit_defined_ivar(obj: VALUE, id: ID, pushval: VALUE) -> VALUE; pub fn rb_zjit_insn_leaf(insn: ::std::os::raw::c_int, opes: *const VALUE) -> bool; + pub fn rb_zjit_local_id(iseq: *const rb_iseq_t, idx: ::std::os::raw::c_uint) -> ID; pub fn rb_iseq_encoded_size(iseq: *const rb_iseq_t) -> ::std::os::raw::c_uint; pub fn rb_iseq_pc_at_idx(iseq: *const rb_iseq_t, insn_idx: u32) -> *mut VALUE; pub fn rb_iseq_opcode_at_pc(iseq: *const rb_iseq_t, pc: *const VALUE) -> ::std::os::raw::c_int; diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 1a268e62da97f7..033531fd566a7d 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -2903,9 +2903,14 @@ impl Display for FrameStatePrinter<'_> { let inner = self.inner; write!(f, "FrameState {{ pc: {:?}, stack: ", self.ptr_map.map_ptr(inner.pc))?; write_vec(f, &inner.stack)?; - write!(f, ", locals: ")?; - write_vec(f, &inner.locals)?; - write!(f, " }}") + write!(f, ", locals: [")?; + for (idx, local) in inner.locals.iter().enumerate() { + let name: ID = unsafe { rb_zjit_local_id(inner.iseq, idx.try_into().unwrap()) }; + let name = name.contents_lossy(); + if idx > 0 { write!(f, ", ")?; } + write!(f, "{name}={local}")?; + } + write!(f, "] }}") } } @@ -4228,6 +4233,39 @@ mod infer_tests { } } +#[cfg(test)] +mod snapshot_tests { + use super::*; + use insta::assert_snapshot; + + #[track_caller] + fn hir_string(method: &str) -> String { + let iseq = crate::cruby::with_rubyvm(|| get_method_iseq("self", method)); + unsafe { crate::cruby::rb_zjit_profile_disable(iseq) }; + let function = iseq_to_hir(iseq).unwrap(); + format!("{}", FunctionPrinter::with_snapshot(&function)) + } + + #[test] + fn test_new_array_with_elements() { + eval("def test(a, b) = [a, b]"); + assert_snapshot!(hir_string("test"), @r" + fn test@:1: + bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject): + v3:Any = Snapshot FrameState { pc: 0x1000, stack: [], locals: [a=v1, b=v2] } + v4:Any = Snapshot FrameState { pc: 0x1008, stack: [], locals: [a=v1, b=v2] } + PatchPoint NoTracePoint + v6:Any = Snapshot FrameState { pc: 0x1010, stack: [v1, v2], locals: [a=v1, b=v2] } + v7:ArrayExact = NewArray v1, v2 + v8:Any = Snapshot FrameState { pc: 0x1018, stack: [v7], locals: [a=v1, b=v2] } + PatchPoint NoTracePoint + v10:Any = Snapshot FrameState { pc: 0x1018, stack: [v7], locals: [a=v1, b=v2] } + CheckInterrupts + Return v7 + "); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 0eb2b8687bdd48..18e12d3af556be 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -115,10 +115,6 @@ make_counters! { exit_block_param_proxy_not_iseq_or_ifunc, } - // unhanded_call_: Unhandled call types - unhandled_call_block_arg, - unhandled_call_tailcall, - // compile_error_: Compile error reasons compile_error_iseq_stack_too_large, compile_error_exception_handler,