diff --git a/NEWS.md b/NEWS.md index fb926d4c7474c1..cf19a5393fd219 100644 --- a/NEWS.md +++ b/NEWS.md @@ -110,6 +110,11 @@ Note: We're only listing outstanding class updates. * `String#unpack` and `String#unpack1` accept new formats `x!` and `@!` to align the current offset to a byte boundary or to the ABI alignment of another directive. [[Feature #22185]] + * Basic bit operations are added. `String#bit_get`, `String#bit_set?`, + `String#bit_set`, `String#bit_clear`, `String#bit_flip` and + `String#bit_count` handle individual bits, and `String#bitwise_not`, + `String#bitwise_and`, `String#bitwise_or`, `String#bitwise_xor` + (with their `!` variants) handle whole strings. [[Feature #22118]] * Symbol @@ -315,6 +320,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [Feature #21951]: https://bugs.ruby-lang.org/issues/21951 [Feature #21981]: https://bugs.ruby-lang.org/issues/21981 [Feature #22097]: https://bugs.ruby-lang.org/issues/22097 +[Feature #22118]: https://bugs.ruby-lang.org/issues/22118 [Feature #22135]: https://bugs.ruby-lang.org/issues/22135 [Feature #22137]: https://bugs.ruby-lang.org/issues/22137 [Feature #22139]: https://bugs.ruby-lang.org/issues/22139 diff --git a/jit.c b/jit.c index 086c207a81a332..88652dddd93dbe 100644 --- a/jit.c +++ b/jit.c @@ -829,6 +829,12 @@ rb_yarv_str_eql_internal(VALUE str1, VALUE str2) return rb_str_eql_internal(str1, str2); } +VALUE +rb_jit_str_simple_append(VALUE str1, VALUE str2) +{ + return rb_str_cat(str1, RSTRING_PTR(str2), RSTRING_LEN(str2)); +} + void rb_jit_str_concat_codepoint(VALUE str, VALUE codepoint); attr_index_t diff --git a/yjit.c b/yjit.c index 7999c10d302c69..01e14fb772628c 100644 --- a/yjit.c +++ b/yjit.c @@ -248,12 +248,6 @@ rb_yjit_builtin_function(const rb_iseq_t *iseq) } } -VALUE -rb_yjit_str_simple_append(VALUE str1, VALUE str2) -{ - return rb_str_cat(str1, RSTRING_PTR(str2), RSTRING_LEN(str2)); -} - extern VALUE *rb_vm_base_ptr(struct rb_control_frame_struct *cfp); VALUE diff --git a/yjit/bindgen/src/main.rs b/yjit/bindgen/src/main.rs index 4352625a57c896..36f25be53edc0e 100644 --- a/yjit/bindgen/src/main.rs +++ b/yjit/bindgen/src/main.rs @@ -262,7 +262,7 @@ fn main() { .allowlist_function("rb_iseq_reset_jit_func") .allowlist_function("rb_yjit_dump_iseq_loc") .allowlist_function("rb_yjit_obj_written") - .allowlist_function("rb_yjit_str_simple_append") + .allowlist_function("rb_jit_str_simple_append") .allowlist_function("rb_RSTRING_PTR") .allowlist_function("rb_RSTRING_LEN") .allowlist_function("rb_ENCODING_GET") diff --git a/yjit/src/codegen.rs b/yjit/src/codegen.rs index c8178e12842cbb..42d16b4b7d619b 100644 --- a/yjit/src/codegen.rs +++ b/yjit/src/codegen.rs @@ -6350,7 +6350,7 @@ fn jit_rb_str_concat( let recv = asm.stack_pop(1); // Test if string encodings differ. If different, use rb_str_append. If the same, - // use rb_yjit_str_simple_append, which calls rb_str_cat. + // use rb_jit_str_simple_append, which calls rb_str_cat. asm_comment!(asm, "<< on strings"); // Take receiver's object flags XOR arg's flags. If any @@ -6368,7 +6368,7 @@ fn jit_rb_str_concat( asm.jnz(enc_mismatch); // If encodings match, call the simple append function and jump to return - let ret_opnd = asm.ccall(rb_yjit_str_simple_append as *const u8, vec![recv, concat_arg]); + let ret_opnd = asm.ccall(rb_jit_str_simple_append as *const u8, vec![recv, concat_arg]); let ret_label = asm.new_label("func_return"); let stack_ret = asm.stack_push(Type::TString); asm.mov(stack_ret, ret_opnd); diff --git a/yjit/src/cruby_bindings.inc.rs b/yjit/src/cruby_bindings.inc.rs index 72152ae278e3c7..659c0af223bfb7 100644 --- a/yjit/src/cruby_bindings.inc.rs +++ b/yjit/src/cruby_bindings.inc.rs @@ -1192,7 +1192,6 @@ extern "C" { 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_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; pub fn rb_str_neq_internal(str1: VALUE, str2: VALUE) -> VALUE; pub fn rb_ary_unshift_m(argc: ::std::os::raw::c_int, argv: *mut VALUE, ary: VALUE) -> VALUE; @@ -1352,5 +1351,6 @@ extern "C" { pub fn rb_jit_fix_mod_fix(recv: VALUE, obj: VALUE) -> VALUE; pub fn rb_jit_fix_div_fix(recv: VALUE, obj: VALUE) -> VALUE; pub fn rb_yarv_str_eql_internal(str1: VALUE, str2: VALUE) -> VALUE; + pub fn rb_jit_str_simple_append(str1: VALUE, str2: VALUE) -> VALUE; pub fn rb_jit_str_concat_codepoint(str_: VALUE, codepoint: VALUE); } diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index 335c110baf613b..bcaff445c9b594 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -79,6 +79,7 @@ fn main() { .allowlist_type("ruby_special_consts") .allowlist_function("rb_utf8_str_new") .allowlist_function("rb_str_buf_append") + .allowlist_function("rb_jit_str_simple_append") .allowlist_function("rb_str_dup") .allowlist_function("rb_str_getbyte") .allowlist_type("ruby_preserved_encindex") diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 801d4e4b272da1..33816ef398c0f0 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -4079,7 +4079,49 @@ fn gen_string_setbyte_fixnum(asm: &mut Assembler, string: Opnd, index: Opnd, val fn gen_string_append(jit: &mut JITState, asm: &mut Assembler, function: &Function, string: Opnd, val: Opnd, state: &FrameState) -> Opnd { gen_prepare_non_leaf_call(jit, asm, function, state); - asm_ccall!(asm, rb_str_buf_append, string, val) + + // Test if string encodings differ. If different, use rb_str_buf_append. If the same, + // use rb_jit_str_simple_append, which calls rb_str_cat. + asm_comment!(asm, "<< on strings"); + + // Take receiver's object flags XOR arg's flags. If any + // string-encoding flags are different between the two, + // the encodings don't match. + let string_reg = asm.load_mem(string); + let val_reg = asm.load_mem(val); + let flags_xor = asm.xor( + Opnd::mem(VALUE_BITS, string_reg, RUBY_OFFSET_RBASIC_FLAGS), + Opnd::mem(VALUE_BITS, val_reg, RUBY_OFFSET_RBASIC_FLAGS) + ); + asm.test(flags_xor, Opnd::UImm(RUBY_ENCODING_MASK as u64)); + + let hir_block_id = asm.current_block().hir_block_id; + let rpo_idx = asm.current_block().rpo_index; + let mismatch_block = asm.new_block(hir_block_id, false, rpo_idx); + let mismatch_edge = Target::Block(Box::new(lir::BranchEdge { target: mismatch_block, args: vec![] })); + let result_block = asm.new_block(hir_block_id, false, rpo_idx); + let result_edge = Target::Block(Box::new(lir::BranchEdge { target: result_block, args: vec![] })); + + asm.jnz(jit, mismatch_edge); + + // If encodings match, call the simple append function + asm_ccall!(asm, rb_jit_str_simple_append, string, val); + asm.jmp(result_edge.clone()); + + // If encodings are different, use a slower encoding-aware concatenate + asm.set_current_block(mismatch_block); + let label = jit.get_label(asm, mismatch_block, hir_block_id); + asm.write_label(label); + asm_ccall!(asm, rb_str_buf_append, string, val); + asm.jmp(result_edge); + + // Join block + asm.set_current_block(result_block); + let label = jit.get_label(asm, result_block, hir_block_id); + asm.write_label(label); + + // Either append function returns the receiver + string } fn gen_string_append_codepoint(jit: &mut JITState, asm: &mut Assembler, function: &Function, string: Opnd, val: Opnd, state: &FrameState) -> Opnd { diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index 58a1cfb94086a1..33f82548288901 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -3581,6 +3581,67 @@ fn test_string_copy_chilled_gc_stress() { "#), @r#"[String, "hello world!", false, "UTF-8", 12, "hello world"]"#); } +#[test] +fn test_string_append_same_encoding() { + eval(r#" + def test(s, x) = s << x + "#); + assert_contains_opcode("test", YARVINSN_opt_ltlt); + assert_snapshot!(assert_compiles(r#" + s = +"abc" + test(s, "déf") + test(s, "ghé") + [s, s.encoding.name, s.valid_encoding?] + "#), @r#"["abcdéfghé", "UTF-8", true]"#); +} + +#[test] +fn test_string_append_encoding_mismatch() { + eval(r#" + def test(s, x) = s << x + "#); + assert_contains_opcode("test", YARVINSN_opt_ltlt); + // The first append takes the mismatched-encoding path and switches the + // empty BINARY receiver to UTF-8; later appends take the fast path. + assert_snapshot!(assert_compiles(r#" + s = String.new(encoding: Encoding::BINARY) + test(s, "é") + test(s, "é") + [s, s.encoding.name, s.valid_encoding?] + "#), @r#"["éé", "UTF-8", true]"#); +} + +#[test] +fn test_string_append_incompatible_encoding() { + eval(r#" + def test(s, x) = s << x + "#); + assert_contains_opcode("test", YARVINSN_opt_ltlt); + assert_snapshot!(assert_compiles(r#" + s = "\xFF".b + begin + test(s, "é") + :no_error + rescue Encoding::CompatibilityError + :compatibility_error + end + "#), @":compatibility_error"); +} + +#[test] +fn test_string_append_broken_coderange() { + eval(r#" + def test(s, x) = s << x + "#); + assert_contains_opcode("test", YARVINSN_opt_ltlt); + // Same encoding, but the appended bytes break the receiver's coderange. + assert_snapshot!(assert_compiles(r#" + s = +"abc" + test(s, "\xFF".dup.force_encoding(Encoding::UTF_8)) + [s.bytesize, s.valid_encoding?] + "#), @"[4, false]"); +} + #[test] fn test_new_hash_nonempty() { eval(r#" diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index da77593f7b9619..5bce8574e81e87 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -2457,6 +2457,7 @@ unsafe extern "C" { ); pub fn rb_jit_fix_div_fix(recv: VALUE, obj: VALUE) -> VALUE; pub fn rb_yarv_str_eql_internal(str1: VALUE, str2: VALUE) -> VALUE; + pub fn rb_jit_str_simple_append(str1: VALUE, str2: VALUE) -> VALUE; pub fn rb_jit_str_concat_codepoint(str_: VALUE, codepoint: VALUE); pub fn rb_jit_shape_capacity(shape_id: shape_id_t) -> attr_index_t; }