From 5bf7ef2ec9ac864a05d0d743ed48450326432dd9 Mon Sep 17 00:00:00 2001 From: fffonion Date: Thu, 16 Jul 2026 18:04:20 +0800 Subject: [PATCH 1/7] feat(jit): specialize regex and dynamic string operations - lower regex match and replace through SSA native helpers - keep literal string helpers inside hot traces - preserve tagged equality for dynamic string comparisons - cover native specialization and move semantics --- src/builtins/runtime/regex.rs | 13 +++ src/vm/jit/ir.rs | 29 ++++++ src/vm/jit/native/lower.rs | 154 ++++++++++++++++++++++++++-- src/vm/jit/recorder.rs | 184 +++++++++++++++++++++++++++++----- src/vm/native/bridge.rs | 61 +++++++++++ src/vm/native/codegen.rs | 27 +++++ src/vm/native/mod.rs | 20 ++-- tests/jit/jit_tests.rs | 107 +++++++++++++++++--- 8 files changed, 539 insertions(+), 56 deletions(-) diff --git a/src/builtins/runtime/regex.rs b/src/builtins/runtime/regex.rs index a3885ddc..b335cf53 100644 --- a/src/builtins/runtime/regex.rs +++ b/src/builtins/runtime/regex.rs @@ -107,6 +107,10 @@ pub(super) fn builtin_re_match(vm: &mut Vm, pattern: &str, text: &str) -> VmResu Ok(regex.is_match(text)) } +pub(crate) fn native_re_match(vm: &mut Vm, pattern: &str, text: &str) -> VmResult { + builtin_re_match_impl(vm, pattern, text) +} + /// Returns the first substring matched by a regular expression. #[pd_host_function(name = "re::find")] pub(super) fn builtin_re_find(vm: &mut Vm, pattern: &str, text: &str) -> VmResult> { @@ -126,6 +130,15 @@ pub(super) fn builtin_re_replace( Ok(regex.replace_all(text, replacement).into_owned()) } +pub(crate) fn native_re_replace( + vm: &mut Vm, + pattern: &str, + text: &str, + replacement: &str, +) -> VmResult { + builtin_re_replace_impl(vm, pattern, text, replacement) +} + /// Splits a string on regular-expression matches. #[pd_host_function(name = "re::split")] pub(super) fn builtin_re_split(vm: &mut Vm, pattern: &str, text: &str) -> VmResult { diff --git a/src/vm/jit/ir.rs b/src/vm/jit/ir.rs index 5fb78eae..cc80b6c2 100644 --- a/src/vm/jit/ir.rs +++ b/src/vm/jit/ir.rs @@ -139,6 +139,15 @@ pub(crate) enum SsaInstKind { text: SsaValueId, needle: SsaValueId, }, + RegexMatch { + pattern: SsaValueId, + text: SsaValueId, + }, + RegexReplace { + pattern: SsaValueId, + text: SsaValueId, + replacement: SsaValueId, + }, StringReplaceLiteral { text: SsaValueId, needle: SsaValueId, @@ -331,6 +340,10 @@ pub(crate) enum SsaInstKind { lhs: SsaValueId, rhs: SsaValueId, }, + ValueCmpEq { + lhs: SsaValueId, + rhs: SsaValueId, + }, IntCmpLt { lhs: SsaValueId, rhs: SsaValueId, @@ -379,6 +392,12 @@ impl SsaInstKind { Self::BytesGet { bytes, index } => vec![*bytes, *index], Self::BytesHas { bytes, index } => vec![*bytes, *index], Self::StringContains { text, needle } => vec![*text, *needle], + Self::RegexMatch { pattern, text } => vec![*pattern, *text], + Self::RegexReplace { + pattern, + text, + replacement, + } => vec![*pattern, *text, *replacement], Self::StringReplaceLiteral { text, needle, @@ -422,6 +441,7 @@ impl SsaInstKind { | Self::FloatCmpLt { lhs, rhs } | Self::FloatCmpGt { lhs, rhs } | Self::IntCmpEq { lhs, rhs } + | Self::ValueCmpEq { lhs, rhs } | Self::IntCmpLt { lhs, rhs } | Self::IntCmpGt { lhs, rhs } => vec![*lhs, *rhs], Self::IntAddImm { lhs, .. } @@ -993,6 +1013,14 @@ fn render_inst_kind(kind: &SsaInstKind) -> String { SsaInstKind::StringContains { text, needle } => { format!("string_contains {text}, {needle}") } + SsaInstKind::RegexMatch { pattern, text } => { + format!("regex_match {pattern}, {text}") + } + SsaInstKind::RegexReplace { + pattern, + text, + replacement, + } => format!("regex_replace {pattern}, {text}, {replacement}"), SsaInstKind::StringReplaceLiteral { text, needle, @@ -1057,6 +1085,7 @@ fn render_inst_kind(kind: &SsaInstKind) -> String { SsaInstKind::FloatCmpLt { lhs, rhs } => format!("fcmp_lt {lhs}, {rhs}"), SsaInstKind::FloatCmpGt { lhs, rhs } => format!("fcmp_gt {lhs}, {rhs}"), SsaInstKind::IntCmpEq { lhs, rhs } => format!("icmp_eq {lhs}, {rhs}"), + SsaInstKind::ValueCmpEq { lhs, rhs } => format!("value_eq {lhs}, {rhs}"), SsaInstKind::IntCmpLt { lhs, rhs } => format!("icmp_lt {lhs}, {rhs}"), SsaInstKind::IntCmpLtImm { lhs, imm } => format!("icmp_lt_imm {lhs}, {imm}"), SsaInstKind::IntCmpGt { lhs, rhs } => format!("icmp_gt {lhs}, {rhs}"), diff --git a/src/vm/jit/native/lower.rs b/src/vm/jit/native/lower.rs index fc09e844..f6d9a02a 100644 --- a/src/vm/jit/native/lower.rs +++ b/src/vm/jit/native/lower.rs @@ -20,12 +20,14 @@ use crate::vm::native::{ map_iter_next_signature, map_iter_take_key_entry_address, map_iter_take_signature, map_iter_take_value_entry_address, map_set_entry_address, map_set_signature, non_yielding_host_call_entry_address, non_yielding_host_call_signature, pack_shared_signature, - restore_sparse_exit_state_entry_address, shared_array_from_buffer_entry_address, - shared_bytes_from_buffer_entry_address, shared_string_from_buffer_entry_address, - sparse_restore_exit_signature, string_binary_transform_signature, - string_contains_entry_address, string_contains_signature, string_lower_ascii_entry_address, - string_replace_literal_entry_address, string_replace_signature, - string_split_literal_entry_address, string_unary_transform_signature, value_slot_signature, + regex_match_entry_address, regex_match_signature, regex_replace_entry_address, + regex_replace_signature, restore_sparse_exit_state_entry_address, + shared_array_from_buffer_entry_address, shared_bytes_from_buffer_entry_address, + shared_string_from_buffer_entry_address, sparse_restore_exit_signature, + string_binary_transform_signature, string_contains_entry_address, string_contains_signature, + string_lower_ascii_entry_address, string_replace_literal_entry_address, + string_replace_signature, string_split_literal_entry_address, string_unary_transform_signature, + value_eq_entry_address, value_eq_signature, value_slot_signature, write_heap_value_to_slot_entry_address, zero_bytes_entry_address, }; use cranelift_codegen::ir::condcodes::{FloatCC, IntCC}; @@ -100,6 +102,7 @@ fn try_compile_ssa_trace( let clone_value_sig = clone_value_signature(pointer_type, call_conv); let non_yielding_host_call_sig = non_yielding_host_call_signature(pointer_type, call_conv); let value_slot_sig = value_slot_signature(pointer_type, call_conv); + let value_eq_sig = value_eq_signature(pointer_type, call_conv); let box_heap_value_sig = box_heap_value_signature(pointer_type, call_conv); let alloc_buffer_sig = alloc_buffer_signature(pointer_type, call_conv); let free_buffer_sig = free_buffer_signature(pointer_type, call_conv); @@ -115,6 +118,8 @@ fn try_compile_ssa_trace( let sparse_restore_exit_sig = sparse_restore_exit_signature(pointer_type, call_conv); let resume_linked_trace_sig = entry_signature(pointer_type, call_conv); let string_contains_sig = string_contains_signature(pointer_type, call_conv); + let regex_match_sig = regex_match_signature(pointer_type, call_conv); + let regex_replace_sig = regex_replace_signature(pointer_type, call_conv); let string_lower_sig = string_unary_transform_signature(pointer_type, call_conv); let string_replace_sig = string_replace_signature(pointer_type, call_conv); let string_split_sig = string_binary_transform_signature(pointer_type, call_conv); @@ -150,18 +155,23 @@ fn try_compile_ssa_trace( }; let string_refs = SsaStringHelperRefs { contains_ref: b.import_signature(string_contains_sig), + regex_match_ref: b.import_signature(regex_match_sig), + regex_replace_ref: b.import_signature(regex_replace_sig), replace_ref: b.import_signature(string_replace_sig), lower_ascii_ref: b.import_signature(string_lower_sig), split_literal_ref: b.import_signature(string_split_sig), }; let string_addrs = SsaStringHelperAddrs { contains: string_contains_entry_address(), + regex_match: regex_match_entry_address(), + regex_replace: regex_replace_entry_address(), replace_literal: string_replace_literal_entry_address(), lower_ascii: string_lower_ascii_entry_address(), split_literal: string_split_literal_entry_address(), }; let deopt_refs = SsaDeoptHelperRefs { clone_value_ref: b.import_signature(clone_value_sig), + value_eq_ref: b.import_signature(value_eq_sig), non_yielding_host_call_ref: b.import_signature(non_yielding_host_call_sig), clear_value_slot_ref: b.import_signature(value_slot_sig), box_heap_value_ref: b.import_signature(box_heap_value_sig), @@ -178,6 +188,7 @@ fn try_compile_ssa_trace( }; let deopt_addrs = SsaDeoptHelperAddrs { clone_value: clone_value_to_slot_entry_address(), + value_eq: value_eq_entry_address(), non_yielding_host_call: non_yielding_host_call_entry_address(), clear_value_slot: clear_value_slot_entry_address(), box_heap_value: write_heap_value_to_slot_entry_address(), @@ -425,6 +436,7 @@ struct SsaExitLowering { #[derive(Clone, Copy)] struct SsaDeoptHelperRefs { clone_value_ref: cranelift_codegen::ir::SigRef, + value_eq_ref: cranelift_codegen::ir::SigRef, non_yielding_host_call_ref: cranelift_codegen::ir::SigRef, clear_value_slot_ref: cranelift_codegen::ir::SigRef, box_heap_value_ref: cranelift_codegen::ir::SigRef, @@ -443,6 +455,7 @@ struct SsaDeoptHelperRefs { #[derive(Clone, Copy)] struct SsaDeoptHelperAddrs { clone_value: usize, + value_eq: usize, non_yielding_host_call: usize, clear_value_slot: usize, box_heap_value: usize, @@ -461,6 +474,8 @@ struct SsaDeoptHelperAddrs { #[derive(Clone, Copy)] struct SsaStringHelperRefs { contains_ref: cranelift_codegen::ir::SigRef, + regex_match_ref: cranelift_codegen::ir::SigRef, + regex_replace_ref: cranelift_codegen::ir::SigRef, replace_ref: cranelift_codegen::ir::SigRef, lower_ascii_ref: cranelift_codegen::ir::SigRef, split_literal_ref: cranelift_codegen::ir::SigRef, @@ -469,6 +484,8 @@ struct SsaStringHelperRefs { #[derive(Clone, Copy)] struct SsaStringHelperAddrs { contains: usize, + regex_match: usize, + regex_replace: usize, replace_literal: usize, lower_ascii: usize, split_literal: usize, @@ -555,6 +572,8 @@ fn ssa_trace_supported(ssa: &SsaTrace) -> bool { | SsaInstKind::BytesGet { .. } | SsaInstKind::BytesHas { .. } | SsaInstKind::StringContains { .. } + | SsaInstKind::RegexMatch { .. } + | SsaInstKind::RegexReplace { .. } | SsaInstKind::StringReplaceLiteral { .. } | SsaInstKind::StringLowerAscii { .. } | SsaInstKind::StringSplitLiteral { .. } @@ -604,6 +623,7 @@ fn ssa_trace_supported(ssa: &SsaTrace) -> bool { | SsaInstKind::FloatCmpLt { .. } | SsaInstKind::FloatCmpGt { .. } | SsaInstKind::IntCmpEq { .. } + | SsaInstKind::ValueCmpEq { .. } | SsaInstKind::IntCmpLt { .. } | SsaInstKind::IntCmpLtImm { .. } | SsaInstKind::IntCmpGt { .. } @@ -834,6 +854,7 @@ fn ssa_inst_requires_owned_value_slot(kind: &SsaInstKind) -> bool { | SsaInstKind::StringSlice { .. } | SsaInstKind::BytesSlice { .. } | SsaInstKind::StringGet { .. } + | SsaInstKind::RegexReplace { .. } | SsaInstKind::StringReplaceLiteral { .. } | SsaInstKind::StringLowerAscii { .. } | SsaInstKind::StringSplitLiteral { .. } @@ -1702,6 +1723,64 @@ fn lower_ssa_inst( ssa_call_string_contains(b, pointer_type, string_refs, string_addrs, text, needle)?; b.ins().icmp_imm(IntCC::NotEqual, raw, 0) } + SsaInstKind::RegexMatch { pattern, text } => { + let pattern = values[pattern]; + let text = values[text]; + let raw = ssa_call_regex_match( + b, + pointer_type, + string_refs, + string_addrs, + vm_ptr, + pattern, + text, + )?; + let error = b.ins().icmp_imm(IntCC::SignedLessThan, raw, 0); + let failed = b.create_block(); + let matched = b.create_block(); + b.ins().brif(error, failed, &[], matched, &[]); + b.switch_to_block(failed); + let status = b.ins().iconst(types::I32, STATUS_ERROR as i64); + jump_with_status(b, exit_block, status); + b.switch_to_block(matched); + b.ins().icmp_imm(IntCC::NotEqual, raw, 0) + } + SsaInstKind::RegexReplace { + pattern, + text, + replacement, + } => { + let pattern = values[pattern]; + let text = values[text]; + let replacement = values[replacement]; + let out_raw = ssa_call_regex_replace( + b, + pointer_type, + string_refs, + string_addrs, + vm_ptr, + pattern, + text, + replacement, + )?; + let error = b.ins().icmp_imm(IntCC::Equal, out_raw, 0); + let failed = b.create_block(); + let replaced = b.create_block(); + b.ins().brif(error, failed, &[], replaced, &[]); + b.switch_to_block(failed); + let status = b.ins().iconst(types::I32, STATUS_ERROR as i64); + jump_with_status(b, exit_block, status); + b.switch_to_block(replaced); + let out = owned_value_temp_slot_addr( + b, + pointer_type, + owned_value_temps, + SsaTempValueSlotKey::Output(output.id), + )?; + clear_owned_value_temp_slot(b, pointer_type, helper_refs, helper_addrs, out)?; + ssa_store_heap_ptr_in_value(b, layout.value, out, layout.value.string_tag, out_raw); + out + } SsaInstKind::StringReplaceLiteral { text, needle, @@ -2688,6 +2767,17 @@ fn lower_ssa_inst( b.ins().fcmp(FloatCC::GreaterThan, values[lhs], values[rhs]) } SsaInstKind::IntCmpEq { lhs, rhs } => b.ins().icmp(IntCC::Equal, values[lhs], values[rhs]), + SsaInstKind::ValueCmpEq { lhs, rhs } => { + let raw = ssa_call_value_eq( + b, + pointer_type, + helper_refs, + helper_addrs, + values[lhs], + values[rhs], + )?; + b.ins().icmp_imm(IntCC::NotEqual, raw, 0) + } SsaInstKind::IntCmpLt { lhs, rhs } => { b.ins() .icmp(IntCC::SignedLessThan, values[lhs], values[rhs]) @@ -3217,6 +3307,21 @@ fn ssa_value_buffer_slot_addr( Ok(ssa_value_addr(b, pointer_type, base_ptr, index, value_size)) } +fn ssa_call_value_eq( + b: &mut FunctionBuilder, + pointer_type: cranelift_codegen::ir::Type, + helper_refs: SsaDeoptHelperRefs, + helper_addrs: SsaDeoptHelperAddrs, + lhs: cranelift_codegen::ir::Value, + rhs: cranelift_codegen::ir::Value, +) -> VmResult { + let helper_ptr = iconst_ptr_from_addr(b, pointer_type, helper_addrs.value_eq)?; + let call = b + .ins() + .call_indirect(helper_refs.value_eq_ref, helper_ptr, &[lhs, rhs]); + Ok(b.inst_results(call)[0]) +} + fn ssa_call_status_helper( b: &mut FunctionBuilder, exit_block: Block, @@ -3753,6 +3858,43 @@ fn ssa_call_string_contains( Ok(b.inst_results(call)[0]) } +fn ssa_call_regex_match( + b: &mut FunctionBuilder, + pointer_type: cranelift_codegen::ir::Type, + string_refs: SsaStringHelperRefs, + string_addrs: SsaStringHelperAddrs, + vm_ptr: cranelift_codegen::ir::Value, + pattern: cranelift_codegen::ir::Value, + text: cranelift_codegen::ir::Value, +) -> VmResult { + let helper_ptr = iconst_ptr_from_addr(b, pointer_type, string_addrs.regex_match)?; + let call = b.ins().call_indirect( + string_refs.regex_match_ref, + helper_ptr, + &[vm_ptr, pattern, text], + ); + Ok(b.inst_results(call)[0]) +} + +fn ssa_call_regex_replace( + b: &mut FunctionBuilder, + pointer_type: cranelift_codegen::ir::Type, + string_refs: SsaStringHelperRefs, + string_addrs: SsaStringHelperAddrs, + vm_ptr: cranelift_codegen::ir::Value, + pattern: cranelift_codegen::ir::Value, + text: cranelift_codegen::ir::Value, + replacement: cranelift_codegen::ir::Value, +) -> VmResult { + let helper_ptr = iconst_ptr_from_addr(b, pointer_type, string_addrs.regex_replace)?; + let call = b.ins().call_indirect( + string_refs.regex_replace_ref, + helper_ptr, + &[vm_ptr, pattern, text, replacement], + ); + Ok(b.inst_results(call)[0]) +} + fn ssa_call_string_replace_literal( b: &mut FunctionBuilder, pointer_type: cranelift_codegen::ir::Type, diff --git a/src/vm/jit/recorder.rs b/src/vm/jit/recorder.rs index ac8b424b..0335dee5 100644 --- a/src/vm/jit/recorder.rs +++ b/src/vm/jit/recorder.rs @@ -465,6 +465,8 @@ enum SpecializedBuiltinKind { BytesGet, BytesHas, StringContains, + RegexMatch, + RegexReplace, StringReplaceLiteral, StringLowerAscii, StringSplitLiteral, @@ -915,19 +917,27 @@ pub(crate) fn record_trace( DecodedOp::Compare { ip, opcode } => { let rhs = frame.pop()?; let lhs = frame.pop()?; - let (name, out) = - match select_numeric_compare(program, ip, opcode, lhs.info, rhs.info)? { - NumericCompareKind::Int(kind) => { - let lhs = ensure_int(&mut builder, current_block, ip, lhs)?; - let rhs = ensure_int(&mut builder, current_block, ip, rhs)?; - emit_int_compare(&mut builder, current_block, ip, kind, lhs, rhs)? - } - NumericCompareKind::Float(kind) => { - let lhs = ensure_float(&mut builder, current_block, ip, lhs)?; - let rhs = ensure_float(&mut builder, current_block, ip, rhs)?; - emit_float_compare(&mut builder, current_block, ip, kind, lhs, rhs)? - } - }; + let numeric = select_numeric_compare(program, ip, opcode, lhs.info, rhs.info); + let (name, out) = match numeric { + Ok(NumericCompareKind::Int(kind)) => { + let lhs = ensure_int(&mut builder, current_block, ip, lhs)?; + let rhs = ensure_int(&mut builder, current_block, ip, rhs)?; + emit_int_compare(&mut builder, current_block, ip, kind, lhs, rhs)? + } + Ok(NumericCompareKind::Float(kind)) => { + let lhs = ensure_float(&mut builder, current_block, ip, lhs)?; + let rhs = ensure_float(&mut builder, current_block, ip, rhs)?; + emit_float_compare(&mut builder, current_block, ip, kind, lhs, rhs)? + } + Err(_) + if opcode == OpCode::Ceq as u8 + && lhs.info.repr == SsaValueRepr::Tagged + && rhs.info.repr == SsaValueRepr::Tagged => + { + emit_value_eq(&mut builder, current_block, ip, lhs, rhs)? + } + Err(err) => return Err(err), + }; op_names.push(name.to_string()); frame.push(out); } @@ -1323,19 +1333,27 @@ fn infer_loop_header_plan( DecodedOp::Compare { ip, opcode } => { let rhs = frame.pop()?; let lhs = frame.pop()?; - match select_numeric_compare(program, ip, opcode, lhs, rhs)? { - NumericCompareKind::Int(kind) => { + match select_numeric_compare(program, ip, opcode, lhs, rhs) { + Ok(NumericCompareKind::Int(kind)) => { let lhs = expect_int_info(lhs)?; let rhs = expect_int_info(rhs)?; validate_int_compare_operands(program, ip, kind, lhs, rhs)?; frame.push(result_info_for_int_compare(kind, lhs, rhs)); } - NumericCompareKind::Float(kind) => { + Ok(NumericCompareKind::Float(kind)) => { let lhs = expect_float_info(lhs)?; let rhs = expect_float_info(rhs)?; validate_float_compare_operands(program, ip, kind, lhs, rhs)?; frame.push(result_info_for_float_compare(kind, lhs, rhs)); } + Err(_) + if opcode == OpCode::Ceq as u8 + && lhs.repr == SsaValueRepr::Tagged + && rhs.repr == SsaValueRepr::Tagged => + { + frame.push(ValueInfo::bool(None)); + } + Err(err) => return Err(err), } } DecodedOp::Call { @@ -2294,6 +2312,33 @@ fn emit_bool_not( )) } +fn emit_value_eq( + builder: &mut SsaTraceBuilder, + block: super::ir::SsaBlockId, + ip: usize, + lhs: SymbolicValue, + rhs: SymbolicValue, +) -> Result<(&'static str, SymbolicValue), TraceRecordError> { + let result = builder + .append_value_inst( + block, + ip, + SsaValueRepr::Bool, + SsaInstKind::ValueCmpEq { + lhs: lhs.value.id, + rhs: rhs.value.id, + }, + ) + .map_err(|err| TraceRecordError::InvalidIr(err.to_string()))?; + Ok(( + "value_eq", + SymbolicValue { + value: result, + info: ValueInfo::bool(None), + }, + )) +} + fn emit_int_compare( builder: &mut SsaTraceBuilder, block: super::ir::SsaBlockId, @@ -2533,6 +2578,15 @@ fn select_specialized_builtin_kind( container_was_moved: bool, ) -> Option { match builtin { + BuiltinFunction::ReMatch => return Some(SpecializedBuiltinKind::RegexMatch), + BuiltinFunction::ReReplace => return Some(SpecializedBuiltinKind::RegexReplace), + BuiltinFunction::StringContains => return Some(SpecializedBuiltinKind::StringContains), + BuiltinFunction::StringLowerAscii => { + return Some(SpecializedBuiltinKind::StringLowerAscii); + } + BuiltinFunction::StringSplitLiteral => { + return Some(SpecializedBuiltinKind::StringSplitLiteral); + } BuiltinFunction::MapIterNext => return Some(SpecializedBuiltinKind::MapIterNext), BuiltinFunction::MapIterTakeKey => return Some(SpecializedBuiltinKind::MapIterTakeKey), BuiltinFunction::MapIterTakeValue => { @@ -2578,18 +2632,11 @@ fn select_specialized_builtin_kind( } (BuiltinFunction::Get, HeapContainerKind::Bytes) => Some(SpecializedBuiltinKind::BytesGet), (BuiltinFunction::Has, HeapContainerKind::Bytes) => Some(SpecializedBuiltinKind::BytesHas), - (BuiltinFunction::StringContains, HeapContainerKind::String) => { - Some(SpecializedBuiltinKind::StringContains) - } - (BuiltinFunction::StringReplaceLiteral, HeapContainerKind::String) => { + (BuiltinFunction::StringReplaceLiteral, HeapContainerKind::String) + if container_was_moved => + { Some(SpecializedBuiltinKind::StringReplaceLiteral) } - (BuiltinFunction::StringLowerAscii, HeapContainerKind::String) => { - Some(SpecializedBuiltinKind::StringLowerAscii) - } - (BuiltinFunction::StringSplitLiteral, HeapContainerKind::String) => { - Some(SpecializedBuiltinKind::StringSplitLiteral) - } (BuiltinFunction::Concat, HeapContainerKind::String) => { Some(SpecializedBuiltinKind::StringConcat) } @@ -2674,6 +2721,19 @@ fn analyze_specialized_builtin_call( frame.push(ValueInfo::bool(None)); Ok("string_contains") } + SpecializedBuiltinKind::RegexMatch => { + let _ = frame.pop()?; + let _ = frame.pop()?; + frame.push(ValueInfo::bool(None)); + Ok("regex_match") + } + SpecializedBuiltinKind::RegexReplace => { + let _ = frame.pop()?; + let _ = frame.pop()?; + let _ = frame.pop()?; + frame.push(ValueInfo::tagged_typed(ValueType::String)); + Ok("regex_replace") + } SpecializedBuiltinKind::StringReplaceLiteral => { let _ = frame.pop()?; let _ = frame.pop()?; @@ -3010,6 +3070,78 @@ fn emit_specialized_builtin_call( .map_err(|err| TraceRecordError::InvalidIr(err.to_string()))?; Ok(("string_contains", out)) } + SpecializedBuiltinKind::RegexMatch => { + let text = ensure_heap_ptr( + builder, + block, + ip, + frame.pop()?, + HeapContainerKind::String.value_type(), + )?; + let pattern = ensure_heap_ptr( + builder, + block, + ip, + frame.pop()?, + HeapContainerKind::String.value_type(), + )?; + let out = builder + .append_value_inst( + block, + ip, + SsaValueRepr::Bool, + SsaInstKind::RegexMatch { + pattern: pattern.value.id, + text: text.value.id, + }, + ) + .map(|value| SymbolicValue { + value, + info: ValueInfo::bool(None), + }) + .map_err(|err| TraceRecordError::InvalidIr(err.to_string()))?; + Ok(("regex_match", out)) + } + SpecializedBuiltinKind::RegexReplace => { + let replacement = ensure_heap_ptr( + builder, + block, + ip, + frame.pop()?, + HeapContainerKind::String.value_type(), + )?; + let text = ensure_heap_ptr( + builder, + block, + ip, + frame.pop()?, + HeapContainerKind::String.value_type(), + )?; + let pattern = ensure_heap_ptr( + builder, + block, + ip, + frame.pop()?, + HeapContainerKind::String.value_type(), + )?; + let out = builder + .append_value_inst( + block, + ip, + SsaValueRepr::Tagged, + SsaInstKind::RegexReplace { + pattern: pattern.value.id, + text: text.value.id, + replacement: replacement.value.id, + }, + ) + .map(|value| SymbolicValue { + value, + info: ValueInfo::tagged_typed(ValueType::String), + }) + .map_err(|err| TraceRecordError::InvalidIr(err.to_string()))?; + Ok(("regex_replace", out)) + } SpecializedBuiltinKind::StringReplaceLiteral => { let replacement = ensure_heap_ptr( builder, diff --git a/src/vm/native/bridge.rs b/src/vm/native/bridge.rs index 74927436..9c85c4c4 100644 --- a/src/vm/native/bridge.rs +++ b/src/vm/native/bridge.rs @@ -203,6 +203,14 @@ pub(crate) fn string_contains_entry_address() -> usize { pd_vm_native_string_contains as *const () as usize } +pub(crate) fn regex_match_entry_address() -> usize { + pd_vm_native_regex_match as *const () as usize +} + +pub(crate) fn regex_replace_entry_address() -> usize { + pd_vm_native_regex_replace as *const () as usize +} + pub(crate) fn string_replace_literal_entry_address() -> usize { pd_vm_native_string_replace_literal as *const () as usize } @@ -404,6 +412,59 @@ pub(crate) extern "C" fn pd_vm_native_string_contains( ) } +pub(crate) extern "C" fn pd_vm_native_regex_match( + vm: *mut Vm, + pattern_ptr: *mut u8, + text_ptr: *mut u8, +) -> i32 { + let Some(vm_ref) = (unsafe { vm.as_mut() }) else { + store_bridge_error(VmError::JitNative( + "native regex-match helper received null vm pointer".to_string(), + )); + return STATUS_ERROR; + }; + let pattern = unsafe { std::mem::ManuallyDrop::new(arc_from_repr_ptr::(pattern_ptr)) }; + let text = unsafe { std::mem::ManuallyDrop::new(arc_from_repr_ptr::(text_ptr)) }; + match crate::builtins::runtime::regex::native_re_match(vm_ref, pattern.as_str(), text.as_str()) + { + Ok(matched) => i32::from(matched), + Err(err) => { + store_bridge_error(err); + STATUS_ERROR + } + } +} + +pub(crate) extern "C" fn pd_vm_native_regex_replace( + vm: *mut Vm, + pattern_ptr: *mut u8, + text_ptr: *mut u8, + replacement_ptr: *mut u8, +) -> *mut u8 { + let Some(vm_ref) = (unsafe { vm.as_mut() }) else { + store_bridge_error(VmError::JitNative( + "native regex-replace helper received null vm pointer".to_string(), + )); + return std::ptr::null_mut(); + }; + let pattern = unsafe { std::mem::ManuallyDrop::new(arc_from_repr_ptr::(pattern_ptr)) }; + let text = unsafe { std::mem::ManuallyDrop::new(arc_from_repr_ptr::(text_ptr)) }; + let replacement = + unsafe { std::mem::ManuallyDrop::new(arc_from_repr_ptr::(replacement_ptr)) }; + match crate::builtins::runtime::regex::native_re_replace( + vm_ref, + pattern.as_str(), + text.as_str(), + replacement.as_str(), + ) { + Ok(replaced) => arc_into_repr_ptr(Arc::new(replaced)), + Err(err) => { + store_bridge_error(err); + std::ptr::null_mut() + } + } +} + pub(crate) extern "C" fn pd_vm_native_string_replace_literal( text_ptr: *mut u8, needle_ptr: *mut u8, diff --git a/src/vm/native/codegen.rs b/src/vm/native/codegen.rs index 258b40eb..685b58ed 100644 --- a/src/vm/native/codegen.rs +++ b/src/vm/native/codegen.rs @@ -74,6 +74,33 @@ pub(crate) fn string_contains_signature( sig } +#[cfg(feature = "cranelift-jit")] +pub(crate) fn regex_match_signature( + pointer_type: cranelift_codegen::ir::Type, + call_conv: cranelift_codegen::isa::CallConv, +) -> Signature { + let mut sig = Signature::new(call_conv); + sig.params.push(AbiParam::new(pointer_type)); + sig.params.push(AbiParam::new(pointer_type)); + sig.params.push(AbiParam::new(pointer_type)); + sig.returns.push(AbiParam::new(types::I32)); + sig +} + +#[cfg(feature = "cranelift-jit")] +pub(crate) fn regex_replace_signature( + pointer_type: cranelift_codegen::ir::Type, + call_conv: cranelift_codegen::isa::CallConv, +) -> Signature { + let mut sig = Signature::new(call_conv); + sig.params.push(AbiParam::new(pointer_type)); + sig.params.push(AbiParam::new(pointer_type)); + sig.params.push(AbiParam::new(pointer_type)); + sig.params.push(AbiParam::new(pointer_type)); + sig.returns.push(AbiParam::new(pointer_type)); + sig +} + #[cfg(feature = "cranelift-jit")] pub(crate) fn string_unary_transform_signature( pointer_type: cranelift_codegen::ir::Type, diff --git a/src/vm/native/mod.rs b/src/vm/native/mod.rs index 51014d53..64e4c22e 100644 --- a/src/vm/native/mod.rs +++ b/src/vm/native/mod.rs @@ -17,12 +17,12 @@ pub(crate) use bridge::{ interrupt_helper_entry_offset, map_get_entry_address, map_has_entry_address, map_iter_next_entry_address, map_iter_take_key_entry_address, map_iter_take_value_entry_address, map_set_entry_address, non_yielding_host_call_entry_address, - restore_exit_state_entry_address, restore_sparse_exit_state_entry_address, - shared_array_from_buffer_entry_address, shared_bytes_from_buffer_entry_address, - shared_string_from_buffer_entry_address, store_bridge_error, string_contains_entry_address, - string_lower_ascii_entry_address, string_replace_literal_entry_address, - string_split_literal_entry_address, take_bridge_error, value_eq_entry_address, - write_heap_value_to_slot_entry_address, zero_bytes_entry_address, + regex_match_entry_address, regex_replace_entry_address, restore_exit_state_entry_address, + restore_sparse_exit_state_entry_address, shared_array_from_buffer_entry_address, + shared_bytes_from_buffer_entry_address, shared_string_from_buffer_entry_address, + store_bridge_error, string_contains_entry_address, string_lower_ascii_entry_address, + string_replace_literal_entry_address, string_split_literal_entry_address, take_bridge_error, + value_eq_entry_address, write_heap_value_to_slot_entry_address, zero_bytes_entry_address, }; #[cfg(feature = "cranelift-jit")] pub(crate) use codegen::{ @@ -30,10 +30,10 @@ pub(crate) use codegen::{ collection_get_signature, collection_mutation_signature, collection_predicate_signature, copy_bytes_signature, entry_signature, free_buffer_signature, helper_signature, jump_with_status, map_iter_next_signature, map_iter_take_signature, map_set_signature, - non_yielding_host_call_signature, pack_shared_signature, restore_exit_signature, - sparse_restore_exit_signature, string_binary_transform_signature, string_contains_signature, - string_replace_signature, string_unary_transform_signature, value_eq_signature, - value_slot_signature, + non_yielding_host_call_signature, pack_shared_signature, regex_match_signature, + regex_replace_signature, restore_exit_signature, sparse_restore_exit_signature, + string_binary_transform_signature, string_contains_signature, string_replace_signature, + string_unary_transform_signature, value_eq_signature, value_slot_signature, }; pub(crate) use exec::{ExecutableBuffer, prepare_for_execution}; pub(crate) use layout::{ diff --git a/tests/jit/jit_tests.rs b/tests/jit/jit_tests.rs index c07dc983..5fe46cb6 100644 --- a/tests/jit/jit_tests.rs +++ b/tests/jit/jit_tests.rs @@ -1230,16 +1230,11 @@ fn trace_jit_preserves_local_move_semantics_across_fuel_yields() { ); if native_jit_supported() { - let snapshot = vm.jit_snapshot(); assert!( - snapshot - .attempts - .iter() - .any(|attempt| attempt.result.is_err()), - "string-equality loop should record an NYI attempt, dump:\n{}", + vm.jit_native_exec_count() > 0, + "string-equality loop should use native value_eq, dump:\n{}", vm.dump_jit_info() ); - assert_eq!(vm.jit_native_exec_count(), 0); } } @@ -1453,16 +1448,11 @@ fn trace_jit_preserves_local_move_semantics_across_epoch_yields() { Some(&Value::Int(50)), "move-heavy loop should preserve final result across epoch yields" ); - let snapshot = vm.jit_snapshot(); assert!( - snapshot - .attempts - .iter() - .any(|attempt| attempt.result.is_err()), - "string-equality loop should record an NYI attempt, dump:\n{}", + vm.jit_native_exec_count() > 0, + "string-equality loop should use native value_eq, dump:\n{}", vm.dump_jit_info() ); - assert_eq!(vm.jit_native_exec_count(), 0); } #[test] @@ -4630,3 +4620,92 @@ fn trace_jit_specializes_literal_string_builtins_without_call_boundary() { vm.dump_jit_info() ); } + +#[test] +fn trace_jit_specializes_loop_carried_string_builtins() { + if !native_jit_supported() { + return; + } + let source = r#" + let values: [string] = ["abc"]; + let mut text: string = (&values)[0]; + let mut i = 0; + let mut found = false; + let mut same = false; + while i < 8 { + same = (&text) == "abc"; + found = string_contains(&text, "a"); + text = string_replace_literal(text, "x", "x"); + i = i + 1; + } + found; + same; + text; + "#; + let compiled = + compile_source(source).expect("loop-carried string builtin compile should succeed"); + let mut vm = Vm::new(compiled.program.with_local_count(compiled.locals)); + vm.set_jit_config(JitConfig { + enabled: true, + hot_loop_threshold: 1, + max_trace_len: 512, + }); + assert_eq!( + vm.run() + .expect("loop-carried string builtin jit should run"), + VmStatus::Halted + ); + assert_eq!( + vm.stack(), + &[Value::Bool(true), Value::Bool(true), Value::string("abc")] + ); + let snapshot = vm.jit_snapshot(); + assert_native_ssa_specialized_trace( + &vm, + &snapshot, + "loop-carried string builtin", + &["value_eq", "string_contains", "string_replace_literal"], + ); +} + +#[test] +fn trace_jit_specializes_regex_builtins_without_call_boundary() { + if !native_jit_supported() { + return; + } + let source = r#" + use re; + let mut i = 0; + let mut matched = false; + let mut replaced = ""; + while i < 8 { + matched = re::match("(?i)^rustscript$", "RustScript"); + replaced = re::replace("\\s+", "a b", ""); + i = i + 1; + } + matched; + replaced; + "#; + let compiled = compile_source(source).expect("regex match compile should succeed"); + let mut vm = Vm::new(compiled.program.with_local_count(compiled.locals)); + vm.set_jit_config(JitConfig { + enabled: true, + hot_loop_threshold: 1, + max_trace_len: 512, + }); + assert_eq!( + vm.run().expect("regex match jit should run"), + VmStatus::Halted + ); + assert_eq!(vm.stack(), &[Value::Bool(true), Value::string("ab")]); + let snapshot = vm.jit_snapshot(); + assert_native_ssa_specialized_trace( + &vm, + &snapshot, + "regex builtin loop", + &["regex_match", "regex_replace"], + ); + assert_eq!(vm.regex_cache_entry_count(), 2); + assert_eq!(vm.regex_cache_compile_count(), 2); + assert!(vm.regex_cache_hit_count() >= 14); +} From 55705dd430db1b78a5674465383b4443768bf969 Mon Sep 17 00:00:00 2001 From: fffonion Date: Fri, 17 Jul 2026 00:51:13 +0800 Subject: [PATCH 2/7] feat(strings): support ordered literal replacement arrays - overload string_replace_literal for parallel string arrays - preserve replacement order and validate array shapes - expose display conversion for native JIT helpers - add stdlib coverage for replacement sequences --- src/builtins/runtime/core.rs | 30 +++++++++++++++++++++++++++++- src/builtins/runtime/mod.rs | 11 +++++++++-- stdlib/tests/strings.rss | 4 ++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/builtins/runtime/core.rs b/src/builtins/runtime/core.rs index 8fb1ff7e..54e4223c 100644 --- a/src/builtins/runtime/core.rs +++ b/src/builtins/runtime/core.rs @@ -425,7 +425,7 @@ pub(super) fn builtin_type_of_impl(value: VmValueRef<'_>) -> String { /// Convert a value into a display string. #[pd_host_function(name = "__to_string")] -pub(super) fn builtin_to_string_impl(value: VmValueRef<'_>) -> String { +pub(crate) fn builtin_to_string_impl(value: VmValueRef<'_>) -> String { render_value_for_display(value) } @@ -792,6 +792,34 @@ pub(crate) fn builtin_string_replace_literal_impl( text.replace(needle, replacement) } +/// Apply ordered literal replacements from parallel needle/replacement arrays. +#[pd_host_function(name = "string_replace_literal")] +pub(crate) fn builtin_string_replace_literal_many_impl( + text: VmStringRef<'_>, + needles: VmArrayRef<'_>, + replacements: VmArrayRef<'_>, +) -> VmResult { + if needles.len() != replacements.len() { + return Err(VmError::HostError( + "string_replace_literal array lengths must match".to_string(), + )); + } + let mut out = text.to_string(); + for (needle, replacement) in needles.iter().zip(replacements.iter()) { + let (Value::String(needle), Value::String(replacement)) = (needle, replacement) else { + return Err(VmError::TypeMismatch("string arrays")); + }; + if needle.is_empty() || out.contains(needle.as_str()) { + out = builtin_string_replace_literal_impl( + out.as_str(), + needle.as_str(), + replacement.as_str(), + ); + } + } + Ok(out) +} + /// Lower ASCII `A`-`Z` bytes in `text` while preserving UTF-8. #[pd_host_function(name = "string_lower_ascii")] pub(crate) fn builtin_string_lower_ascii_impl(text: VmStringRef<'_>) -> String { diff --git a/src/builtins/runtime/mod.rs b/src/builtins/runtime/mod.rs index 96f03d35..cff772fd 100644 --- a/src/builtins/runtime/mod.rs +++ b/src/builtins/runtime/mod.rs @@ -76,8 +76,15 @@ pub(crate) fn execute_builtin_call( BuiltinFunction::MapIterClose => map_iter::close(vm, args).map(BuiltinCallOutcome::Return), BuiltinFunction::StringContains => core::builtin_string_contains(args) .map(IntoBuiltinCallOutcome::into_builtin_call_outcome), - BuiltinFunction::StringReplaceLiteral => core::builtin_string_replace_literal(args) - .map(IntoBuiltinCallOutcome::into_builtin_call_outcome), + BuiltinFunction::StringReplaceLiteral => { + if matches!(args, [Value::String(_), Value::Array(_), Value::Array(_)]) { + core::builtin_string_replace_literal_many(args) + .map(IntoBuiltinCallOutcome::into_builtin_call_outcome) + } else { + core::builtin_string_replace_literal(args) + .map(IntoBuiltinCallOutcome::into_builtin_call_outcome) + } + } BuiltinFunction::StringLowerAscii => core::builtin_string_lower_ascii(args) .map(IntoBuiltinCallOutcome::into_builtin_call_outcome), BuiltinFunction::StringSplitLiteral => core::builtin_string_split_literal(args) diff --git a/stdlib/tests/strings.rss b/stdlib/tests/strings.rss index ef3d35c9..f21db4a3 100644 --- a/stdlib/tests/strings.rss +++ b/stdlib/tests/strings.rss @@ -10,6 +10,10 @@ assert(contains("hello", "ell")); assert(contains("hello", "zzz") == false); assert(equals(trim(" rustscript\n"), "rustscript")); assert(equals(replace("go gopher go", "go", "rs"), "rs rspher rs")); +assert(equals( + string_replace_literal("%27x%20y+z", ["%27", "%20", "+"], ["'", " ", " "]), + "'x y z" +)); let mut parts = split("a,b,c", ","); assert(parts.length == 3); From d334dd44cbb65ebd1614d4ebb5fa12b3f3a10633 Mon Sep 17 00:00:00 2001 From: fffonion Date: Fri, 17 Jul 2026 00:51:33 +0800 Subject: [PATCH 3/7] feat(jit): specialize dynamic WAF trace operations - add checked tagged-value length lowering - specialize ordered literal replacement arrays - fold known type and display conversions inside traces - trust proven unboxed numeric SSA representations over stale hints - add focused recorder and end-to-end JIT coverage --- src/vm/jit/ir.rs | 37 ++++- src/vm/jit/native/lower.rs | 155 ++++++++++++++++- src/vm/jit/recorder.rs | 332 +++++++++++++++++++++++++++++++++++-- src/vm/native/bridge.rs | 79 +++++++++ src/vm/native/codegen.rs | 12 ++ src/vm/native/mod.rs | 10 +- tests/jit/jit_tests.rs | 152 ++++++++++++++++- 7 files changed, 748 insertions(+), 29 deletions(-) diff --git a/src/vm/jit/ir.rs b/src/vm/jit/ir.rs index cc80b6c2..39b97ca2 100644 --- a/src/vm/jit/ir.rs +++ b/src/vm/jit/ir.rs @@ -107,6 +107,9 @@ pub(crate) enum SsaInstKind { input: SsaValueId, tag: ValueType, }, + ValueLen { + value: SsaValueId, + }, StringLen { text: SsaValueId, }, @@ -153,9 +156,20 @@ pub(crate) enum SsaInstKind { needle: SsaValueId, replacement: SsaValueId, }, + StringReplaceLiteralMany { + text: SsaValueId, + needles: SsaValueId, + replacements: SsaValueId, + }, StringLowerAscii { text: SsaValueId, }, + TypeOf { + value: SsaValueId, + }, + ToString { + value: SsaValueId, + }, StringSplitLiteral { text: SsaValueId, delimiter: SsaValueId, @@ -371,6 +385,7 @@ impl SsaInstKind { | Self::UnboxFloat { input } | Self::UnboxBool { input } | Self::UnboxHeapPtr { input, .. } + | Self::ValueLen { value: input } | Self::StringLen { text: input } | Self::BytesLen { bytes: input } | Self::ArrayLen { array: input } @@ -399,11 +414,13 @@ impl SsaInstKind { replacement, } => vec![*pattern, *text, *replacement], Self::StringReplaceLiteral { - text, - needle, - replacement, + text, needle, replacement, } => vec![*text, *needle, *replacement], + Self::StringReplaceLiteralMany { + text, needles, replacements, + } => vec![*text, *needles, *replacements], Self::StringLowerAscii { text } => vec![*text], + Self::TypeOf { value } | Self::ToString { value } => vec![*value], Self::StringSplitLiteral { text, delimiter } => vec![*text, *delimiter], Self::StringConcat { lhs, rhs } | Self::BytesConcat { lhs, rhs } => vec![*lhs, *rhs], Self::BytesFromArrayU8 { array } => vec![*array], @@ -995,6 +1012,7 @@ fn render_inst_kind(kind: &SsaInstKind) -> String { SsaInstKind::UnboxFloat { input } => format!("unbox_float {input}"), SsaInstKind::UnboxBool { input } => format!("unbox_bool {input}"), SsaInstKind::UnboxHeapPtr { input, tag } => format!("unbox_ptr {input}, {tag:?}"), + SsaInstKind::ValueLen { value } => format!("value_len {value}"), SsaInstKind::StringLen { text } => format!("string_len {text}"), SsaInstKind::BytesLen { bytes } => format!("bytes_len {bytes}"), SsaInstKind::StringSlice { @@ -1021,12 +1039,15 @@ fn render_inst_kind(kind: &SsaInstKind) -> String { text, replacement, } => format!("regex_replace {pattern}, {text}, {replacement}"), - SsaInstKind::StringReplaceLiteral { - text, - needle, - replacement, - } => format!("string_replace_literal {text}, {needle}, {replacement}"), + SsaInstKind::StringReplaceLiteral { text, needle, replacement } => { + format!("string_replace_literal {text}, {needle}, {replacement}") + } + SsaInstKind::StringReplaceLiteralMany { text, needles, replacements } => { + format!("string_replace_literal_many {text}, {needles}, {replacements}") + } SsaInstKind::StringLowerAscii { text } => format!("string_lower_ascii {text}"), + SsaInstKind::TypeOf { value } => format!("type_of {value}"), + SsaInstKind::ToString { value } => format!("to_string {value}"), SsaInstKind::StringSplitLiteral { text, delimiter } => { format!("string_split_literal {text}, {delimiter}") } diff --git a/src/vm/jit/native/lower.rs b/src/vm/jit/native/lower.rs index f6d9a02a..42df91bf 100644 --- a/src/vm/jit/native/lower.rs +++ b/src/vm/jit/native/lower.rs @@ -26,8 +26,10 @@ use crate::vm::native::{ shared_string_from_buffer_entry_address, sparse_restore_exit_signature, string_binary_transform_signature, string_contains_entry_address, string_contains_signature, string_lower_ascii_entry_address, string_replace_literal_entry_address, + string_replace_literal_many_entry_address, string_replace_signature, string_split_literal_entry_address, string_unary_transform_signature, - value_eq_entry_address, value_eq_signature, value_slot_signature, + to_string_entry_address, type_of_entry_address, value_eq_entry_address, value_eq_signature, + value_len_entry_address, value_len_signature, value_slot_signature, write_heap_value_to_slot_entry_address, zero_bytes_entry_address, }; use cranelift_codegen::ir::condcodes::{FloatCC, IntCC}; @@ -103,6 +105,7 @@ fn try_compile_ssa_trace( let non_yielding_host_call_sig = non_yielding_host_call_signature(pointer_type, call_conv); let value_slot_sig = value_slot_signature(pointer_type, call_conv); let value_eq_sig = value_eq_signature(pointer_type, call_conv); + let value_len_sig = value_len_signature(pointer_type, call_conv); let box_heap_value_sig = box_heap_value_signature(pointer_type, call_conv); let alloc_buffer_sig = alloc_buffer_signature(pointer_type, call_conv); let free_buffer_sig = free_buffer_signature(pointer_type, call_conv); @@ -158,7 +161,9 @@ fn try_compile_ssa_trace( regex_match_ref: b.import_signature(regex_match_sig), regex_replace_ref: b.import_signature(regex_replace_sig), replace_ref: b.import_signature(string_replace_sig), - lower_ascii_ref: b.import_signature(string_lower_sig), + lower_ascii_ref: b.import_signature(string_lower_sig.clone()), + type_of_ref: b.import_signature(string_lower_sig.clone()), + to_string_ref: b.import_signature(string_lower_sig), split_literal_ref: b.import_signature(string_split_sig), }; let string_addrs = SsaStringHelperAddrs { @@ -166,12 +171,16 @@ fn try_compile_ssa_trace( regex_match: regex_match_entry_address(), regex_replace: regex_replace_entry_address(), replace_literal: string_replace_literal_entry_address(), + replace_literal_many: string_replace_literal_many_entry_address(), lower_ascii: string_lower_ascii_entry_address(), + type_of: type_of_entry_address(), + to_string: to_string_entry_address(), split_literal: string_split_literal_entry_address(), }; let deopt_refs = SsaDeoptHelperRefs { clone_value_ref: b.import_signature(clone_value_sig), value_eq_ref: b.import_signature(value_eq_sig), + value_len_ref: b.import_signature(value_len_sig), non_yielding_host_call_ref: b.import_signature(non_yielding_host_call_sig), clear_value_slot_ref: b.import_signature(value_slot_sig), box_heap_value_ref: b.import_signature(box_heap_value_sig), @@ -189,6 +198,7 @@ fn try_compile_ssa_trace( let deopt_addrs = SsaDeoptHelperAddrs { clone_value: clone_value_to_slot_entry_address(), value_eq: value_eq_entry_address(), + value_len: value_len_entry_address(), non_yielding_host_call: non_yielding_host_call_entry_address(), clear_value_slot: clear_value_slot_entry_address(), box_heap_value: write_heap_value_to_slot_entry_address(), @@ -437,6 +447,7 @@ struct SsaExitLowering { struct SsaDeoptHelperRefs { clone_value_ref: cranelift_codegen::ir::SigRef, value_eq_ref: cranelift_codegen::ir::SigRef, + value_len_ref: cranelift_codegen::ir::SigRef, non_yielding_host_call_ref: cranelift_codegen::ir::SigRef, clear_value_slot_ref: cranelift_codegen::ir::SigRef, box_heap_value_ref: cranelift_codegen::ir::SigRef, @@ -456,6 +467,7 @@ struct SsaDeoptHelperRefs { struct SsaDeoptHelperAddrs { clone_value: usize, value_eq: usize, + value_len: usize, non_yielding_host_call: usize, clear_value_slot: usize, box_heap_value: usize, @@ -478,6 +490,8 @@ struct SsaStringHelperRefs { regex_replace_ref: cranelift_codegen::ir::SigRef, replace_ref: cranelift_codegen::ir::SigRef, lower_ascii_ref: cranelift_codegen::ir::SigRef, + type_of_ref: cranelift_codegen::ir::SigRef, + to_string_ref: cranelift_codegen::ir::SigRef, split_literal_ref: cranelift_codegen::ir::SigRef, } @@ -487,7 +501,10 @@ struct SsaStringHelperAddrs { regex_match: usize, regex_replace: usize, replace_literal: usize, + replace_literal_many: usize, lower_ascii: usize, + type_of: usize, + to_string: usize, split_literal: usize, } @@ -564,6 +581,7 @@ fn ssa_trace_supported(ssa: &SsaTrace) -> bool { | SsaInstKind::UnboxInt { .. } | SsaInstKind::UnboxFloat { .. } | SsaInstKind::UnboxBool { .. } + | SsaInstKind::ValueLen { .. } | SsaInstKind::StringLen { .. } | SsaInstKind::BytesLen { .. } | SsaInstKind::StringSlice { .. } @@ -575,7 +593,10 @@ fn ssa_trace_supported(ssa: &SsaTrace) -> bool { | SsaInstKind::RegexMatch { .. } | SsaInstKind::RegexReplace { .. } | SsaInstKind::StringReplaceLiteral { .. } + | SsaInstKind::StringReplaceLiteralMany { .. } | SsaInstKind::StringLowerAscii { .. } + | SsaInstKind::TypeOf { .. } + | SsaInstKind::ToString { .. } | SsaInstKind::StringSplitLiteral { .. } | SsaInstKind::StringConcat { .. } | SsaInstKind::BytesConcat { .. } @@ -856,7 +877,10 @@ fn ssa_inst_requires_owned_value_slot(kind: &SsaInstKind) -> bool { | SsaInstKind::StringGet { .. } | SsaInstKind::RegexReplace { .. } | SsaInstKind::StringReplaceLiteral { .. } + | SsaInstKind::StringReplaceLiteralMany { .. } | SsaInstKind::StringLowerAscii { .. } + | SsaInstKind::TypeOf { .. } + | SsaInstKind::ToString { .. } | SsaInstKind::StringSplitLiteral { .. } | SsaInstKind::BytesFromArrayU8 { .. } | SsaInstKind::BytesToArrayU8 { .. } @@ -1242,6 +1266,34 @@ fn lower_ssa_inst( b.switch_to_block(cont); out } + SsaInstKind::ValueLen { value } => { + let value = values[value]; + let out_slot = b.create_sized_stack_slot(StackSlotData::new( + StackSlotKind::ExplicitSlot, + std::mem::size_of::() as u32, + std::mem::align_of::().trailing_zeros() as u8, + )); + let out = b.ins().stack_addr(pointer_type, out_slot, 0); + let helper_ptr = iconst_ptr_from_addr(b, pointer_type, helper_addrs.value_len)?; + let call = b.ins().call_indirect( + helper_refs.value_len_ref, + helper_ptr, + &[value, out], + ); + let status = b.inst_results(call)[0]; + let success = b.create_block(); + let fail = b.create_block(); + let ok = b + .ins() + .icmp_imm(IntCC::Equal, status, i64::from(STATUS_CONTINUE)); + b.ins().brif(ok, success, &[], fail, &[]); + + b.switch_to_block(fail); + jump_with_status(b, exit_block, status); + + b.switch_to_block(success); + b.ins().stack_load(types::I64, out_slot, 0) + } SsaInstKind::StringLen { text } => { let string_data = ssa_load_heap_data_ptr(b, layout.value, values[text]); let bytes_ptr = b.ins().load( @@ -1781,6 +1833,33 @@ fn lower_ssa_inst( ssa_store_heap_ptr_in_value(b, layout.value, out, layout.value.string_tag, out_raw); out } + SsaInstKind::StringReplaceLiteralMany { + text, + needles, + replacements, + } => { + let text = values[text]; + let needles = values[needles]; + let replacements = values[replacements]; + let out = owned_value_temp_slot_addr( + b, + pointer_type, + owned_value_temps, + SsaTempValueSlotKey::Output(output.id), + )?; + let out_raw = ssa_call_string_replace_literal_many( + b, + pointer_type, + string_refs, + string_addrs, + text, + needles, + replacements, + )?; + clear_owned_value_temp_slot(b, pointer_type, helper_refs, helper_addrs, out)?; + ssa_store_heap_ptr_in_value(b, layout.value, out, layout.value.string_tag, out_raw); + out + } SsaInstKind::StringReplaceLiteral { text, needle, @@ -1822,6 +1901,32 @@ fn lower_ssa_inst( ssa_store_heap_ptr_in_value(b, layout.value, out, layout.value.string_tag, out_raw); out } + SsaInstKind::TypeOf { value } => { + let value = values[value]; + let out = owned_value_temp_slot_addr( + b, + pointer_type, + owned_value_temps, + SsaTempValueSlotKey::Output(output.id), + )?; + let out_raw = ssa_call_type_of(b, pointer_type, string_refs, string_addrs, value)?; + clear_owned_value_temp_slot(b, pointer_type, helper_refs, helper_addrs, out)?; + ssa_store_heap_ptr_in_value(b, layout.value, out, layout.value.string_tag, out_raw); + out + } + SsaInstKind::ToString { value } => { + let value = values[value]; + let out = owned_value_temp_slot_addr( + b, + pointer_type, + owned_value_temps, + SsaTempValueSlotKey::Output(output.id), + )?; + let out_raw = ssa_call_to_string(b, pointer_type, string_refs, string_addrs, value)?; + clear_owned_value_temp_slot(b, pointer_type, helper_refs, helper_addrs, out)?; + ssa_store_heap_ptr_in_value(b, layout.value, out, layout.value.string_tag, out_raw); + out + } SsaInstKind::StringSplitLiteral { text, delimiter } => { let text = values[text]; let delimiter = values[delimiter]; @@ -3913,6 +4018,24 @@ fn ssa_call_string_replace_literal( Ok(b.inst_results(call)[0]) } +fn ssa_call_string_replace_literal_many( + b: &mut FunctionBuilder, + pointer_type: cranelift_codegen::ir::Type, + string_refs: SsaStringHelperRefs, + string_addrs: SsaStringHelperAddrs, + text: cranelift_codegen::ir::Value, + needles: cranelift_codegen::ir::Value, + replacements: cranelift_codegen::ir::Value, +) -> VmResult { + let helper_ptr = iconst_ptr_from_addr(b, pointer_type, string_addrs.replace_literal_many)?; + let call = b.ins().call_indirect( + string_refs.replace_ref, + helper_ptr, + &[text, needles, replacements], + ); + Ok(b.inst_results(call)[0]) +} + fn ssa_call_string_lower_ascii( b: &mut FunctionBuilder, pointer_type: cranelift_codegen::ir::Type, @@ -3927,6 +4050,34 @@ fn ssa_call_string_lower_ascii( Ok(b.inst_results(call)[0]) } +fn ssa_call_type_of( + b: &mut FunctionBuilder, + pointer_type: cranelift_codegen::ir::Type, + string_refs: SsaStringHelperRefs, + string_addrs: SsaStringHelperAddrs, + value: cranelift_codegen::ir::Value, +) -> VmResult { + let helper_ptr = iconst_ptr_from_addr(b, pointer_type, string_addrs.type_of)?; + let call = b + .ins() + .call_indirect(string_refs.type_of_ref, helper_ptr, &[value]); + Ok(b.inst_results(call)[0]) +} + +fn ssa_call_to_string( + b: &mut FunctionBuilder, + pointer_type: cranelift_codegen::ir::Type, + string_refs: SsaStringHelperRefs, + string_addrs: SsaStringHelperAddrs, + value: cranelift_codegen::ir::Value, +) -> VmResult { + let helper_ptr = iconst_ptr_from_addr(b, pointer_type, string_addrs.to_string)?; + let call = b + .ins() + .call_indirect(string_refs.to_string_ref, helper_ptr, &[value]); + Ok(b.inst_results(call)[0]) +} + fn ssa_call_string_split_literal( b: &mut FunctionBuilder, pointer_type: cranelift_codegen::ir::Type, diff --git a/src/vm/jit/recorder.rs b/src/vm/jit/recorder.rs index 0335dee5..6707b7cd 100644 --- a/src/vm/jit/recorder.rs +++ b/src/vm/jit/recorder.rs @@ -87,6 +87,7 @@ struct ValueInfo { const_float: Option, const_bool: Option, known_type: Option, + force_value_eq: bool, source_local: Option, } @@ -98,6 +99,7 @@ impl ValueInfo { const_float: None, const_bool: None, known_type: None, + force_value_eq: false, source_local: None, } } @@ -109,6 +111,7 @@ impl ValueInfo { const_float: None, const_bool: None, known_type: Some(known_type), + force_value_eq: false, source_local: None, } } @@ -120,6 +123,7 @@ impl ValueInfo { const_float: None, const_bool: None, known_type: Some(ValueType::Int), + force_value_eq: false, source_local: None, } } @@ -131,6 +135,7 @@ impl ValueInfo { const_float: value, const_bool: None, known_type: Some(ValueType::Float), + force_value_eq: false, source_local: None, } } @@ -142,6 +147,7 @@ impl ValueInfo { const_float: None, const_bool: value, known_type: Some(ValueType::Bool), + force_value_eq: false, source_local: None, } } @@ -153,6 +159,7 @@ impl ValueInfo { const_float: None, const_bool: None, known_type: Some(tag), + force_value_eq: false, source_local: None, } } @@ -170,6 +177,12 @@ impl ValueInfo { } } + fn type_name() -> Self { + let mut info = Self::tagged_typed(ValueType::String); + info.force_value_eq = true; + info + } + fn sourced_from(mut self, local: u8) -> Self { self.source_local = Some(local); self @@ -457,6 +470,7 @@ enum HeapContainerKind { #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum SpecializedBuiltinKind { + ValueLen, StringLen, BytesLen, StringSlice, @@ -468,7 +482,12 @@ enum SpecializedBuiltinKind { RegexMatch, RegexReplace, StringReplaceLiteral, + StringReplaceLiteralMany, StringLowerAscii, + TypeOf, + TypeOfKnown(ValueType), + ToString, + ToStringIdentity, StringSplitLiteral, StringConcat, BytesConcat, @@ -731,6 +750,7 @@ pub(crate) fn record_trace( const_float: None, const_bool: None, known_type: loop_plan.stack_known_types[index], + force_value_eq: false, source_local: None, }, }); @@ -788,6 +808,7 @@ pub(crate) fn record_trace( const_float: None, const_bool: None, known_type: loop_plan.local_known_types[local], + force_value_eq: false, source_local: None, }, }); @@ -1133,13 +1154,28 @@ pub(crate) fn record_trace( .iter() .all(|arg| arg.info.source_local != Some(local)) }); - if let Some(kind) = select_specialized_builtin_kind( - program, - ip, - builtin, - args[0].info, - container_was_moved, - ) { + let replace_many = builtin == BuiltinFunction::StringReplaceLiteral + && (operand_types(program, ip).1 == ValueType::Array + || (args.get(1).is_some_and(|arg| { + observed_heap_container_kind(arg.info) + == Some(HeapContainerKind::Array) + }) + && args.get(2).is_some_and(|arg| { + observed_heap_container_kind(arg.info) + == Some(HeapContainerKind::Array) + }))); + let specialized_kind = if replace_many { + Some(SpecializedBuiltinKind::StringReplaceLiteralMany) + } else { + select_specialized_builtin_kind( + program, + ip, + builtin, + args[0].info, + container_was_moved, + ) + }; + if let Some(kind) = specialized_kind { let (name, out) = emit_specialized_builtin_call( &mut builder, current_block, @@ -1532,6 +1568,39 @@ fn select_numeric_binop( lhs: ValueInfo, rhs: ValueInfo, ) -> Result { + if lhs.repr == SsaValueRepr::I64 && rhs.repr == SsaValueRepr::I64 { + let kind = match opcode { + x if x == OpCode::Add as u8 => IntBinOpKind::Add, + x if x == OpCode::Sub as u8 => IntBinOpKind::Sub, + x if x == OpCode::Mul as u8 => IntBinOpKind::Mul, + x if x == OpCode::Div as u8 => IntBinOpKind::Div, + x if x == OpCode::Mod as u8 => IntBinOpKind::Mod, + x if x == OpCode::Shl as u8 => IntBinOpKind::Shl, + x if x == OpCode::Shr as u8 => IntBinOpKind::Shr, + x if x == OpCode::Lshr as u8 => IntBinOpKind::Lshr, + _ => { + return Err(TraceRecordError::UnsupportedTrace( + "SSA recorder expected a numeric binop opcode".to_string(), + )); + } + }; + return Ok(NumericBinOpKind::Int(kind)); + } + if lhs.repr == SsaValueRepr::F64 && rhs.repr == SsaValueRepr::F64 { + let kind = match opcode { + x if x == OpCode::Add as u8 => FloatBinOpKind::Add, + x if x == OpCode::Sub as u8 => FloatBinOpKind::Sub, + x if x == OpCode::Mul as u8 => FloatBinOpKind::Mul, + x if x == OpCode::Div as u8 => FloatBinOpKind::Div, + x if x == OpCode::Mod as u8 => FloatBinOpKind::Mod, + _ => { + return Err(TraceRecordError::UnsupportedTrace( + "SSA recorder expected a numeric binop opcode".to_string(), + )); + } + }; + return Ok(NumericBinOpKind::Float(kind)); + } let operand_types = operand_types(program, ip); let observed_concat = observed_concat_binop_kind(lhs, rhs); let int_like = matches!(lhs.repr, SsaValueRepr::I64 | SsaValueRepr::Tagged) @@ -1734,6 +1803,43 @@ fn select_numeric_compare( lhs: ValueInfo, rhs: ValueInfo, ) -> Result { + if lhs.force_value_eq || rhs.force_value_eq { + return Err(TraceRecordError::UnsupportedTrace( + "SSA recorder requires value equality for known non-numeric operands".to_string(), + )); + } + if lhs.repr == SsaValueRepr::I64 && rhs.repr == SsaValueRepr::I64 { + return match opcode { + x if x == OpCode::Ceq as u8 => { + Ok(NumericCompareKind::Int(IntCompareKind::Eq)) + } + x if x == OpCode::Clt as u8 => { + Ok(NumericCompareKind::Int(IntCompareKind::Lt)) + } + x if x == OpCode::Cgt as u8 => { + Ok(NumericCompareKind::Int(IntCompareKind::Gt)) + } + _ => Err(TraceRecordError::UnsupportedTrace( + "SSA recorder expected a numeric comparison opcode".to_string(), + )), + }; + } + if lhs.repr == SsaValueRepr::F64 && rhs.repr == SsaValueRepr::F64 { + return match opcode { + x if x == OpCode::Ceq as u8 => { + Ok(NumericCompareKind::Float(FloatCompareKind::Eq)) + } + x if x == OpCode::Clt as u8 => { + Ok(NumericCompareKind::Float(FloatCompareKind::Lt)) + } + x if x == OpCode::Cgt as u8 => { + Ok(NumericCompareKind::Float(FloatCompareKind::Gt)) + } + _ => Err(TraceRecordError::UnsupportedTrace( + "SSA recorder expected a numeric comparison opcode".to_string(), + )), + }; + } let operand_types = operand_types(program, ip); let int_like = matches!(lhs.repr, SsaValueRepr::I64 | SsaValueRepr::Tagged) && matches!(rhs.repr, SsaValueRepr::I64 | SsaValueRepr::Tagged); @@ -2584,6 +2690,19 @@ fn select_specialized_builtin_kind( BuiltinFunction::StringLowerAscii => { return Some(SpecializedBuiltinKind::StringLowerAscii); } + BuiltinFunction::TypeOf => { + return Some(match container.known_type { + Some(value_type) => SpecializedBuiltinKind::TypeOfKnown(value_type), + None => SpecializedBuiltinKind::TypeOf, + }); + } + BuiltinFunction::ToString => { + return Some(if container.known_type == Some(ValueType::String) { + SpecializedBuiltinKind::ToStringIdentity + } else { + SpecializedBuiltinKind::ToString + }); + } BuiltinFunction::StringSplitLiteral => { return Some(SpecializedBuiltinKind::StringSplitLiteral); } @@ -2602,6 +2721,7 @@ fn select_specialized_builtin_kind( | BuiltinFunction::Get | BuiltinFunction::Has | BuiltinFunction::Concat + | BuiltinFunction::StringReplaceLiteral | BuiltinFunction::Set | BuiltinFunction::ArrayPush ) { @@ -2614,7 +2734,10 @@ fn select_specialized_builtin_kind( } } else { observed_kind - }?; + }; + let Some(container_kind) = container_kind else { + return (builtin == BuiltinFunction::Len).then_some(SpecializedBuiltinKind::ValueLen); + }; match (builtin, container_kind) { (BuiltinFunction::Len, HeapContainerKind::String) => { @@ -2632,9 +2755,7 @@ fn select_specialized_builtin_kind( } (BuiltinFunction::Get, HeapContainerKind::Bytes) => Some(SpecializedBuiltinKind::BytesGet), (BuiltinFunction::Has, HeapContainerKind::Bytes) => Some(SpecializedBuiltinKind::BytesHas), - (BuiltinFunction::StringReplaceLiteral, HeapContainerKind::String) - if container_was_moved => - { + (BuiltinFunction::StringReplaceLiteral, HeapContainerKind::String) => { Some(SpecializedBuiltinKind::StringReplaceLiteral) } (BuiltinFunction::Concat, HeapContainerKind::String) => { @@ -2673,6 +2794,11 @@ fn analyze_specialized_builtin_call( kind: SpecializedBuiltinKind, ) -> Result<&'static str, TraceRecordError> { match kind { + SpecializedBuiltinKind::ValueLen => { + let _ = frame.pop()?; + frame.push(ValueInfo::int(None)); + Ok("value_len") + } SpecializedBuiltinKind::StringLen => { let _ = frame.pop()?; frame.push(ValueInfo::int(None)); @@ -2734,18 +2860,37 @@ fn analyze_specialized_builtin_call( frame.push(ValueInfo::tagged_typed(ValueType::String)); Ok("regex_replace") } - SpecializedBuiltinKind::StringReplaceLiteral => { + SpecializedBuiltinKind::StringReplaceLiteral + | SpecializedBuiltinKind::StringReplaceLiteralMany => { let _ = frame.pop()?; let _ = frame.pop()?; let _ = frame.pop()?; frame.push(ValueInfo::tagged_typed(ValueType::String)); - Ok("string_replace_literal") + Ok(if matches!(kind, SpecializedBuiltinKind::StringReplaceLiteralMany) { + "string_replace_literal_many" + } else { + "string_replace_literal" + }) } SpecializedBuiltinKind::StringLowerAscii => { let _ = frame.pop()?; frame.push(ValueInfo::tagged_typed(ValueType::String)); Ok("string_lower_ascii") } + SpecializedBuiltinKind::TypeOf | SpecializedBuiltinKind::TypeOfKnown(_) => { + let _ = frame.pop()?; + frame.push(ValueInfo::type_name()); + Ok("type_of") + } + SpecializedBuiltinKind::ToString | SpecializedBuiltinKind::ToStringIdentity => { + let _ = frame.pop()?; + frame.push(ValueInfo::tagged_typed(ValueType::String)); + Ok(if matches!(kind, SpecializedBuiltinKind::ToStringIdentity) { + "to_string_identity" + } else { + "to_string" + }) + } SpecializedBuiltinKind::StringSplitLiteral => { let _ = frame.pop()?; let _ = frame.pop()?; @@ -2854,6 +2999,24 @@ fn emit_specialized_builtin_call( kind: SpecializedBuiltinKind, ) -> Result<(&'static str, SymbolicValue), TraceRecordError> { match kind { + SpecializedBuiltinKind::ValueLen => { + let value = frame.pop()?; + let out = builder + .append_value_inst( + block, + ip, + SsaValueRepr::I64, + SsaInstKind::ValueLen { + value: value.value.id, + }, + ) + .map(|value| SymbolicValue { + value, + info: ValueInfo::int(None), + }) + .map_err(|err| TraceRecordError::InvalidIr(err.to_string()))?; + Ok(("value_len", out)) + } SpecializedBuiltinKind::StringLen => { let text = frame.pop()?; let text = ensure_heap_ptr( @@ -3142,6 +3305,31 @@ fn emit_specialized_builtin_call( .map_err(|err| TraceRecordError::InvalidIr(err.to_string()))?; Ok(("regex_replace", out)) } + SpecializedBuiltinKind::StringReplaceLiteralMany => { + let replacements = ensure_heap_ptr( + builder, block, ip, frame.pop()?, ValueType::Array, + )?; + let needles = ensure_heap_ptr( + builder, block, ip, frame.pop()?, ValueType::Array, + )?; + let text = ensure_heap_ptr( + builder, block, ip, frame.pop()?, ValueType::String, + )?; + let value = builder + .append_value_inst( + block, ip, SsaValueRepr::Tagged, + SsaInstKind::StringReplaceLiteralMany { + text: text.value.id, + needles: needles.value.id, + replacements: replacements.value.id, + }, + ) + .map(|value| SymbolicValue { + value, info: ValueInfo::tagged_typed(ValueType::String), + }) + .map_err(|err| TraceRecordError::InvalidIr(err.to_string()))?; + Ok(("string_replace_literal_many", value)) + } SpecializedBuiltinKind::StringReplaceLiteral => { let replacement = ensure_heap_ptr( builder, @@ -3206,6 +3394,70 @@ fn emit_specialized_builtin_call( .map_err(|err| TraceRecordError::InvalidIr(err.to_string()))?; Ok(("string_lower_ascii", out)) } + SpecializedBuiltinKind::TypeOfKnown(value_type) => { + let _ = frame.pop()?; + let type_name = match value_type { + ValueType::Null => "null", + ValueType::Int => "int", + ValueType::Float => "float", + ValueType::Bool => "bool", + ValueType::String => "string", + ValueType::Bytes => "bytes", + ValueType::Array => "array", + ValueType::Map => "map", + ValueType::Unknown => { + return Err(TraceRecordError::UnsupportedTrace( + "type_of known specialization received unknown type".to_string(), + )); + } + }; + let constant = Value::string(type_name); + let info = ValueInfo::type_name(); + let value = builder + .append_value_inst(block, ip, info.repr, SsaInstKind::Constant(constant)) + .map_err(|err| TraceRecordError::InvalidIr(err.to_string()))?; + Ok(("type_of", SymbolicValue { value, info })) + } + SpecializedBuiltinKind::ToStringIdentity => { + let value = frame.pop()?; + Ok(("to_string_identity", value)) + } + SpecializedBuiltinKind::ToString => { + let value = frame.pop()?; + let out = builder + .append_value_inst( + block, + ip, + SsaValueRepr::Tagged, + SsaInstKind::ToString { + value: value.value.id, + }, + ) + .map(|value| SymbolicValue { + value, + info: ValueInfo::tagged_typed(ValueType::String), + }) + .map_err(|err| TraceRecordError::InvalidIr(err.to_string()))?; + Ok(("to_string", out)) + } + SpecializedBuiltinKind::TypeOf => { + let value = frame.pop()?; + let out = builder + .append_value_inst( + block, + ip, + SsaValueRepr::Tagged, + SsaInstKind::TypeOf { + value: value.value.id, + }, + ) + .map(|value| SymbolicValue { + value, + info: ValueInfo::type_name(), + }) + .map_err(|err| TraceRecordError::InvalidIr(err.to_string()))?; + Ok(("type_of", out)) + } SpecializedBuiltinKind::StringSplitLiteral => { let delimiter = ensure_heap_ptr( builder, @@ -3785,6 +4037,9 @@ fn validate_int_operands( lhs: ValueInfo, rhs: ValueInfo, ) -> Result<(), TraceRecordError> { + if lhs.repr == SsaValueRepr::I64 && rhs.repr == SsaValueRepr::I64 { + return Ok(()); + } let explicit = operand_types(program, ip); let has_evidence = lhs.repr == SsaValueRepr::I64 || rhs.repr == SsaValueRepr::I64 @@ -3820,6 +4075,9 @@ fn validate_int_compare_operands( lhs: ValueInfo, rhs: ValueInfo, ) -> Result<(), TraceRecordError> { + if lhs.repr == SsaValueRepr::I64 && rhs.repr == SsaValueRepr::I64 { + return Ok(()); + } let explicit = operand_types(program, ip); let has_evidence = lhs.repr == SsaValueRepr::I64 || rhs.repr == SsaValueRepr::I64 @@ -3855,6 +4113,9 @@ fn validate_float_operands( lhs: ValueInfo, rhs: ValueInfo, ) -> Result<(), TraceRecordError> { + if lhs.repr == SsaValueRepr::F64 && rhs.repr == SsaValueRepr::F64 { + return Ok(()); + } let explicit = operand_types(program, ip); let has_evidence = lhs.repr == SsaValueRepr::F64 || rhs.repr == SsaValueRepr::F64 @@ -4117,6 +4378,7 @@ fn continue_with_frame( const_float: None, const_bool: None, known_type: value.info.known_type, + force_value_eq: value.info.force_value_eq, source_local: None, }, }); @@ -4139,6 +4401,7 @@ fn continue_with_frame( const_float: None, const_bool: None, known_type: value.info.known_type, + force_value_eq: value.info.force_value_eq, source_local: None, }, }); @@ -4305,6 +4568,49 @@ mod tests { code[start..start + 4].copy_from_slice(&target.to_le_bytes()); } + #[test] + fn unknown_len_container_uses_checked_value_len_specialization() { + let program = Program::new(Vec::new(), Vec::new()); + assert_eq!( + select_specialized_builtin_kind( + &program, + 0, + BuiltinFunction::Len, + ValueInfo::tagged(), + false, + ), + Some(SpecializedBuiltinKind::ValueLen), + ); + } + + #[test] + fn unboxed_numeric_compare_ignores_stale_operand_type_hint() { + let program = Program::new(Vec::new(), Vec::new()).with_type_map(crate::TypeMap { + operand_types: std::collections::HashMap::from([( + 7, + (ValueType::Null, ValueType::Int), + )]), + ..crate::TypeMap::default() + }); + + let lhs = ValueInfo::int(None); + let rhs = ValueInfo::int(None); + assert_eq!( + select_numeric_compare(&program, 7, OpCode::Clt as u8, lhs, rhs) + .expect("unboxed integer compare should override stale hint"), + NumericCompareKind::Int(IntCompareKind::Lt), + ); + validate_int_compare_operands(&program, 7, IntCompareKind::Lt, lhs, rhs) + .expect("unboxed integer validator should override stale hint"); + assert_eq!( + select_numeric_binop(&program, 7, OpCode::Add as u8, lhs, rhs) + .expect("unboxed integer binop should override stale hint"), + NumericBinOpKind::Int(IntBinOpKind::Add), + ); + validate_int_operands(&program, 7, IntBinOpKind::Add, lhs, rhs) + .expect("unboxed integer binop validator should override stale hint"); + } + #[test] fn records_linear_local_increment_trace_directly() { let mut bc = BytecodeBuilder::new(); diff --git a/src/vm/native/bridge.rs b/src/vm/native/bridge.rs index 9c85c4c4..f63fd680 100644 --- a/src/vm/native/bridge.rs +++ b/src/vm/native/bridge.rs @@ -215,10 +215,22 @@ pub(crate) fn string_replace_literal_entry_address() -> usize { pd_vm_native_string_replace_literal as *const () as usize } +pub(crate) fn string_replace_literal_many_entry_address() -> usize { + pd_vm_native_string_replace_literal_many as *const () as usize +} + pub(crate) fn string_lower_ascii_entry_address() -> usize { pd_vm_native_string_lower_ascii as *const () as usize } +pub(crate) fn type_of_entry_address() -> usize { + pd_vm_native_type_of as *const () as usize +} + +pub(crate) fn to_string_entry_address() -> usize { + pd_vm_native_to_string as *const () as usize +} + pub(crate) fn string_split_literal_entry_address() -> usize { pd_vm_native_string_split_literal as *const () as usize } @@ -239,6 +251,10 @@ pub(crate) fn value_eq_entry_address() -> usize { pd_vm_native_value_eq as *const () as usize } +pub(crate) fn value_len_entry_address() -> usize { + pd_vm_native_value_len as *const () as usize +} + pub(crate) fn write_heap_value_to_slot_entry_address() -> usize { pd_vm_native_write_heap_value_to_slot as *const () as usize } @@ -474,6 +490,9 @@ pub(crate) extern "C" fn pd_vm_native_string_replace_literal( let needle = unsafe { std::mem::ManuallyDrop::new(arc_from_repr_ptr::(needle_ptr)) }; let replacement = unsafe { std::mem::ManuallyDrop::new(arc_from_repr_ptr::(replacement_ptr)) }; + if !needle.is_empty() && !text.contains(needle.as_str()) { + return arc_into_repr_ptr(Arc::clone(&*text)); + } arc_into_repr_ptr(Arc::new( crate::builtins::runtime::core::builtin_string_replace_literal_impl( text.as_str(), @@ -483,6 +502,20 @@ pub(crate) extern "C" fn pd_vm_native_string_replace_literal( )) } +pub(crate) extern "C" fn pd_vm_native_string_replace_literal_many( + text_ptr: *mut u8, needles_ptr: *mut u8, replacements_ptr: *mut u8, +) -> *mut u8 { + let text = unsafe { std::mem::ManuallyDrop::new(arc_from_repr_ptr::(text_ptr)) }; + let needles = unsafe { std::mem::ManuallyDrop::new(arc_from_repr_ptr::>(needles_ptr)) }; + let replacements = unsafe { std::mem::ManuallyDrop::new(arc_from_repr_ptr::>(replacements_ptr)) }; + match crate::builtins::runtime::core::builtin_string_replace_literal_many_impl( + text.as_str(), needles.as_slice(), replacements.as_slice(), + ) { + Ok(value) => arc_into_repr_ptr(Arc::new(value)), + Err(error) => { store_bridge_error(error); std::ptr::null_mut() } + } +} + pub(crate) extern "C" fn pd_vm_native_string_lower_ascii(text_ptr: *mut u8) -> *mut u8 { let text = unsafe { std::mem::ManuallyDrop::new(arc_from_repr_ptr::(text_ptr)) }; arc_into_repr_ptr(Arc::new( @@ -490,6 +523,27 @@ pub(crate) extern "C" fn pd_vm_native_string_lower_ascii(text_ptr: *mut u8) -> * )) } +pub(crate) extern "C" fn pd_vm_native_type_of(value_ptr: *const Value) -> *mut u8 { + let name = match unsafe { &*value_ptr } { + Value::Null => "null", + Value::Int(_) => "int", + Value::Float(_) => "float", + Value::Bool(_) => "bool", + Value::String(_) => "string", + Value::Bytes(_) => "bytes", + Value::Array(_) => "array", + Value::Map(_) => "map", + }; + arc_into_repr_ptr(Arc::new(name.to_string())) +} + +pub(crate) extern "C" fn pd_vm_native_to_string(value_ptr: *const Value) -> *mut u8 { + let value = unsafe { &*value_ptr }; + arc_into_repr_ptr(Arc::new( + crate::builtins::runtime::core::builtin_to_string_impl(value), + )) +} + pub(crate) extern "C" fn pd_vm_native_string_split_literal( text_ptr: *mut u8, delimiter_ptr: *mut u8, @@ -562,6 +616,31 @@ pub(crate) extern "C" fn pd_vm_native_value_eq(lhs: *const Value, rhs: *const Va i32::from(unsafe { *lhs == *rhs }) } +pub(crate) extern "C" fn pd_vm_native_value_len(value: *const Value, out: *mut i64) -> i32 { + if value.is_null() || out.is_null() { + store_bridge_error(VmError::JitNative( + "native value-len helper received null pointer".to_string(), + )); + return STATUS_ERROR; + } + let len = match unsafe { &*value } { + Value::String(value) => value.chars().count(), + Value::Bytes(value) => value.len(), + Value::Array(value) => value.len(), + Value::Map(value) => value.len(), + _ => { + store_bridge_error(VmError::TypeMismatch("string, bytes, array, or map")); + return STATUS_ERROR; + } + }; + let Ok(len) = i64::try_from(len) else { + store_bridge_error(VmError::IntegerOverflow("len")); + return STATUS_ERROR; + }; + unsafe { out.write(len) }; + STATUS_CONTINUE +} + pub(crate) extern "C" fn pd_vm_native_write_heap_value_to_slot( dst: *mut Value, repr_ptr: *mut u8, diff --git a/src/vm/native/codegen.rs b/src/vm/native/codegen.rs index 685b58ed..7768260e 100644 --- a/src/vm/native/codegen.rs +++ b/src/vm/native/codegen.rs @@ -291,6 +291,18 @@ pub(crate) fn value_eq_signature( sig } +#[cfg(feature = "cranelift-jit")] +pub(crate) fn value_len_signature( + pointer_type: cranelift_codegen::ir::Type, + call_conv: cranelift_codegen::isa::CallConv, +) -> Signature { + let mut sig = Signature::new(call_conv); + sig.params.push(AbiParam::new(pointer_type)); + sig.params.push(AbiParam::new(pointer_type)); + sig.returns.push(AbiParam::new(types::I32)); + sig +} + #[cfg(feature = "cranelift-jit")] pub(crate) fn box_heap_value_signature( pointer_type: cranelift_codegen::ir::Type, diff --git a/src/vm/native/mod.rs b/src/vm/native/mod.rs index 64e4c22e..d3f3de41 100644 --- a/src/vm/native/mod.rs +++ b/src/vm/native/mod.rs @@ -21,8 +21,11 @@ pub(crate) use bridge::{ restore_sparse_exit_state_entry_address, shared_array_from_buffer_entry_address, shared_bytes_from_buffer_entry_address, shared_string_from_buffer_entry_address, store_bridge_error, string_contains_entry_address, string_lower_ascii_entry_address, - string_replace_literal_entry_address, string_split_literal_entry_address, take_bridge_error, - value_eq_entry_address, write_heap_value_to_slot_entry_address, zero_bytes_entry_address, + string_replace_literal_entry_address, string_replace_literal_many_entry_address, + string_split_literal_entry_address, take_bridge_error, + to_string_entry_address, type_of_entry_address, value_eq_entry_address, + value_len_entry_address, write_heap_value_to_slot_entry_address, + zero_bytes_entry_address, }; #[cfg(feature = "cranelift-jit")] pub(crate) use codegen::{ @@ -33,7 +36,8 @@ pub(crate) use codegen::{ non_yielding_host_call_signature, pack_shared_signature, regex_match_signature, regex_replace_signature, restore_exit_signature, sparse_restore_exit_signature, string_binary_transform_signature, string_contains_signature, string_replace_signature, - string_unary_transform_signature, value_eq_signature, value_slot_signature, + string_unary_transform_signature, value_eq_signature, value_len_signature, + value_slot_signature, }; pub(crate) use exec::{ExecutableBuffer, prepare_for_execution}; pub(crate) use layout::{ diff --git a/tests/jit/jit_tests.rs b/tests/jit/jit_tests.rs index 5fe46cb6..b9656717 100644 --- a/tests/jit/jit_tests.rs +++ b/tests/jit/jit_tests.rs @@ -2893,7 +2893,12 @@ fn trace_jit_supports_float_and_string_loops_through_ssa() { assert_eq!(string_status, VmStatus::Halted); assert_eq!(string_vm.stack(), &[Value::string("xxx")]); let string_snapshot = string_vm.jit_snapshot(); - assert_native_ssa_call_boundary_trace(&string_vm, &string_snapshot, "string add loop"); + assert_native_ssa_specialized_trace( + &string_vm, + &string_snapshot, + "string add loop", + &["type_of", "to_string_identity", "string_concat"], + ); } #[test] @@ -3085,12 +3090,17 @@ fn trace_jit_supports_string_call_boundary_exits() { assert_eq!(status, VmStatus::Halted); assert_eq!(vm.stack(), &[Value::string("xxxxxx")]); let snapshot = vm.jit_snapshot(); - assert_native_ssa_call_boundary_trace(&vm, &snapshot, "string concat loop"); + assert_native_ssa_specialized_trace( + &vm, + &snapshot, + "string concat loop", + &["type_of", "to_string_identity", "string_concat"], + ); let bridge_hits = vm.jit_native_bridge_stats_snapshot(); assert!( bridge_hits.iter().all(|(_, count)| *count == 0), - "string concat loop should not need native helper bridges for call-boundary execution, bridge hits: {bridge_hits:?}\n{}", + "string concat loop should not need native helper bridges, bridge hits: {bridge_hits:?}\n{}", vm.dump_jit_info() ); } @@ -4668,6 +4678,142 @@ fn trace_jit_specializes_loop_carried_string_builtins() { ); } +#[test] +fn trace_jit_specializes_literal_replace_many() { + if !native_jit_supported() { + return; + } + let source = r#" + let mut i = 0; + let mut out = ""; + let needles = ["%27", "%20", "+"]; + let replacements = ["'", " ", " "]; + while i < 8 { + out = string_replace_literal( + "%27x%20y+z", + &needles, + &replacements + ); + i = i + 1; + } + out == "'x y z"; + "#; + let program = compile_source(source).expect("replace-many fixture should compile"); + let mut vm = Vm::new_with_jit_config( + program.program, + JitConfig { + enabled: true, + hot_loop_threshold: 1, + max_trace_len: 512, + }, + ); + assert_eq!( + vm.run().expect("replace-many fixture should run"), + VmStatus::Halted + ); + assert_eq!(vm.stack(), &[Value::Bool(true)]); + let snapshot = vm.jit_snapshot(); + assert!( + snapshot.traces.iter().any(|trace| { + trace.op_names.iter().any(|name| name == "string_replace_literal_many") + }), + "expected replace-many SSA specialization, got {}", + vm.dump_jit_info() + ); +} + +#[test] +fn trace_jit_preserves_dynamic_concat_type_guards() { + if !native_jit_supported() { + return; + } + let source = r#" + fn encode_map(values: map) -> string { + let keys = (&values).keys; + let mut out = ""; + for i in 0..keys.length { + let key: string = (&keys)[i]; + out = out + key + "=" + (&values)[key] + "\n"; + } + out + } + let values: map = { "a": "one", "b": "two" }; + let mut i = 0; + let mut out = ""; + while i < 8 { + out = encode_map(&values); + i = i + 1; + } + string_contains(&out, "a=one"); + "#; + let compiled = compile_source(source).expect("dynamic concat fixture should compile"); + let mut vm = Vm::new(compiled.program.with_local_count(compiled.locals)); + vm.set_jit_config(JitConfig { + enabled: true, + hot_loop_threshold: 1, + max_trace_len: 512, + }); + let status = vm.run().unwrap_or_else(|error| { + panic!( + "dynamic concat fixture failed at ip {} stack={:?}: {error:?}\n{}", + vm.ip(), + vm.stack(), + vm.dump_jit_info(), + ) + }); + assert_eq!(status, VmStatus::Halted); + assert_eq!(vm.stack(), &[Value::Bool(true)]); + let snapshot = vm.jit_snapshot(); + assert!( + snapshot.traces.iter().any(|trace| { + !trace.has_call + && ["type_of", "to_string_identity", "string_concat"] + .iter() + .all(|required| trace.op_names().iter().any(|op| op == required)) + }), + "dynamic concat inner trace should specialize type dispatch without calls:\n{}", + vm.dump_jit_info(), + ); +} + +#[test] +fn trace_jit_folds_known_type_of_guards_after_map_get() { + if !native_jit_supported() { + return; + } + let source = r#" + let values = { "key": "value", "number": 1 }; + let keys = ["key", "key"]; + let mut i = 0; + let mut matched = false; + while i < 8 { + let key = (&keys)[i % 2]; + matched = type((&values)[key]) == "string"; + i = i + 1; + } + matched; + "#; + let compiled = compile_source(source).expect("known type guard fixture should compile"); + let mut vm = Vm::new(compiled.program.with_local_count(compiled.locals)); + vm.set_jit_config(JitConfig { + enabled: true, + hot_loop_threshold: 1, + max_trace_len: 512, + }); + assert_eq!( + vm.run().expect("known type guard fixture should run"), + VmStatus::Halted + ); + assert_eq!(vm.stack(), &[Value::Bool(true)]); + let snapshot = vm.jit_snapshot(); + assert_native_ssa_specialized_trace( + &vm, + &snapshot, + "known type guard after map get", + &["map_get", "type_of", "value_eq"], + ); +} + #[test] fn trace_jit_specializes_regex_builtins_without_call_boundary() { if !native_jit_supported() { From 8f18bf3e7d72fbd8768d8f8fc787a6bb8f44e509 Mon Sep 17 00:00:00 2001 From: fffonion Date: Fri, 17 Jul 2026 00:51:48 +0800 Subject: [PATCH 4/7] feat(jit): expose terminal call exit addresses Allow workload diagnostics to attribute hot call-boundary exits to their bytecode instruction. --- src/vm/jit/trace.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/vm/jit/trace.rs b/src/vm/jit/trace.rs index 2dc4c0d8..55097972 100644 --- a/src/vm/jit/trace.rs +++ b/src/vm/jit/trace.rs @@ -128,6 +128,12 @@ impl JitTrace { .filter(|dirty| **dirty) .count() as u64 } + + pub fn terminal_call_exit_ip(&self) -> Option { + (self.op_names.last().map(String::as_str) == Some("call")) + .then(|| self.ssa.exits.last().map(|exit| exit.exit_ip)) + .flatten() + } } #[derive(Clone, Debug, PartialEq, Eq)] From d1e59810731de9dc19f2fd8ed3d61b3c875520d4 Mon Sep 17 00:00:00 2001 From: fffonion Date: Fri, 17 Jul 2026 01:05:41 +0800 Subject: [PATCH 5/7] style(jit): apply rustfmt to WAF specializations --- src/vm/jit/ir.rs | 20 ++++++++-- src/vm/jit/native/lower.rs | 18 ++++----- src/vm/jit/recorder.rs | 76 ++++++++++++++++---------------------- src/vm/native/bridge.rs | 19 +++++++--- src/vm/native/mod.rs | 7 ++-- tests/jit/jit_tests.rs | 5 ++- 6 files changed, 77 insertions(+), 68 deletions(-) diff --git a/src/vm/jit/ir.rs b/src/vm/jit/ir.rs index 39b97ca2..fb090cd2 100644 --- a/src/vm/jit/ir.rs +++ b/src/vm/jit/ir.rs @@ -414,10 +414,14 @@ impl SsaInstKind { replacement, } => vec![*pattern, *text, *replacement], Self::StringReplaceLiteral { - text, needle, replacement, + text, + needle, + replacement, } => vec![*text, *needle, *replacement], Self::StringReplaceLiteralMany { - text, needles, replacements, + text, + needles, + replacements, } => vec![*text, *needles, *replacements], Self::StringLowerAscii { text } => vec![*text], Self::TypeOf { value } | Self::ToString { value } => vec![*value], @@ -1039,10 +1043,18 @@ fn render_inst_kind(kind: &SsaInstKind) -> String { text, replacement, } => format!("regex_replace {pattern}, {text}, {replacement}"), - SsaInstKind::StringReplaceLiteral { text, needle, replacement } => { + SsaInstKind::StringReplaceLiteral { + text, + needle, + replacement, + } => { format!("string_replace_literal {text}, {needle}, {replacement}") } - SsaInstKind::StringReplaceLiteralMany { text, needles, replacements } => { + SsaInstKind::StringReplaceLiteralMany { + text, + needles, + replacements, + } => { format!("string_replace_literal_many {text}, {needles}, {replacements}") } SsaInstKind::StringLowerAscii { text } => format!("string_lower_ascii {text}"), diff --git a/src/vm/jit/native/lower.rs b/src/vm/jit/native/lower.rs index 42df91bf..a400bd65 100644 --- a/src/vm/jit/native/lower.rs +++ b/src/vm/jit/native/lower.rs @@ -26,11 +26,11 @@ use crate::vm::native::{ shared_string_from_buffer_entry_address, sparse_restore_exit_signature, string_binary_transform_signature, string_contains_entry_address, string_contains_signature, string_lower_ascii_entry_address, string_replace_literal_entry_address, - string_replace_literal_many_entry_address, - string_replace_signature, string_split_literal_entry_address, string_unary_transform_signature, - to_string_entry_address, type_of_entry_address, value_eq_entry_address, value_eq_signature, - value_len_entry_address, value_len_signature, value_slot_signature, - write_heap_value_to_slot_entry_address, zero_bytes_entry_address, + string_replace_literal_many_entry_address, string_replace_signature, + string_split_literal_entry_address, string_unary_transform_signature, to_string_entry_address, + type_of_entry_address, value_eq_entry_address, value_eq_signature, value_len_entry_address, + value_len_signature, value_slot_signature, write_heap_value_to_slot_entry_address, + zero_bytes_entry_address, }; use cranelift_codegen::ir::condcodes::{FloatCC, IntCC}; use cranelift_codegen::ir::immediates::Ieee64; @@ -1275,11 +1275,9 @@ fn lower_ssa_inst( )); let out = b.ins().stack_addr(pointer_type, out_slot, 0); let helper_ptr = iconst_ptr_from_addr(b, pointer_type, helper_addrs.value_len)?; - let call = b.ins().call_indirect( - helper_refs.value_len_ref, - helper_ptr, - &[value, out], - ); + let call = b + .ins() + .call_indirect(helper_refs.value_len_ref, helper_ptr, &[value, out]); let status = b.inst_results(call)[0]; let success = b.create_block(); let fail = b.create_block(); diff --git a/src/vm/jit/recorder.rs b/src/vm/jit/recorder.rs index 6707b7cd..56b664a7 100644 --- a/src/vm/jit/recorder.rs +++ b/src/vm/jit/recorder.rs @@ -1159,11 +1159,10 @@ pub(crate) fn record_trace( || (args.get(1).is_some_and(|arg| { observed_heap_container_kind(arg.info) == Some(HeapContainerKind::Array) - }) - && args.get(2).is_some_and(|arg| { - observed_heap_container_kind(arg.info) - == Some(HeapContainerKind::Array) - }))); + }) && args.get(2).is_some_and(|arg| { + observed_heap_container_kind(arg.info) + == Some(HeapContainerKind::Array) + }))); let specialized_kind = if replace_many { Some(SpecializedBuiltinKind::StringReplaceLiteralMany) } else { @@ -1810,15 +1809,9 @@ fn select_numeric_compare( } if lhs.repr == SsaValueRepr::I64 && rhs.repr == SsaValueRepr::I64 { return match opcode { - x if x == OpCode::Ceq as u8 => { - Ok(NumericCompareKind::Int(IntCompareKind::Eq)) - } - x if x == OpCode::Clt as u8 => { - Ok(NumericCompareKind::Int(IntCompareKind::Lt)) - } - x if x == OpCode::Cgt as u8 => { - Ok(NumericCompareKind::Int(IntCompareKind::Gt)) - } + x if x == OpCode::Ceq as u8 => Ok(NumericCompareKind::Int(IntCompareKind::Eq)), + x if x == OpCode::Clt as u8 => Ok(NumericCompareKind::Int(IntCompareKind::Lt)), + x if x == OpCode::Cgt as u8 => Ok(NumericCompareKind::Int(IntCompareKind::Gt)), _ => Err(TraceRecordError::UnsupportedTrace( "SSA recorder expected a numeric comparison opcode".to_string(), )), @@ -1826,15 +1819,9 @@ fn select_numeric_compare( } if lhs.repr == SsaValueRepr::F64 && rhs.repr == SsaValueRepr::F64 { return match opcode { - x if x == OpCode::Ceq as u8 => { - Ok(NumericCompareKind::Float(FloatCompareKind::Eq)) - } - x if x == OpCode::Clt as u8 => { - Ok(NumericCompareKind::Float(FloatCompareKind::Lt)) - } - x if x == OpCode::Cgt as u8 => { - Ok(NumericCompareKind::Float(FloatCompareKind::Gt)) - } + x if x == OpCode::Ceq as u8 => Ok(NumericCompareKind::Float(FloatCompareKind::Eq)), + x if x == OpCode::Clt as u8 => Ok(NumericCompareKind::Float(FloatCompareKind::Lt)), + x if x == OpCode::Cgt as u8 => Ok(NumericCompareKind::Float(FloatCompareKind::Gt)), _ => Err(TraceRecordError::UnsupportedTrace( "SSA recorder expected a numeric comparison opcode".to_string(), )), @@ -2866,11 +2853,13 @@ fn analyze_specialized_builtin_call( let _ = frame.pop()?; let _ = frame.pop()?; frame.push(ValueInfo::tagged_typed(ValueType::String)); - Ok(if matches!(kind, SpecializedBuiltinKind::StringReplaceLiteralMany) { - "string_replace_literal_many" - } else { - "string_replace_literal" - }) + Ok( + if matches!(kind, SpecializedBuiltinKind::StringReplaceLiteralMany) { + "string_replace_literal_many" + } else { + "string_replace_literal" + }, + ) } SpecializedBuiltinKind::StringLowerAscii => { let _ = frame.pop()?; @@ -2885,11 +2874,13 @@ fn analyze_specialized_builtin_call( SpecializedBuiltinKind::ToString | SpecializedBuiltinKind::ToStringIdentity => { let _ = frame.pop()?; frame.push(ValueInfo::tagged_typed(ValueType::String)); - Ok(if matches!(kind, SpecializedBuiltinKind::ToStringIdentity) { - "to_string_identity" - } else { - "to_string" - }) + Ok( + if matches!(kind, SpecializedBuiltinKind::ToStringIdentity) { + "to_string_identity" + } else { + "to_string" + }, + ) } SpecializedBuiltinKind::StringSplitLiteral => { let _ = frame.pop()?; @@ -3306,18 +3297,14 @@ fn emit_specialized_builtin_call( Ok(("regex_replace", out)) } SpecializedBuiltinKind::StringReplaceLiteralMany => { - let replacements = ensure_heap_ptr( - builder, block, ip, frame.pop()?, ValueType::Array, - )?; - let needles = ensure_heap_ptr( - builder, block, ip, frame.pop()?, ValueType::Array, - )?; - let text = ensure_heap_ptr( - builder, block, ip, frame.pop()?, ValueType::String, - )?; + let replacements = ensure_heap_ptr(builder, block, ip, frame.pop()?, ValueType::Array)?; + let needles = ensure_heap_ptr(builder, block, ip, frame.pop()?, ValueType::Array)?; + let text = ensure_heap_ptr(builder, block, ip, frame.pop()?, ValueType::String)?; let value = builder .append_value_inst( - block, ip, SsaValueRepr::Tagged, + block, + ip, + SsaValueRepr::Tagged, SsaInstKind::StringReplaceLiteralMany { text: text.value.id, needles: needles.value.id, @@ -3325,7 +3312,8 @@ fn emit_specialized_builtin_call( }, ) .map(|value| SymbolicValue { - value, info: ValueInfo::tagged_typed(ValueType::String), + value, + info: ValueInfo::tagged_typed(ValueType::String), }) .map_err(|err| TraceRecordError::InvalidIr(err.to_string()))?; Ok(("string_replace_literal_many", value)) diff --git a/src/vm/native/bridge.rs b/src/vm/native/bridge.rs index f63fd680..abbdfbd1 100644 --- a/src/vm/native/bridge.rs +++ b/src/vm/native/bridge.rs @@ -503,16 +503,25 @@ pub(crate) extern "C" fn pd_vm_native_string_replace_literal( } pub(crate) extern "C" fn pd_vm_native_string_replace_literal_many( - text_ptr: *mut u8, needles_ptr: *mut u8, replacements_ptr: *mut u8, + text_ptr: *mut u8, + needles_ptr: *mut u8, + replacements_ptr: *mut u8, ) -> *mut u8 { let text = unsafe { std::mem::ManuallyDrop::new(arc_from_repr_ptr::(text_ptr)) }; - let needles = unsafe { std::mem::ManuallyDrop::new(arc_from_repr_ptr::>(needles_ptr)) }; - let replacements = unsafe { std::mem::ManuallyDrop::new(arc_from_repr_ptr::>(replacements_ptr)) }; + let needles = + unsafe { std::mem::ManuallyDrop::new(arc_from_repr_ptr::>(needles_ptr)) }; + let replacements = + unsafe { std::mem::ManuallyDrop::new(arc_from_repr_ptr::>(replacements_ptr)) }; match crate::builtins::runtime::core::builtin_string_replace_literal_many_impl( - text.as_str(), needles.as_slice(), replacements.as_slice(), + text.as_str(), + needles.as_slice(), + replacements.as_slice(), ) { Ok(value) => arc_into_repr_ptr(Arc::new(value)), - Err(error) => { store_bridge_error(error); std::ptr::null_mut() } + Err(error) => { + store_bridge_error(error); + std::ptr::null_mut() + } } } diff --git a/src/vm/native/mod.rs b/src/vm/native/mod.rs index d3f3de41..d53553ae 100644 --- a/src/vm/native/mod.rs +++ b/src/vm/native/mod.rs @@ -22,10 +22,9 @@ pub(crate) use bridge::{ shared_bytes_from_buffer_entry_address, shared_string_from_buffer_entry_address, store_bridge_error, string_contains_entry_address, string_lower_ascii_entry_address, string_replace_literal_entry_address, string_replace_literal_many_entry_address, - string_split_literal_entry_address, take_bridge_error, - to_string_entry_address, type_of_entry_address, value_eq_entry_address, - value_len_entry_address, write_heap_value_to_slot_entry_address, - zero_bytes_entry_address, + string_split_literal_entry_address, take_bridge_error, to_string_entry_address, + type_of_entry_address, value_eq_entry_address, value_len_entry_address, + write_heap_value_to_slot_entry_address, zero_bytes_entry_address, }; #[cfg(feature = "cranelift-jit")] pub(crate) use codegen::{ diff --git a/tests/jit/jit_tests.rs b/tests/jit/jit_tests.rs index b9656717..1734c654 100644 --- a/tests/jit/jit_tests.rs +++ b/tests/jit/jit_tests.rs @@ -4715,7 +4715,10 @@ fn trace_jit_specializes_literal_replace_many() { let snapshot = vm.jit_snapshot(); assert!( snapshot.traces.iter().any(|trace| { - trace.op_names.iter().any(|name| name == "string_replace_literal_many") + trace + .op_names + .iter() + .any(|name| name == "string_replace_literal_many") }), "expected replace-many SSA specialization, got {}", vm.dump_jit_info() From b9bb9b9893523e9bf1f5a3d8e62690e14b89085b Mon Sep 17 00:00:00 2001 From: fffonion Date: Fri, 17 Jul 2026 16:47:01 +0800 Subject: [PATCH 6/7] fix(jit): propagate replace-many bridge failures --- src/builtins/runtime/core.rs | 6 +++++- src/vm/jit/native/lower.rs | 21 +++++++++++++++------ tests/jit/jit_tests.rs | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/builtins/runtime/core.rs b/src/builtins/runtime/core.rs index 54e4223c..4f78f9ae 100644 --- a/src/builtins/runtime/core.rs +++ b/src/builtins/runtime/core.rs @@ -792,7 +792,11 @@ pub(crate) fn builtin_string_replace_literal_impl( text.replace(needle, replacement) } -/// Apply ordered literal replacements from parallel needle/replacement arrays. +/// Apply literal replacements in order from parallel `needles` and `replacements` arrays. +/// +/// Each pair uses the same non-overlapping semantics as the scalar overload. Empty needles +/// leave the current output unchanged. Both arrays must have equal length and contain strings. +/// A length mismatch or a non-string element returns an error. #[pd_host_function(name = "string_replace_literal")] pub(crate) fn builtin_string_replace_literal_many_impl( text: VmStringRef<'_>, diff --git a/src/vm/jit/native/lower.rs b/src/vm/jit/native/lower.rs index a400bd65..fe4f6ef5 100644 --- a/src/vm/jit/native/lower.rs +++ b/src/vm/jit/native/lower.rs @@ -1839,12 +1839,6 @@ fn lower_ssa_inst( let text = values[text]; let needles = values[needles]; let replacements = values[replacements]; - let out = owned_value_temp_slot_addr( - b, - pointer_type, - owned_value_temps, - SsaTempValueSlotKey::Output(output.id), - )?; let out_raw = ssa_call_string_replace_literal_many( b, pointer_type, @@ -1854,6 +1848,20 @@ fn lower_ssa_inst( needles, replacements, )?; + let error = b.ins().icmp_imm(IntCC::Equal, out_raw, 0); + let failed = b.create_block(); + let replaced = b.create_block(); + b.ins().brif(error, failed, &[], replaced, &[]); + b.switch_to_block(failed); + let status = b.ins().iconst(types::I32, STATUS_ERROR as i64); + jump_with_status(b, exit_block, status); + b.switch_to_block(replaced); + let out = owned_value_temp_slot_addr( + b, + pointer_type, + owned_value_temps, + SsaTempValueSlotKey::Output(output.id), + )?; clear_owned_value_temp_slot(b, pointer_type, helper_refs, helper_addrs, out)?; ssa_store_heap_ptr_in_value(b, layout.value, out, layout.value.string_tag, out_raw); out @@ -3979,6 +3987,7 @@ fn ssa_call_regex_match( Ok(b.inst_results(call)[0]) } +#[allow(clippy::too_many_arguments)] fn ssa_call_regex_replace( b: &mut FunctionBuilder, pointer_type: cranelift_codegen::ir::Type, diff --git a/tests/jit/jit_tests.rs b/tests/jit/jit_tests.rs index 1734c654..cf24260e 100644 --- a/tests/jit/jit_tests.rs +++ b/tests/jit/jit_tests.rs @@ -4858,3 +4858,39 @@ fn trace_jit_specializes_regex_builtins_without_call_boundary() { assert_eq!(vm.regex_cache_compile_count(), 2); assert!(vm.regex_cache_hit_count() >= 14); } + +#[test] +fn trace_jit_propagates_replace_many_length_mismatch() { + if !native_jit_supported() { + return; + } + let source = r#" + let needles = ["a"]; + let replacement_sets: [[string]] = [["x"], ["x"], ["x"], ["x"], ["x"], []]; + let mut replacements: [string] = ["x"]; + let mut i = 0; + let mut out = ""; + while i < 6 { + replacements = (&replacement_sets)[i]; + out = string_replace_literal("a", &needles, &replacements); + i = i + 1; + } + out; + "#; + let compiled = compile_source(source).expect("replace-many mismatch fixture should compile"); + let mut vm = Vm::new_with_jit_config( + compiled.program.with_local_count(compiled.locals), + JitConfig { + enabled: true, + hot_loop_threshold: 1, + max_trace_len: 512, + }, + ); + let error = vm + .run() + .expect_err("replace-many mismatch should propagate its host error"); + assert_eq!( + error.to_string(), + "host error: string_replace_literal array lengths must match" + ); +} From 084861003b302597ce44922d4deab229a505812f Mon Sep 17 00:00:00 2001 From: fffonion Date: Fri, 17 Jul 2026 17:45:14 +0800 Subject: [PATCH 7/7] revert(strings): remove replacement-array overload --- README.md | 3 ++ src/builtins/runtime/core.rs | 32 -------------- src/builtins/runtime/mod.rs | 11 +---- src/vm/jit/ir.rs | 17 -------- src/vm/jit/native/lower.rs | 66 ++-------------------------- src/vm/jit/recorder.rs | 61 ++++---------------------- src/vm/native/bridge.rs | 27 ------------ src/vm/native/mod.rs | 7 ++- stdlib/tests/strings.rss | 4 -- tests/jit/jit_tests.rs | 83 ------------------------------------ 10 files changed, 21 insertions(+), 290 deletions(-) diff --git a/README.md b/README.md index 37bf8ccc..9ec5dcbd 100644 --- a/README.md +++ b/README.md @@ -761,6 +761,9 @@ frontend/source patterns may still reach a `Bridge/Exit` path even when an SSA i | `len(map)` | Bridge/Exit | Inline | | `get(map)` | Bridge/Exit | Helper | | `has(map)` | Bridge/Exit | Helper | +| `string_contains`, `string_replace_literal`, `string_lower_ascii`, `string_split_literal` | Bridge/Exit | Helper | +| `re::match`, `re::replace` | Bridge/Exit | Helper | +| Dynamic `type`, `to_string`, equality, and `len` | Bridge/Exit | Helper (known `type` / string `to_string` cases are folded) | | All other builtins | Bridge/Exit | Bridge/Exit or NYI, depending on trace shape | | Host imports | Bridge/Exit | Bridge/Exit or branch-exit trace, never inline | diff --git a/src/builtins/runtime/core.rs b/src/builtins/runtime/core.rs index 4f78f9ae..747cbe9d 100644 --- a/src/builtins/runtime/core.rs +++ b/src/builtins/runtime/core.rs @@ -792,38 +792,6 @@ pub(crate) fn builtin_string_replace_literal_impl( text.replace(needle, replacement) } -/// Apply literal replacements in order from parallel `needles` and `replacements` arrays. -/// -/// Each pair uses the same non-overlapping semantics as the scalar overload. Empty needles -/// leave the current output unchanged. Both arrays must have equal length and contain strings. -/// A length mismatch or a non-string element returns an error. -#[pd_host_function(name = "string_replace_literal")] -pub(crate) fn builtin_string_replace_literal_many_impl( - text: VmStringRef<'_>, - needles: VmArrayRef<'_>, - replacements: VmArrayRef<'_>, -) -> VmResult { - if needles.len() != replacements.len() { - return Err(VmError::HostError( - "string_replace_literal array lengths must match".to_string(), - )); - } - let mut out = text.to_string(); - for (needle, replacement) in needles.iter().zip(replacements.iter()) { - let (Value::String(needle), Value::String(replacement)) = (needle, replacement) else { - return Err(VmError::TypeMismatch("string arrays")); - }; - if needle.is_empty() || out.contains(needle.as_str()) { - out = builtin_string_replace_literal_impl( - out.as_str(), - needle.as_str(), - replacement.as_str(), - ); - } - } - Ok(out) -} - /// Lower ASCII `A`-`Z` bytes in `text` while preserving UTF-8. #[pd_host_function(name = "string_lower_ascii")] pub(crate) fn builtin_string_lower_ascii_impl(text: VmStringRef<'_>) -> String { diff --git a/src/builtins/runtime/mod.rs b/src/builtins/runtime/mod.rs index cff772fd..96f03d35 100644 --- a/src/builtins/runtime/mod.rs +++ b/src/builtins/runtime/mod.rs @@ -76,15 +76,8 @@ pub(crate) fn execute_builtin_call( BuiltinFunction::MapIterClose => map_iter::close(vm, args).map(BuiltinCallOutcome::Return), BuiltinFunction::StringContains => core::builtin_string_contains(args) .map(IntoBuiltinCallOutcome::into_builtin_call_outcome), - BuiltinFunction::StringReplaceLiteral => { - if matches!(args, [Value::String(_), Value::Array(_), Value::Array(_)]) { - core::builtin_string_replace_literal_many(args) - .map(IntoBuiltinCallOutcome::into_builtin_call_outcome) - } else { - core::builtin_string_replace_literal(args) - .map(IntoBuiltinCallOutcome::into_builtin_call_outcome) - } - } + BuiltinFunction::StringReplaceLiteral => core::builtin_string_replace_literal(args) + .map(IntoBuiltinCallOutcome::into_builtin_call_outcome), BuiltinFunction::StringLowerAscii => core::builtin_string_lower_ascii(args) .map(IntoBuiltinCallOutcome::into_builtin_call_outcome), BuiltinFunction::StringSplitLiteral => core::builtin_string_split_literal(args) diff --git a/src/vm/jit/ir.rs b/src/vm/jit/ir.rs index fb090cd2..498db899 100644 --- a/src/vm/jit/ir.rs +++ b/src/vm/jit/ir.rs @@ -156,11 +156,6 @@ pub(crate) enum SsaInstKind { needle: SsaValueId, replacement: SsaValueId, }, - StringReplaceLiteralMany { - text: SsaValueId, - needles: SsaValueId, - replacements: SsaValueId, - }, StringLowerAscii { text: SsaValueId, }, @@ -418,11 +413,6 @@ impl SsaInstKind { needle, replacement, } => vec![*text, *needle, *replacement], - Self::StringReplaceLiteralMany { - text, - needles, - replacements, - } => vec![*text, *needles, *replacements], Self::StringLowerAscii { text } => vec![*text], Self::TypeOf { value } | Self::ToString { value } => vec![*value], Self::StringSplitLiteral { text, delimiter } => vec![*text, *delimiter], @@ -1050,13 +1040,6 @@ fn render_inst_kind(kind: &SsaInstKind) -> String { } => { format!("string_replace_literal {text}, {needle}, {replacement}") } - SsaInstKind::StringReplaceLiteralMany { - text, - needles, - replacements, - } => { - format!("string_replace_literal_many {text}, {needles}, {replacements}") - } SsaInstKind::StringLowerAscii { text } => format!("string_lower_ascii {text}"), SsaInstKind::TypeOf { value } => format!("type_of {value}"), SsaInstKind::ToString { value } => format!("to_string {value}"), diff --git a/src/vm/jit/native/lower.rs b/src/vm/jit/native/lower.rs index fe4f6ef5..5185cebb 100644 --- a/src/vm/jit/native/lower.rs +++ b/src/vm/jit/native/lower.rs @@ -26,11 +26,10 @@ use crate::vm::native::{ shared_string_from_buffer_entry_address, sparse_restore_exit_signature, string_binary_transform_signature, string_contains_entry_address, string_contains_signature, string_lower_ascii_entry_address, string_replace_literal_entry_address, - string_replace_literal_many_entry_address, string_replace_signature, - string_split_literal_entry_address, string_unary_transform_signature, to_string_entry_address, - type_of_entry_address, value_eq_entry_address, value_eq_signature, value_len_entry_address, - value_len_signature, value_slot_signature, write_heap_value_to_slot_entry_address, - zero_bytes_entry_address, + string_replace_signature, string_split_literal_entry_address, string_unary_transform_signature, + to_string_entry_address, type_of_entry_address, value_eq_entry_address, value_eq_signature, + value_len_entry_address, value_len_signature, value_slot_signature, + write_heap_value_to_slot_entry_address, zero_bytes_entry_address, }; use cranelift_codegen::ir::condcodes::{FloatCC, IntCC}; use cranelift_codegen::ir::immediates::Ieee64; @@ -171,7 +170,6 @@ fn try_compile_ssa_trace( regex_match: regex_match_entry_address(), regex_replace: regex_replace_entry_address(), replace_literal: string_replace_literal_entry_address(), - replace_literal_many: string_replace_literal_many_entry_address(), lower_ascii: string_lower_ascii_entry_address(), type_of: type_of_entry_address(), to_string: to_string_entry_address(), @@ -501,7 +499,6 @@ struct SsaStringHelperAddrs { regex_match: usize, regex_replace: usize, replace_literal: usize, - replace_literal_many: usize, lower_ascii: usize, type_of: usize, to_string: usize, @@ -593,7 +590,6 @@ fn ssa_trace_supported(ssa: &SsaTrace) -> bool { | SsaInstKind::RegexMatch { .. } | SsaInstKind::RegexReplace { .. } | SsaInstKind::StringReplaceLiteral { .. } - | SsaInstKind::StringReplaceLiteralMany { .. } | SsaInstKind::StringLowerAscii { .. } | SsaInstKind::TypeOf { .. } | SsaInstKind::ToString { .. } @@ -877,7 +873,6 @@ fn ssa_inst_requires_owned_value_slot(kind: &SsaInstKind) -> bool { | SsaInstKind::StringGet { .. } | SsaInstKind::RegexReplace { .. } | SsaInstKind::StringReplaceLiteral { .. } - | SsaInstKind::StringReplaceLiteralMany { .. } | SsaInstKind::StringLowerAscii { .. } | SsaInstKind::TypeOf { .. } | SsaInstKind::ToString { .. } @@ -1831,41 +1826,6 @@ fn lower_ssa_inst( ssa_store_heap_ptr_in_value(b, layout.value, out, layout.value.string_tag, out_raw); out } - SsaInstKind::StringReplaceLiteralMany { - text, - needles, - replacements, - } => { - let text = values[text]; - let needles = values[needles]; - let replacements = values[replacements]; - let out_raw = ssa_call_string_replace_literal_many( - b, - pointer_type, - string_refs, - string_addrs, - text, - needles, - replacements, - )?; - let error = b.ins().icmp_imm(IntCC::Equal, out_raw, 0); - let failed = b.create_block(); - let replaced = b.create_block(); - b.ins().brif(error, failed, &[], replaced, &[]); - b.switch_to_block(failed); - let status = b.ins().iconst(types::I32, STATUS_ERROR as i64); - jump_with_status(b, exit_block, status); - b.switch_to_block(replaced); - let out = owned_value_temp_slot_addr( - b, - pointer_type, - owned_value_temps, - SsaTempValueSlotKey::Output(output.id), - )?; - clear_owned_value_temp_slot(b, pointer_type, helper_refs, helper_addrs, out)?; - ssa_store_heap_ptr_in_value(b, layout.value, out, layout.value.string_tag, out_raw); - out - } SsaInstKind::StringReplaceLiteral { text, needle, @@ -4025,24 +3985,6 @@ fn ssa_call_string_replace_literal( Ok(b.inst_results(call)[0]) } -fn ssa_call_string_replace_literal_many( - b: &mut FunctionBuilder, - pointer_type: cranelift_codegen::ir::Type, - string_refs: SsaStringHelperRefs, - string_addrs: SsaStringHelperAddrs, - text: cranelift_codegen::ir::Value, - needles: cranelift_codegen::ir::Value, - replacements: cranelift_codegen::ir::Value, -) -> VmResult { - let helper_ptr = iconst_ptr_from_addr(b, pointer_type, string_addrs.replace_literal_many)?; - let call = b.ins().call_indirect( - string_refs.replace_ref, - helper_ptr, - &[text, needles, replacements], - ); - Ok(b.inst_results(call)[0]) -} - fn ssa_call_string_lower_ascii( b: &mut FunctionBuilder, pointer_type: cranelift_codegen::ir::Type, diff --git a/src/vm/jit/recorder.rs b/src/vm/jit/recorder.rs index 56b664a7..7bcdea6b 100644 --- a/src/vm/jit/recorder.rs +++ b/src/vm/jit/recorder.rs @@ -482,7 +482,6 @@ enum SpecializedBuiltinKind { RegexMatch, RegexReplace, StringReplaceLiteral, - StringReplaceLiteralMany, StringLowerAscii, TypeOf, TypeOfKnown(ValueType), @@ -1154,26 +1153,13 @@ pub(crate) fn record_trace( .iter() .all(|arg| arg.info.source_local != Some(local)) }); - let replace_many = builtin == BuiltinFunction::StringReplaceLiteral - && (operand_types(program, ip).1 == ValueType::Array - || (args.get(1).is_some_and(|arg| { - observed_heap_container_kind(arg.info) - == Some(HeapContainerKind::Array) - }) && args.get(2).is_some_and(|arg| { - observed_heap_container_kind(arg.info) - == Some(HeapContainerKind::Array) - }))); - let specialized_kind = if replace_many { - Some(SpecializedBuiltinKind::StringReplaceLiteralMany) - } else { - select_specialized_builtin_kind( - program, - ip, - builtin, - args[0].info, - container_was_moved, - ) - }; + let specialized_kind = select_specialized_builtin_kind( + program, + ip, + builtin, + args[0].info, + container_was_moved, + ); if let Some(kind) = specialized_kind { let (name, out) = emit_specialized_builtin_call( &mut builder, @@ -2847,19 +2833,12 @@ fn analyze_specialized_builtin_call( frame.push(ValueInfo::tagged_typed(ValueType::String)); Ok("regex_replace") } - SpecializedBuiltinKind::StringReplaceLiteral - | SpecializedBuiltinKind::StringReplaceLiteralMany => { + SpecializedBuiltinKind::StringReplaceLiteral => { let _ = frame.pop()?; let _ = frame.pop()?; let _ = frame.pop()?; frame.push(ValueInfo::tagged_typed(ValueType::String)); - Ok( - if matches!(kind, SpecializedBuiltinKind::StringReplaceLiteralMany) { - "string_replace_literal_many" - } else { - "string_replace_literal" - }, - ) + Ok("string_replace_literal") } SpecializedBuiltinKind::StringLowerAscii => { let _ = frame.pop()?; @@ -3296,28 +3275,6 @@ fn emit_specialized_builtin_call( .map_err(|err| TraceRecordError::InvalidIr(err.to_string()))?; Ok(("regex_replace", out)) } - SpecializedBuiltinKind::StringReplaceLiteralMany => { - let replacements = ensure_heap_ptr(builder, block, ip, frame.pop()?, ValueType::Array)?; - let needles = ensure_heap_ptr(builder, block, ip, frame.pop()?, ValueType::Array)?; - let text = ensure_heap_ptr(builder, block, ip, frame.pop()?, ValueType::String)?; - let value = builder - .append_value_inst( - block, - ip, - SsaValueRepr::Tagged, - SsaInstKind::StringReplaceLiteralMany { - text: text.value.id, - needles: needles.value.id, - replacements: replacements.value.id, - }, - ) - .map(|value| SymbolicValue { - value, - info: ValueInfo::tagged_typed(ValueType::String), - }) - .map_err(|err| TraceRecordError::InvalidIr(err.to_string()))?; - Ok(("string_replace_literal_many", value)) - } SpecializedBuiltinKind::StringReplaceLiteral => { let replacement = ensure_heap_ptr( builder, diff --git a/src/vm/native/bridge.rs b/src/vm/native/bridge.rs index abbdfbd1..a9816d36 100644 --- a/src/vm/native/bridge.rs +++ b/src/vm/native/bridge.rs @@ -215,10 +215,6 @@ pub(crate) fn string_replace_literal_entry_address() -> usize { pd_vm_native_string_replace_literal as *const () as usize } -pub(crate) fn string_replace_literal_many_entry_address() -> usize { - pd_vm_native_string_replace_literal_many as *const () as usize -} - pub(crate) fn string_lower_ascii_entry_address() -> usize { pd_vm_native_string_lower_ascii as *const () as usize } @@ -502,29 +498,6 @@ pub(crate) extern "C" fn pd_vm_native_string_replace_literal( )) } -pub(crate) extern "C" fn pd_vm_native_string_replace_literal_many( - text_ptr: *mut u8, - needles_ptr: *mut u8, - replacements_ptr: *mut u8, -) -> *mut u8 { - let text = unsafe { std::mem::ManuallyDrop::new(arc_from_repr_ptr::(text_ptr)) }; - let needles = - unsafe { std::mem::ManuallyDrop::new(arc_from_repr_ptr::>(needles_ptr)) }; - let replacements = - unsafe { std::mem::ManuallyDrop::new(arc_from_repr_ptr::>(replacements_ptr)) }; - match crate::builtins::runtime::core::builtin_string_replace_literal_many_impl( - text.as_str(), - needles.as_slice(), - replacements.as_slice(), - ) { - Ok(value) => arc_into_repr_ptr(Arc::new(value)), - Err(error) => { - store_bridge_error(error); - std::ptr::null_mut() - } - } -} - pub(crate) extern "C" fn pd_vm_native_string_lower_ascii(text_ptr: *mut u8) -> *mut u8 { let text = unsafe { std::mem::ManuallyDrop::new(arc_from_repr_ptr::(text_ptr)) }; arc_into_repr_ptr(Arc::new( diff --git a/src/vm/native/mod.rs b/src/vm/native/mod.rs index d53553ae..0501cd92 100644 --- a/src/vm/native/mod.rs +++ b/src/vm/native/mod.rs @@ -21,10 +21,9 @@ pub(crate) use bridge::{ restore_sparse_exit_state_entry_address, shared_array_from_buffer_entry_address, shared_bytes_from_buffer_entry_address, shared_string_from_buffer_entry_address, store_bridge_error, string_contains_entry_address, string_lower_ascii_entry_address, - string_replace_literal_entry_address, string_replace_literal_many_entry_address, - string_split_literal_entry_address, take_bridge_error, to_string_entry_address, - type_of_entry_address, value_eq_entry_address, value_len_entry_address, - write_heap_value_to_slot_entry_address, zero_bytes_entry_address, + string_replace_literal_entry_address, string_split_literal_entry_address, take_bridge_error, + to_string_entry_address, type_of_entry_address, value_eq_entry_address, + value_len_entry_address, write_heap_value_to_slot_entry_address, zero_bytes_entry_address, }; #[cfg(feature = "cranelift-jit")] pub(crate) use codegen::{ diff --git a/stdlib/tests/strings.rss b/stdlib/tests/strings.rss index f21db4a3..ef3d35c9 100644 --- a/stdlib/tests/strings.rss +++ b/stdlib/tests/strings.rss @@ -10,10 +10,6 @@ assert(contains("hello", "ell")); assert(contains("hello", "zzz") == false); assert(equals(trim(" rustscript\n"), "rustscript")); assert(equals(replace("go gopher go", "go", "rs"), "rs rspher rs")); -assert(equals( - string_replace_literal("%27x%20y+z", ["%27", "%20", "+"], ["'", " ", " "]), - "'x y z" -)); let mut parts = split("a,b,c", ","); assert(parts.length == 3); diff --git a/tests/jit/jit_tests.rs b/tests/jit/jit_tests.rs index cf24260e..b2f043ea 100644 --- a/tests/jit/jit_tests.rs +++ b/tests/jit/jit_tests.rs @@ -4678,53 +4678,6 @@ fn trace_jit_specializes_loop_carried_string_builtins() { ); } -#[test] -fn trace_jit_specializes_literal_replace_many() { - if !native_jit_supported() { - return; - } - let source = r#" - let mut i = 0; - let mut out = ""; - let needles = ["%27", "%20", "+"]; - let replacements = ["'", " ", " "]; - while i < 8 { - out = string_replace_literal( - "%27x%20y+z", - &needles, - &replacements - ); - i = i + 1; - } - out == "'x y z"; - "#; - let program = compile_source(source).expect("replace-many fixture should compile"); - let mut vm = Vm::new_with_jit_config( - program.program, - JitConfig { - enabled: true, - hot_loop_threshold: 1, - max_trace_len: 512, - }, - ); - assert_eq!( - vm.run().expect("replace-many fixture should run"), - VmStatus::Halted - ); - assert_eq!(vm.stack(), &[Value::Bool(true)]); - let snapshot = vm.jit_snapshot(); - assert!( - snapshot.traces.iter().any(|trace| { - trace - .op_names - .iter() - .any(|name| name == "string_replace_literal_many") - }), - "expected replace-many SSA specialization, got {}", - vm.dump_jit_info() - ); -} - #[test] fn trace_jit_preserves_dynamic_concat_type_guards() { if !native_jit_supported() { @@ -4858,39 +4811,3 @@ fn trace_jit_specializes_regex_builtins_without_call_boundary() { assert_eq!(vm.regex_cache_compile_count(), 2); assert!(vm.regex_cache_hit_count() >= 14); } - -#[test] -fn trace_jit_propagates_replace_many_length_mismatch() { - if !native_jit_supported() { - return; - } - let source = r#" - let needles = ["a"]; - let replacement_sets: [[string]] = [["x"], ["x"], ["x"], ["x"], ["x"], []]; - let mut replacements: [string] = ["x"]; - let mut i = 0; - let mut out = ""; - while i < 6 { - replacements = (&replacement_sets)[i]; - out = string_replace_literal("a", &needles, &replacements); - i = i + 1; - } - out; - "#; - let compiled = compile_source(source).expect("replace-many mismatch fixture should compile"); - let mut vm = Vm::new_with_jit_config( - compiled.program.with_local_count(compiled.locals), - JitConfig { - enabled: true, - hot_loop_threshold: 1, - max_trace_len: 512, - }, - ); - let error = vm - .run() - .expect_err("replace-many mismatch should propagate its host error"); - assert_eq!( - error.to_string(), - "host error: string_replace_literal array lengths must match" - ); -}