From 0e0fad1e58f23ee0e12ab06c1bcd171cba2947f2 Mon Sep 17 00:00:00 2001 From: Nozomi Hijikata <121233810+nozomemein@users.noreply.github.com> Date: Sat, 17 Jan 2026 11:19:54 +0900 Subject: [PATCH 1/4] ZJIT: Specialize OPTIMIZED_METHOD_TYPE_CALL (#15859) Closes: https://github.com/Shopify/ruby/issues/865 ## Benchmark ### lobsters - wall clock time - before patch: Average of last 10, non-warmup iters: 809ms - after patch: Average of last 10, non-warmup iters: 754ms - zjit stats below
before patch ``` ***ZJIT: Printing ZJIT statistics on exit*** Top-20 not inlined C methods (54.9% of total 18,003,698): Hash#fetch: 3,184,106 (17.7%) Regexp#match?: 707,148 ( 3.9%) Hash#key?: 689,879 ( 3.8%) String#sub!: 489,841 ( 2.7%) Array#include?: 470,648 ( 2.6%) Set#include?: 397,520 ( 2.2%) String#<<: 396,279 ( 2.2%) String#start_with?: 373,666 ( 2.1%) Kernel#dup: 352,617 ( 2.0%) Array#any?: 350,454 ( 1.9%) Hash#delete: 331,784 ( 1.8%) String.new: 307,248 ( 1.7%) Integer#===: 262,336 ( 1.5%) Symbol#end_with?: 255,538 ( 1.4%) Kernel#is_a?: 247,292 ( 1.4%) Process.clock_gettime: 221,588 ( 1.2%) Integer#>: 219,718 ( 1.2%) String#match?: 216,903 ( 1.2%) String#downcase: 213,108 ( 1.2%) Integer#<=: 202,617 ( 1.1%) Top-20 calls to C functions from JIT code (80.3% of total 130,255,689): rb_vm_opt_send_without_block: 28,329,698 (21.7%) rb_hash_aref: 8,992,191 ( 6.9%) rb_vm_env_write: 8,526,087 ( 6.5%) rb_vm_send: 8,337,448 ( 6.4%) rb_zjit_writebarrier_check_immediate: 7,809,310 ( 6.0%) rb_obj_is_kind_of: 6,098,929 ( 4.7%) rb_vm_getinstancevariable: 5,783,055 ( 4.4%) rb_vm_invokesuper: 5,038,443 ( 3.9%) rb_ivar_get_at_no_ractor_check: 4,762,093 ( 3.7%) rb_ary_entry: 4,283,966 ( 3.3%) rb_hash_aset: 2,429,862 ( 1.9%) rb_vm_setinstancevariable: 2,343,571 ( 1.8%) rb_vm_opt_getconstant_path: 2,284,810 ( 1.8%) Hash#fetch: 1,778,515 ( 1.4%) fetch: 1,405,591 ( 1.1%) rb_vm_invokeblock: 1,381,332 ( 1.1%) rb_str_buf_append: 1,362,272 ( 1.0%) rb_ec_ary_new_from_values: 1,324,997 ( 1.0%) rb_class_allocate_instance: 1,288,936 ( 1.0%) rb_hash_new_with_size: 998,628 ( 0.8%) Top-2 not optimized method types for send (100.0% of total 4,896,274): iseq: 4,893,452 (99.9%) null: 2,822 ( 0.1%) Top-4 not optimized method types for send_without_block (100.0% of total 782,296): optimized_send: 479,562 (61.3%) optimized_call: 256,609 (32.8%) null: 41,967 ( 5.4%) optimized_block_call: 4,158 ( 0.5%) Top-4 instructions with uncategorized fallback reason (100.0% of total 7,250,555): invokesuper: 5,038,443 (69.5%) invokeblock: 1,381,332 (19.1%) sendforward: 798,924 (11.0%) opt_send_without_block: 31,856 ( 0.4%) Top-18 send fallback reasons (100.0% of total 43,885,845): send_without_block_polymorphic: 18,533,639 (42.2%) uncategorized: 7,250,555 (16.5%) send_not_optimized_method_type: 4,896,274 (11.2%) send_without_block_no_profiles: 4,741,871 (10.8%) send_no_profiles: 2,865,577 ( 6.5%) one_or_more_complex_arg_pass: 2,825,240 ( 6.4%) send_without_block_not_optimized_method_type_optimized: 740,329 ( 1.7%) send_without_block_megamorphic: 709,818 ( 1.6%) send_polymorphic: 541,186 ( 1.2%) send_without_block_not_optimized_need_permission: 382,622 ( 0.9%) too_many_args_for_lir: 173,244 ( 0.4%) argc_param_mismatch: 50,382 ( 0.1%) send_without_block_not_optimized_method_type: 41,967 ( 0.1%) send_without_block_cfunc_array_variadic: 36,302 ( 0.1%) obj_to_string_not_string: 34,169 ( 0.1%) send_without_block_direct_keyword_mismatch: 32,436 ( 0.1%) send_megamorphic: 28,613 ( 0.1%) ccall_with_frame_too_many_args: 1,621 ( 0.0%) Top-4 setivar fallback reasons (100.0% of total 2,343,571): not_monomorphic: 2,120,856 (90.5%) not_t_object: 125,163 ( 5.3%) too_complex: 97,531 ( 4.2%) new_shape_needs_extension: 21 ( 0.0%) Top-2 getivar fallback reasons (100.0% of total 5,908,168): not_monomorphic: 5,658,909 (95.8%) too_complex: 249,259 ( 4.2%) Top-3 definedivar fallback reasons (100.0% of total 405,079): not_monomorphic: 397,150 (98.0%) too_complex: 5,122 ( 1.3%) not_t_object: 2,807 ( 0.7%) Top-6 invokeblock handler (100.0% of total 1,381,332): monomorphic_iseq: 685,359 (49.6%) polymorphic: 521,992 (37.8%) monomorphic_other: 104,640 ( 7.6%) monomorphic_ifunc: 55,505 ( 4.0%) no_profiles: 9,164 ( 0.7%) megamorphic: 4,672 ( 0.3%) Top-9 popular complex argument-parameter features not optimized (100.0% of total 3,097,538): param_kw_opt: 1,333,367 (43.0%) param_block: 632,885 (20.4%) param_forwardable: 600,601 (19.4%) param_rest: 329,020 (10.6%) param_kwrest: 119,971 ( 3.9%) caller_kw_splat: 39,001 ( 1.3%) caller_splat: 36,785 ( 1.2%) caller_blockarg: 5,798 ( 0.2%) caller_kwarg: 110 ( 0.0%) Top-1 compile error reasons (100.0% of total 186,900): exception_handler: 186,900 (100.0%) Top-7 unhandled YARV insns (100.0% of total 186,598): getblockparam: 99,414 (53.3%) invokesuperforward: 81,667 (43.8%) setblockparam: 2,837 ( 1.5%) getconstant: 1,537 ( 0.8%) checkmatch: 616 ( 0.3%) expandarray: 360 ( 0.2%) once: 167 ( 0.1%) Top-3 unhandled HIR insns (100.0% of total 236,962): throw: 198,474 (83.8%) invokebuiltin: 35,767 (15.1%) array_max: 2,721 ( 1.1%) Top-19 side exit reasons (100.0% of total 15,427,184): guard_type_failure: 6,865,696 (44.5%) guard_shape_failure: 6,779,586 (43.9%) block_param_proxy_not_iseq_or_ifunc: 1,030,319 ( 6.7%) unhandled_hir_insn: 236,962 ( 1.5%) compile_error: 186,900 ( 1.2%) unhandled_yarv_insn: 186,598 ( 1.2%) fixnum_mult_overflow: 50,739 ( 0.3%) block_param_proxy_modified: 28,119 ( 0.2%) patchpoint_no_singleton_class: 14,903 ( 0.1%) unhandled_newarray_send_pack: 14,481 ( 0.1%) fixnum_lshift_overflow: 10,085 ( 0.1%) patchpoint_stable_constant_names: 9,198 ( 0.1%) patchpoint_no_ep_escape: 7,815 ( 0.1%) expandarray_failure: 4,533 ( 0.0%) patchpoint_method_redefined: 662 ( 0.0%) obj_to_string_fallback: 277 ( 0.0%) guard_less_failure: 163 ( 0.0%) interrupt: 128 ( 0.0%) guard_greater_eq_failure: 20 ( 0.0%) send_count: 151,233,937 dynamic_send_count: 43,885,845 (29.0%) optimized_send_count: 107,348,092 (71.0%) dynamic_setivar_count: 2,343,571 ( 1.5%) dynamic_getivar_count: 5,908,168 ( 3.9%) dynamic_definedivar_count: 405,079 ( 0.3%) iseq_optimized_send_count: 37,324,023 (24.7%) inline_cfunc_optimized_send_count: 46,056,028 (30.5%) inline_iseq_optimized_send_count: 3,756,875 ( 2.5%) non_variadic_cfunc_optimized_send_count: 11,618,909 ( 7.7%) variadic_cfunc_optimized_send_count: 8,592,257 ( 5.7%) compiled_iseq_count: 5,289 failed_iseq_count: 0 compile_time: 1,664ms profile_time: 13ms gc_time: 20ms invalidation_time: 479ms vm_write_pc_count: 127,571,422 vm_write_sp_count: 127,571,422 vm_write_locals_count: 122,781,971 vm_write_stack_count: 122,781,971 vm_write_to_parent_iseq_local_count: 689,945 vm_read_from_parent_iseq_local_count: 14,721,820 guard_type_count: 167,633,896 guard_type_exit_ratio: 4.1% guard_shape_count: 0 code_region_bytes: 38,912,000 zjit_alloc_bytes: 40,542,102 total_mem_bytes: 79,454,102 side_exit_count: 15,427,184 total_insn_count: 927,373,567 vm_insn_count: 156,976,359 zjit_insn_count: 770,397,208 ratio_in_zjit: 83.1% ```
after patch ``` ***ZJIT: Printing ZJIT statistics on exit*** Top-20 not inlined C methods (55.0% of total 18,012,630): Hash#fetch: 3,184,101 (17.7%) Regexp#match?: 707,150 ( 3.9%) Hash#key?: 689,871 ( 3.8%) String#sub!: 489,841 ( 2.7%) Array#include?: 470,648 ( 2.6%) Set#include?: 397,520 ( 2.2%) String#<<: 396,279 ( 2.2%) String#start_with?: 382,538 ( 2.1%) Kernel#dup: 352,617 ( 2.0%) Array#any?: 350,454 ( 1.9%) Hash#delete: 331,802 ( 1.8%) String.new: 307,248 ( 1.7%) Integer#===: 262,336 ( 1.5%) Symbol#end_with?: 255,540 ( 1.4%) Kernel#is_a?: 247,292 ( 1.4%) Process.clock_gettime: 221,588 ( 1.2%) Integer#>: 219,718 ( 1.2%) String#match?: 216,905 ( 1.2%) String#downcase: 213,107 ( 1.2%) Integer#<=: 202,617 ( 1.1%) Top-20 calls to C functions from JIT code (80.1% of total 130,218,934): rb_vm_opt_send_without_block: 28,073,153 (21.6%) rb_hash_aref: 8,992,167 ( 6.9%) rb_vm_env_write: 8,526,089 ( 6.5%) rb_vm_send: 8,337,453 ( 6.4%) rb_zjit_writebarrier_check_immediate: 7,786,426 ( 6.0%) rb_obj_is_kind_of: 6,098,927 ( 4.7%) rb_vm_getinstancevariable: 5,783,053 ( 4.4%) rb_vm_invokesuper: 5,038,444 ( 3.9%) rb_ivar_get_at_no_ractor_check: 4,762,093 ( 3.7%) rb_ary_entry: 4,283,965 ( 3.3%) rb_hash_aset: 2,429,864 ( 1.9%) rb_vm_setinstancevariable: 2,343,573 ( 1.8%) rb_vm_opt_getconstant_path: 2,284,809 ( 1.8%) Hash#fetch: 1,778,510 ( 1.4%) fetch: 1,405,591 ( 1.1%) rb_vm_invokeblock: 1,381,329 ( 1.1%) rb_str_buf_append: 1,362,272 ( 1.0%) rb_ec_ary_new_from_values: 1,325,005 ( 1.0%) rb_class_allocate_instance: 1,288,944 ( 1.0%) rb_hash_new_with_size: 998,629 ( 0.8%) Top-2 not optimized method types for send (100.0% of total 4,896,276): iseq: 4,893,454 (99.9%) null: 2,822 ( 0.1%) Top-3 not optimized method types for send_without_block (100.0% of total 525,687): optimized_send: 479,562 (91.2%) null: 41,967 ( 8.0%) optimized_block_call: 4,158 ( 0.8%) Top-4 instructions with uncategorized fallback reason (100.0% of total 7,250,556): invokesuper: 5,038,444 (69.5%) invokeblock: 1,381,329 (19.1%) sendforward: 798,924 (11.0%) opt_send_without_block: 31,859 ( 0.4%) Top-18 send fallback reasons (100.0% of total 43,629,303): send_without_block_polymorphic: 18,533,669 (42.5%) uncategorized: 7,250,556 (16.6%) send_not_optimized_method_type: 4,896,276 (11.2%) send_without_block_no_profiles: 4,741,899 (10.9%) send_no_profiles: 2,865,579 ( 6.6%) one_or_more_complex_arg_pass: 2,825,242 ( 6.5%) send_without_block_megamorphic: 709,818 ( 1.6%) send_polymorphic: 541,187 ( 1.2%) send_without_block_not_optimized_method_type_optimized: 483,720 ( 1.1%) send_without_block_not_optimized_need_permission: 382,623 ( 0.9%) too_many_args_for_lir: 173,244 ( 0.4%) argc_param_mismatch: 50,382 ( 0.1%) send_without_block_not_optimized_method_type: 41,967 ( 0.1%) send_without_block_cfunc_array_variadic: 36,302 ( 0.1%) obj_to_string_not_string: 34,169 ( 0.1%) send_without_block_direct_keyword_mismatch: 32,436 ( 0.1%) send_megamorphic: 28,613 ( 0.1%) ccall_with_frame_too_many_args: 1,621 ( 0.0%) Top-4 setivar fallback reasons (100.0% of total 2,343,573): not_monomorphic: 2,120,858 (90.5%) not_t_object: 125,163 ( 5.3%) too_complex: 97,531 ( 4.2%) new_shape_needs_extension: 21 ( 0.0%) Top-2 getivar fallback reasons (100.0% of total 5,908,165): not_monomorphic: 5,658,912 (95.8%) too_complex: 249,253 ( 4.2%) Top-3 definedivar fallback reasons (100.0% of total 405,079): not_monomorphic: 397,150 (98.0%) too_complex: 5,122 ( 1.3%) not_t_object: 2,807 ( 0.7%) Top-6 invokeblock handler (100.0% of total 1,381,329): monomorphic_iseq: 685,363 (49.6%) polymorphic: 521,984 (37.8%) monomorphic_other: 104,640 ( 7.6%) monomorphic_ifunc: 55,505 ( 4.0%) no_profiles: 9,164 ( 0.7%) megamorphic: 4,673 ( 0.3%) Top-9 popular complex argument-parameter features not optimized (100.0% of total 3,094,719): param_kw_opt: 1,333,367 (43.1%) param_block: 632,886 (20.5%) param_forwardable: 600,605 (19.4%) param_rest: 329,019 (10.6%) param_kwrest: 119,971 ( 3.9%) caller_kw_splat: 39,001 ( 1.3%) caller_splat: 33,962 ( 1.1%) caller_blockarg: 5,798 ( 0.2%) caller_kwarg: 110 ( 0.0%) Top-1 compile error reasons (100.0% of total 186,917): exception_handler: 186,917 (100.0%) Top-7 unhandled YARV insns (100.0% of total 186,598): getblockparam: 99,414 (53.3%) invokesuperforward: 81,667 (43.8%) setblockparam: 2,837 ( 1.5%) getconstant: 1,537 ( 0.8%) checkmatch: 616 ( 0.3%) expandarray: 360 ( 0.2%) once: 167 ( 0.1%) Top-3 unhandled HIR insns (100.0% of total 236,969): throw: 198,475 (83.8%) invokebuiltin: 35,773 (15.1%) array_max: 2,721 ( 1.1%) Top-19 side exit reasons (100.0% of total 15,450,102): guard_type_failure: 6,888,596 (44.6%) guard_shape_failure: 6,779,586 (43.9%) block_param_proxy_not_iseq_or_ifunc: 1,030,319 ( 6.7%) unhandled_hir_insn: 236,969 ( 1.5%) compile_error: 186,917 ( 1.2%) unhandled_yarv_insn: 186,598 ( 1.2%) fixnum_mult_overflow: 50,739 ( 0.3%) block_param_proxy_modified: 28,119 ( 0.2%) patchpoint_no_singleton_class: 14,903 ( 0.1%) unhandled_newarray_send_pack: 14,481 ( 0.1%) fixnum_lshift_overflow: 10,085 ( 0.1%) patchpoint_stable_constant_names: 9,198 ( 0.1%) patchpoint_no_ep_escape: 7,815 ( 0.1%) expandarray_failure: 4,533 ( 0.0%) patchpoint_method_redefined: 662 ( 0.0%) obj_to_string_fallback: 277 ( 0.0%) guard_less_failure: 163 ( 0.0%) interrupt: 122 ( 0.0%) guard_greater_eq_failure: 20 ( 0.0%) send_count: 150,986,368 dynamic_send_count: 43,629,303 (28.9%) optimized_send_count: 107,357,065 (71.1%) dynamic_setivar_count: 2,343,573 ( 1.6%) dynamic_getivar_count: 5,908,165 ( 3.9%) dynamic_definedivar_count: 405,079 ( 0.3%) iseq_optimized_send_count: 37,324,039 (24.7%) inline_cfunc_optimized_send_count: 46,056,046 (30.5%) inline_iseq_optimized_send_count: 3,756,881 ( 2.5%) non_variadic_cfunc_optimized_send_count: 11,618,958 ( 7.7%) variadic_cfunc_optimized_send_count: 8,601,141 ( 5.7%) compiled_iseq_count: 5,289 failed_iseq_count: 0 compile_time: 1,700ms profile_time: 13ms gc_time: 21ms invalidation_time: 519ms vm_write_pc_count: 127,557,549 vm_write_sp_count: 127,557,549 vm_write_locals_count: 122,768,084 vm_write_stack_count: 122,768,084 vm_write_to_parent_iseq_local_count: 689,953 vm_read_from_parent_iseq_local_count: 14,730,705 guard_type_count: 167,853,730 guard_type_exit_ratio: 4.1% guard_shape_count: 0 code_region_bytes: 38,928,384 zjit_alloc_bytes: 41,103,415 total_mem_bytes: 80,031,799 side_exit_count: 15,450,102 total_insn_count: 927,432,364 vm_insn_count: 157,182,251 zjit_insn_count: 770,250,113 ratio_in_zjit: 83.1% ```
--- jit.c | 8 ++ test/ruby/test_zjit.rb | 68 +++++++++++++ yjit.c | 8 -- yjit/src/cruby_bindings.inc.rs | 16 +-- zjit/src/codegen.rs | 30 ++++++ zjit/src/cruby_bindings.inc.rs | 8 ++ zjit/src/hir.rs | 67 ++++++++++-- zjit/src/hir/opt_tests.rs | 180 +++++++++++++++++++++++++++++++++ 8 files changed, 361 insertions(+), 24 deletions(-) diff --git a/jit.c b/jit.c index fb7a5bd47d4919..9399cb4026c6e8 100644 --- a/jit.c +++ b/jit.c @@ -191,6 +191,14 @@ rb_jit_get_proc_ptr(VALUE procv) return proc; } +VALUE +rb_optimized_call(VALUE *recv, rb_execution_context_t *ec, int argc, VALUE *argv, int kw_splat, VALUE block_handler) +{ + rb_proc_t *proc; + GetProcPtr(recv, proc); + return rb_vm_invoke_proc(ec, proc, argc, argv, kw_splat, block_handler); +} + unsigned int rb_jit_iseq_builtin_attrs(const rb_iseq_t *iseq) { diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 6e46346d5af983..ad2df806d59958 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -470,6 +470,74 @@ def test(&block) }, insns: [:getblockparamproxy] end + def test_optimized_method_call_proc_call + assert_compiles '2', %q{ + p = proc { |x| x * 2 } + def test(p) + p.call(1) + end + test(p) + test(p) + }, call_threshold: 2, insns: [:opt_send_without_block] + end + + def test_optimized_method_call_proc_aref + assert_compiles '4', %q{ + p = proc { |x| x * 2 } + def test(p) + p[2] + end + test(p) + test(p) + }, call_threshold: 2, insns: [:opt_aref] + end + + def test_optimized_method_call_proc_yield + assert_compiles '6', %q{ + p = proc { |x| x * 2 } + def test(p) + p.yield(3) + end + test(p) + test(p) + }, call_threshold: 2, insns: [:opt_send_without_block] + end + + def test_optimized_method_call_proc_kw_splat + assert_compiles '3', %q{ + p = proc { |**kw| kw[:a] + kw[:b] } + def test(p, h) + p.call(**h) + end + h = { a: 1, b: 2 } + test(p, h) + test(p, h) + }, call_threshold: 2, insns: [:opt_send_without_block] + end + + def test_optimized_method_call_proc_call_splat + assert_compiles '43', %q{ + p = proc { |x| x + 1 } + def test(p) + ary = [42] + p.call(*ary) + end + test(p) + test(p) + }, call_threshold: 2 + end + + def test_optimized_method_call_proc_call_kwarg + assert_compiles '1', %q{ + p = proc { |a:| a } + def test(p) + p.call(a: 1) + end + test(p) + test(p) + }, call_threshold: 2 + end + def test_call_a_forwardable_method assert_runs '[]', %q{ def test_root = forwardable diff --git a/yjit.c b/yjit.c index 1cd934c42eee63..2e7216a1915406 100644 --- a/yjit.c +++ b/yjit.c @@ -223,14 +223,6 @@ typedef struct rb_iseq_param_keyword rb_seq_param_keyword_struct; ID rb_get_symbol_id(VALUE namep); -VALUE -rb_optimized_call(VALUE *recv, rb_execution_context_t *ec, int argc, VALUE *argv, int kw_splat, VALUE block_handler) -{ - rb_proc_t *proc; - GetProcPtr(recv, proc); - return rb_vm_invoke_proc(ec, proc, argc, argv, kw_splat, block_handler); -} - // If true, the iseq has only opt_invokebuiltin_delegate(_leave) and leave insns. static bool invokebuiltin_delegate_leave_p(const rb_iseq_t *iseq) diff --git a/yjit/src/cruby_bindings.inc.rs b/yjit/src/cruby_bindings.inc.rs index 9fbcf2169f8779..56994388a3a4a1 100644 --- a/yjit/src/cruby_bindings.inc.rs +++ b/yjit/src/cruby_bindings.inc.rs @@ -1164,14 +1164,6 @@ extern "C" { pub fn rb_iseq_get_yjit_payload(iseq: *const rb_iseq_t) -> *mut ::std::os::raw::c_void; pub fn rb_iseq_set_yjit_payload(iseq: *const rb_iseq_t, payload: *mut ::std::os::raw::c_void); pub fn rb_get_symbol_id(namep: VALUE) -> ID; - pub fn rb_optimized_call( - recv: *mut VALUE, - ec: *mut rb_execution_context_t, - argc: ::std::os::raw::c_int, - argv: *mut VALUE, - kw_splat: ::std::os::raw::c_int, - block_handler: VALUE, - ) -> VALUE; pub fn rb_yjit_builtin_function(iseq: *const rb_iseq_t) -> *const rb_builtin_function; pub fn rb_yjit_str_simple_append(str1: VALUE, str2: VALUE) -> VALUE; pub fn rb_vm_base_ptr(cfp: *mut rb_control_frame_struct) -> *mut VALUE; @@ -1240,6 +1232,14 @@ extern "C" { pub fn rb_get_def_original_id(def: *const rb_method_definition_t) -> ID; pub fn rb_get_def_bmethod_proc(def: *mut rb_method_definition_t) -> VALUE; pub fn rb_jit_get_proc_ptr(procv: VALUE) -> *mut rb_proc_t; + pub fn rb_optimized_call( + recv: *mut VALUE, + ec: *mut rb_execution_context_t, + argc: ::std::os::raw::c_int, + argv: *mut VALUE, + kw_splat: ::std::os::raw::c_int, + block_handler: VALUE, + ) -> VALUE; pub fn rb_jit_iseq_builtin_attrs(iseq: *const rb_iseq_t) -> ::std::os::raw::c_uint; pub fn rb_get_mct_argc(mct: *const rb_method_cfunc_t) -> ::std::os::raw::c_int; pub fn rb_get_mct_func(mct: *const rb_method_cfunc_t) -> *mut ::std::os::raw::c_void; diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 1c453aed771055..7afcff5863bc84 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -404,6 +404,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::SendWithoutBlockDirect { cme, iseq, recv, args, state, .. } => gen_send_iseq_direct(cb, jit, asm, *cme, *iseq, opnd!(recv), opnds!(args), &function.frame_state(*state), None), &Insn::InvokeSuper { cd, blockiseq, state, reason, .. } => gen_invokesuper(jit, asm, cd, blockiseq, &function.frame_state(state), reason), &Insn::InvokeBlock { cd, state, reason, .. } => gen_invokeblock(jit, asm, cd, &function.frame_state(state), reason), + Insn::InvokeProc { recv, args, state, kw_splat } => gen_invokeproc(jit, asm, opnd!(recv), opnds!(args), *kw_splat, &function.frame_state(*state)), // Ensure we have enough room fit ec, self, and arguments // TODO remove this check when we have stack args (we can use Time.new to test it) Insn::InvokeBuiltin { bf, state, .. } if bf.argc + 2 > (C_ARG_OPNDS.len() as i32) => return Err(*state), @@ -1497,6 +1498,35 @@ fn gen_invokeblock( ) } +fn gen_invokeproc( + jit: &mut JITState, + asm: &mut Assembler, + recv: Opnd, + args: Vec, + kw_splat: bool, + state: &FrameState, +) -> lir::Opnd { + gen_prepare_non_leaf_call(jit, asm, state); + + asm_comment!(asm, "call invokeproc"); + + let argv_ptr = gen_push_opnds(asm, &args); + let kw_splat_opnd = Opnd::Imm(i64::from(kw_splat)); + let result = asm_ccall!( + asm, + rb_optimized_call, + recv, + EC, + args.len().into(), + argv_ptr, + kw_splat_opnd, + VM_BLOCK_HANDLER_NONE.into() + ); + gen_pop_opnds(asm, &args); + + result +} + /// Compile a dynamic dispatch for `super` fn gen_invokesuper( jit: &mut JITState, diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 2201bdcffee776..15533180dad72f 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -2114,6 +2114,14 @@ unsafe extern "C" { pub fn rb_get_def_original_id(def: *const rb_method_definition_t) -> ID; pub fn rb_get_def_bmethod_proc(def: *mut rb_method_definition_t) -> VALUE; pub fn rb_jit_get_proc_ptr(procv: VALUE) -> *mut rb_proc_t; + pub fn rb_optimized_call( + recv: *mut VALUE, + ec: *mut rb_execution_context_t, + argc: ::std::os::raw::c_int, + argv: *mut VALUE, + kw_splat: ::std::os::raw::c_int, + block_handler: VALUE, + ) -> VALUE; pub fn rb_jit_iseq_builtin_attrs(iseq: *const rb_iseq_t) -> ::std::os::raw::c_uint; pub fn rb_get_mct_argc(mct: *const rb_method_cfunc_t) -> ::std::os::raw::c_int; pub fn rb_get_mct_func(mct: *const rb_method_cfunc_t) -> *mut ::std::os::raw::c_void; diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 29c34694b7e8c1..e187dd6e64f33e 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -932,6 +932,13 @@ pub enum Insn { state: InsnId, reason: SendFallbackReason, }, + /// Call Proc#call optimized method type. + InvokeProc { + recv: InsnId, + args: Vec, + state: InsnId, + kw_splat: bool, + }, /// Optimized ISEQ call SendWithoutBlockDirect { @@ -1452,6 +1459,16 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { write!(f, " # SendFallbackReason: {reason}")?; Ok(()) } + Insn::InvokeProc { recv, args, kw_splat, .. } => { + write!(f, "InvokeProc {recv}")?; + for arg in args { + write!(f, ", {arg}")?; + } + if *kw_splat { + write!(f, ", kw_splat")?; + } + Ok(()) + } Insn::InvokeBuiltin { bf, args, leaf, .. } => { let bf_name = unsafe { CStr::from_ptr(bf.name) }.to_str().unwrap(); write!(f, "InvokeBuiltin{} {}", @@ -2228,6 +2245,12 @@ impl Function { state, reason, }, + &InvokeProc { recv, ref args, state, kw_splat } => InvokeProc { + recv: find!(recv), + args: find_vec!(args), + state: find!(state), + kw_splat, + }, &InvokeBuiltin { bf, recv, ref args, state, leaf, return_type } => InvokeBuiltin { bf, recv: find!(recv), args: find_vec!(args), state, leaf, return_type }, &ArrayDup { val, state } => ArrayDup { val: find!(val), state }, &HashDup { val, state } => HashDup { val: find!(val), state }, @@ -2416,6 +2439,7 @@ impl Function { Insn::SendForward { .. } => types::BasicObject, Insn::InvokeSuper { .. } => types::BasicObject, Insn::InvokeBlock { .. } => types::BasicObject, + Insn::InvokeProc { .. } => types::BasicObject, Insn::InvokeBuiltin { return_type, .. } => return_type.unwrap_or(types::BasicObject), Insn::Defined { pushval, .. } => Type::from_value(*pushval).union(types::NilClass), Insn::DefinedIvar { pushval, .. } => Type::from_value(*pushval).union(types::NilClass), @@ -2828,14 +2852,7 @@ impl Function { }; let ci = unsafe { get_call_data_ci(cd) }; // info about the call site - // If the call site info indicates that the `Function` has overly complex arguments, then - // do not optimize into a `SendWithoutBlockDirect`. let flags = unsafe { rb_vm_ci_flag(ci) }; - if unspecializable_call_type(flags) { - self.count_complex_call_features(block, flags); - self.set_dynamic_send_reason(insn_id, ComplexArgPass); - self.push_insn_id(block, insn_id); continue; - } let mid = unsafe { vm_ci_mid(ci) }; // Do method lookup @@ -2863,6 +2880,14 @@ impl Function { def_type = unsafe { get_cme_def_type(cme) }; } + // If the call site info indicates that the `Function` has overly complex arguments, then do not optimize into a `SendWithoutBlockDirect`. + // Optimized methods(`VM_METHOD_TYPE_OPTIMIZED`) handle their own argument constraints (e.g., kw_splat for Proc call). + if def_type != VM_METHOD_TYPE_OPTIMIZED && unspecializable_call_type(flags) { + self.count_complex_call_features(block, flags); + self.set_dynamic_send_reason(insn_id, ComplexArgPass); + self.push_insn_id(block, insn_id); continue; + } + if def_type == VM_METHOD_TYPE_ISEQ { // TODO(max): Allow non-iseq; cache cme // Only specialize positional-positional calls @@ -2993,7 +3018,31 @@ impl Function { } else if def_type == VM_METHOD_TYPE_OPTIMIZED { let opt_type: OptimizedMethodType = unsafe { get_cme_def_body_optimized_type(cme) }.into(); match (opt_type, args.as_slice()) { + (OptimizedMethodType::Call, _) => { + if flags & (VM_CALL_ARGS_SPLAT | VM_CALL_KWARG) != 0 { + self.count_complex_call_features(block, flags); + self.set_dynamic_send_reason(insn_id, ComplexArgPass); + self.push_insn_id(block, insn_id); continue; + } + // Check singleton class assumption first, before emitting other patchpoints + if !self.assume_no_singleton_classes(block, klass, state) { + self.set_dynamic_send_reason(insn_id, SingletonClassSeen); + self.push_insn_id(block, insn_id); continue; + } + self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state }); + if let Some(profiled_type) = profiled_type { + recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state }); + } + let kw_splat = flags & VM_CALL_KW_SPLAT != 0; + let invoke_proc = self.push_insn(block, Insn::InvokeProc { recv, args: args.clone(), state, kw_splat }); + self.make_equal_to(insn_id, invoke_proc); + } (OptimizedMethodType::StructAref, &[]) | (OptimizedMethodType::StructAset, &[_]) => { + if unspecializable_call_type(flags) { + self.count_complex_call_features(block, flags); + self.set_dynamic_send_reason(insn_id, ComplexArgPass); + self.push_insn_id(block, insn_id); continue; + } let index: i32 = unsafe { get_cme_def_body_optimized_index(cme) } .try_into() .unwrap(); @@ -4416,7 +4465,8 @@ impl Function { | &Insn::CCallWithFrame { recv, ref args, state, .. } | &Insn::SendWithoutBlockDirect { recv, ref args, state, .. } | &Insn::InvokeBuiltin { recv, ref args, state, .. } - | &Insn::InvokeSuper { recv, ref args, state, .. } => { + | &Insn::InvokeSuper { recv, ref args, state, .. } + | &Insn::InvokeProc { recv, ref args, state, .. } => { worklist.push_back(recv); worklist.extend(args); worklist.push_back(state); @@ -5041,6 +5091,7 @@ impl Function { | Insn::CCallWithFrame { recv, ref args, .. } | Insn::CCallVariadic { recv, ref args, .. } | Insn::InvokeBuiltin { recv, ref args, .. } + | Insn::InvokeProc { recv, ref args, .. } | Insn::ArrayInclude { target: recv, elements: ref args, .. } => { self.assert_subtype(insn_id, recv, types::BasicObject)?; for &arg in args { diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 08aa4474219032..60b4c2598629fc 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -3817,6 +3817,186 @@ mod hir_opt_tests { "); } + #[test] + fn test_specialize_proc_call() { + eval(" + p = proc { |x| x + 1 } + def test(p) + p.call(1) + end + test p + "); + assert_snapshot!(hir_string("test"), @" + fn test@:4: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal :p, l0, SP@4 + Jump bb2(v1, v2) + bb1(v5:BasicObject, v6:BasicObject): + EntryPoint JIT(0) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:BasicObject): + v14:Fixnum[1] = Const Value(1) + PatchPoint NoSingletonClass(Proc@0x1000) + PatchPoint MethodRedefined(Proc@0x1000, call@0x1008, cme:0x1010) + v23:HeapObject[class_exact:Proc] = GuardType v9, HeapObject[class_exact:Proc] + v24:BasicObject = InvokeProc v23, v14 + CheckInterrupts + Return v24 + "); + } + + #[test] + fn test_specialize_proc_aref() { + eval(" + p = proc { |x| x + 1 } + def test(p) + p[2] + end + test p + "); + assert_snapshot!(hir_string("test"), @" + fn test@:4: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal :p, l0, SP@4 + Jump bb2(v1, v2) + bb1(v5:BasicObject, v6:BasicObject): + EntryPoint JIT(0) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:BasicObject): + v14:Fixnum[2] = Const Value(2) + PatchPoint NoSingletonClass(Proc@0x1000) + PatchPoint MethodRedefined(Proc@0x1000, []@0x1008, cme:0x1010) + v24:HeapObject[class_exact:Proc] = GuardType v9, HeapObject[class_exact:Proc] + v25:BasicObject = InvokeProc v24, v14 + CheckInterrupts + Return v25 + "); + } + + #[test] + fn test_specialize_proc_yield() { + eval(" + p = proc { |x| x + 1 } + def test(p) + p.yield(3) + end + test p + "); + assert_snapshot!(hir_string("test"), @" + fn test@:4: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal :p, l0, SP@4 + Jump bb2(v1, v2) + bb1(v5:BasicObject, v6:BasicObject): + EntryPoint JIT(0) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:BasicObject): + v14:Fixnum[3] = Const Value(3) + PatchPoint NoSingletonClass(Proc@0x1000) + PatchPoint MethodRedefined(Proc@0x1000, yield@0x1008, cme:0x1010) + v23:HeapObject[class_exact:Proc] = GuardType v9, HeapObject[class_exact:Proc] + v24:BasicObject = InvokeProc v23, v14 + CheckInterrupts + Return v24 + "); + } + + #[test] + fn test_specialize_proc_eqq() { + eval(" + p = proc { |x| x > 0 } + def test(p) + p === 1 + end + test p + "); + assert_snapshot!(hir_string("test"), @" + fn test@:4: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal :p, l0, SP@4 + Jump bb2(v1, v2) + bb1(v5:BasicObject, v6:BasicObject): + EntryPoint JIT(0) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:BasicObject): + v14:Fixnum[1] = Const Value(1) + PatchPoint NoSingletonClass(Proc@0x1000) + PatchPoint MethodRedefined(Proc@0x1000, ===@0x1008, cme:0x1010) + v23:HeapObject[class_exact:Proc] = GuardType v9, HeapObject[class_exact:Proc] + v24:BasicObject = InvokeProc v23, v14 + CheckInterrupts + Return v24 + "); + } + + #[test] + fn test_dont_specialize_proc_call_splat() { + eval(" + p = proc { } + def test(p) + empty = [] + p.call(*empty) + end + test p + "); + assert_snapshot!(hir_string("test"), @" + fn test@:4: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal :p, l0, SP@5 + v3:NilClass = Const Value(nil) + Jump bb2(v1, v2, v3) + bb1(v6:BasicObject, v7:BasicObject): + EntryPoint JIT(0) + v8:NilClass = Const Value(nil) + Jump bb2(v6, v7, v8) + bb2(v10:BasicObject, v11:BasicObject, v12:NilClass): + v16:ArrayExact = NewArray + v22:ArrayExact = ToArray v16 + IncrCounter complex_arg_pass_caller_splat + v24:BasicObject = SendWithoutBlock v11, :call, v22 # SendFallbackReason: Complex argument passing + CheckInterrupts + Return v24 + "); + } + + #[test] + fn test_dont_specialize_proc_call_kwarg() { + eval(" + p = proc { |a:| a } + def test(p) + p.call(a: 1) + end + test p + "); + assert_snapshot!(hir_string("test"), @" + fn test@:4: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal :p, l0, SP@4 + Jump bb2(v1, v2) + bb1(v5:BasicObject, v6:BasicObject): + EntryPoint JIT(0) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:BasicObject): + v14:Fixnum[1] = Const Value(1) + IncrCounter complex_arg_pass_caller_kwarg + v16:BasicObject = SendWithoutBlock v9, :call, v14 # SendFallbackReason: Complex argument passing + CheckInterrupts + Return v16 + "); + } + #[test] fn test_dont_specialize_definedivar_with_t_data() { eval(" From 3e13b7d4ef546574dcf376e2bb6b630abb46181b Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Fri, 16 Jan 2026 23:07:00 -0500 Subject: [PATCH 2/4] ZJIT: Fix land race InvokeProc and HIR effects landed without an intermediate rebase so we got a conflict in the form of a type checker error (not handled new opcode in a new function). --- zjit/src/hir.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index e187dd6e64f33e..6c2bd09ad3181b 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -1205,6 +1205,7 @@ impl Insn { Insn::IncrCounter(_) => effects::Any, Insn::IncrCounterPtr { .. } => effects::Any, Insn::CheckInterrupts { .. } => effects::Any, + Insn::InvokeProc { .. } => effects::Any, } } From 8ca2f6489bc2b04902472c544edd69664cef0c4e Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 17 Jan 2026 13:38:55 +0900 Subject: [PATCH 3/4] Revert "Fix rb_interned_str: create strings with BINARY (akak ASCII_8BIT) encoding" This reverts commit 1f3c52dc155fb7fbc42fc8e146924091ba1dfa20. --- encoding.c | 6 +++--- ext/io/console/console.c | 8 ++++---- ext/io/console/extconf.rb | 2 +- gc/default/default.c | 2 +- string.c | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/encoding.c b/encoding.c index e9510fe3c146c5..8bb393b471ed54 100644 --- a/encoding.c +++ b/encoding.c @@ -1367,7 +1367,7 @@ enc_names_i(st_data_t name, st_data_t idx, st_data_t args) VALUE *arg = (VALUE *)args; if ((int)idx == (int)arg[0]) { - VALUE str = rb_enc_interned_str_cstr((char *)name, rb_usascii_encoding()); + VALUE str = rb_interned_str_cstr((char *)name); rb_ary_push(arg[1], str); } return ST_CONTINUE; @@ -1873,7 +1873,7 @@ static int rb_enc_name_list_i(st_data_t name, st_data_t idx, st_data_t arg) { VALUE ary = (VALUE)arg; - VALUE str = rb_enc_interned_str_cstr((char *)name, rb_usascii_encoding()); + VALUE str = rb_interned_str_cstr((char *)name); rb_ary_push(ary, str); return ST_CONTINUE; } @@ -1921,7 +1921,7 @@ rb_enc_aliases_enc_i(st_data_t name, st_data_t orig, st_data_t arg) str = rb_fstring_cstr(rb_enc_name(enc)); rb_ary_store(ary, idx, str); } - key = rb_enc_interned_str_cstr((char *)name, rb_usascii_encoding()); + key = rb_interned_str_cstr((char *)name); rb_hash_aset(aliases, key, str); return ST_CONTINUE; } diff --git a/ext/io/console/console.c b/ext/io/console/console.c index 2b0193bb90b631..7ddaf071a8833a 100644 --- a/ext/io/console/console.c +++ b/ext/io/console/console.c @@ -84,9 +84,9 @@ getattr(int fd, conmode *t) static ID id_getc, id_close; static ID id_gets, id_flush, id_chomp_bang; -#ifndef HAVE_RB_ENC_INTERNED_STR_CSTR +#ifndef HAVE_RB_INTERNED_STR_CSTR # define rb_str_to_interned_str(str) rb_str_freeze(str) -# define rb_enc_interned_str_cstr(str, enc) rb_str_freeze(rb_usascii_str_new_cstr(str)) +# define rb_interned_str_cstr(str) rb_str_freeze(rb_usascii_str_new_cstr(str)) #endif #if defined HAVE_RUBY_FIBER_SCHEDULER_H @@ -1897,7 +1897,7 @@ console_ttyname(VALUE io) size_t size = sizeof(termname); int e; if (ttyname_r(fd, tn, size) == 0) - return rb_enc_interned_str_cstr(tn, rb_usascii_encoding()); + return rb_interned_str_cstr(tn); if ((e = errno) == ERANGE) { VALUE s = rb_str_new(0, size); while (1) { @@ -1921,7 +1921,7 @@ console_ttyname(VALUE io) int e = errno; rb_syserr_fail_str(e, rb_sprintf("ttyname(%d)", fd)); } - return rb_enc_interned_str_cstr(tn, rb_usascii_encoding()); + return rb_interned_str_cstr(tn); } # else # error No ttyname function diff --git a/ext/io/console/extconf.rb b/ext/io/console/extconf.rb index e6254c9e90fe98..dd3d221ae51df3 100644 --- a/ext/io/console/extconf.rb +++ b/ext/io/console/extconf.rb @@ -9,7 +9,7 @@ have_func("rb_syserr_new_str(0, Qnil)") or abort -have_func("rb_enc_interned_str_cstr") +have_func("rb_interned_str_cstr") have_func("rb_io_path", "ruby/io.h") have_func("rb_io_descriptor", "ruby/io.h") have_func("rb_io_get_write_io", "ruby/io.h") diff --git a/gc/default/default.c b/gc/default/default.c index 45c0d3e2552fef..013c0749946e2d 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -9582,7 +9582,7 @@ rb_gc_impl_init(void) VALUE opts; /* \GC build options */ rb_define_const(rb_mGC, "OPTS", opts = rb_ary_new()); -#define OPT(o) if (o) rb_ary_push(opts, rb_enc_interned_str(#o, sizeof(#o) - 1, rb_usascii_encoding())) +#define OPT(o) if (o) rb_ary_push(opts, rb_interned_str(#o, sizeof(#o) - 1)) OPT(GC_DEBUG); OPT(USE_RGENGC); OPT(RGENGC_DEBUG); diff --git a/string.c b/string.c index d564c2e2e1bf94..6f4ea03fb37a41 100644 --- a/string.c +++ b/string.c @@ -12709,7 +12709,7 @@ VALUE rb_interned_str(const char *ptr, long len) { struct RString fake_str = {RBASIC_INIT}; - return register_fstring(setup_fake_str(&fake_str, ptr, len, ENCINDEX_ASCII_8BIT), true, false); + return register_fstring(setup_fake_str(&fake_str, ptr, len, ENCINDEX_US_ASCII), true, false); } VALUE From 78b7646bdb91285873ac26bca060591e06c45afe Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 17 Jan 2026 14:19:15 +0900 Subject: [PATCH 4/4] [Bug #21842] Let `rb_interned_str` return US-ASCII if possible --- include/ruby/internal/intern/string.h | 4 ++-- string.c | 10 +++++++++- test/-ext-/string/test_interned_str.rb | 5 +++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/include/ruby/internal/intern/string.h b/include/ruby/internal/intern/string.h index 75a28143fb8f1e..2ec08fc81f9d13 100644 --- a/include/ruby/internal/intern/string.h +++ b/include/ruby/internal/intern/string.h @@ -444,8 +444,8 @@ VALUE rb_str_to_interned_str(VALUE str); * terminating NUL character. * @exception rb_eArgError `len` is negative. * @return A found or created instance of ::rb_cString, of `len` bytes - * length, of "binary" encoding, whose contents are identical to - * that of `ptr`. + * length, of US-ASCII or "binary" encoding, whose contents are + * identical to that of `ptr`. * @pre At least `len` bytes of continuous memory region shall be * accessible via `ptr`. */ diff --git a/string.c b/string.c index 6f4ea03fb37a41..2d74c46a360aa8 100644 --- a/string.c +++ b/string.c @@ -12709,7 +12709,15 @@ VALUE rb_interned_str(const char *ptr, long len) { struct RString fake_str = {RBASIC_INIT}; - return register_fstring(setup_fake_str(&fake_str, ptr, len, ENCINDEX_US_ASCII), true, false); + int encidx = ENCINDEX_US_ASCII; + int coderange = ENC_CODERANGE_7BIT; + if (len > 0 && search_nonascii(ptr, ptr + len)) { + encidx = ENCINDEX_ASCII_8BIT; + coderange = ENC_CODERANGE_VALID; + } + VALUE str = setup_fake_str(&fake_str, ptr, len, encidx); + ENC_CODERANGE_SET(str, coderange); + return register_fstring(str, true, false); } VALUE diff --git a/test/-ext-/string/test_interned_str.rb b/test/-ext-/string/test_interned_str.rb index 340dba41e8c48b..a81cb59aa5fd34 100644 --- a/test/-ext-/string/test_interned_str.rb +++ b/test/-ext-/string/test_interned_str.rb @@ -9,4 +9,9 @@ def test_interned_str src << "b" * 20 assert_equal "a" * 20, interned_str end + + def test_interned_str_encoding + src = :ascii.name + assert_equal Encoding::US_ASCII, Bug::String.rb_interned_str_dup(src).encoding + end end