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