From 3a46fe91d827e35b082ac4be4d74137348dbb20f Mon Sep 17 00:00:00 2001 From: fffonion Date: Tue, 21 Jul 2026 15:47:32 +0800 Subject: [PATCH] fix(jit): keep scalar to_string on tagged path --- src/vm/jit/recorder.rs | 42 +++++++++++++++++++++++++++++++++++++----- tests/jit/jit_tests.rs | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/src/vm/jit/recorder.rs b/src/vm/jit/recorder.rs index 642823d..b22a532 100644 --- a/src/vm/jit/recorder.rs +++ b/src/vm/jit/recorder.rs @@ -3274,11 +3274,11 @@ fn select_specialized_builtin_kind( }); } BuiltinFunction::ToString => { - return Some(if container.known_type == Some(ValueType::String) { - SpecializedBuiltinKind::ToStringIdentity - } else { - SpecializedBuiltinKind::ToString - }); + return match (container.known_type, container.repr) { + (Some(ValueType::String), _) => Some(SpecializedBuiltinKind::ToStringIdentity), + (_, SsaValueRepr::Tagged) => Some(SpecializedBuiltinKind::ToString), + _ => None, + }; } BuiltinFunction::StringSplitLiteral => { return Some(SpecializedBuiltinKind::StringSplitLiteral); @@ -5272,6 +5272,38 @@ mod tests { ); } + #[test] + fn scalar_to_string_stays_on_general_call_path() { + let program = Program::new(Vec::new(), Vec::new()); + for info in [ + ValueInfo::int(None), + ValueInfo::float(None), + ValueInfo::bool(None), + ] { + assert_eq!( + select_specialized_builtin_kind( + &program, + 0, + BuiltinFunction::ToString, + info, + false, + ), + None, + "unboxed scalar to_string needs a tagged Value slot" + ); + } + assert_eq!( + select_specialized_builtin_kind( + &program, + 0, + BuiltinFunction::ToString, + ValueInfo::tagged(), + false, + ), + Some(SpecializedBuiltinKind::ToString) + ); + } + #[test] fn unboxed_numeric_compare_ignores_stale_operand_type_hint() { let program = Program::new(Vec::new(), Vec::new()).with_type_map(crate::TypeMap { diff --git a/tests/jit/jit_tests.rs b/tests/jit/jit_tests.rs index af1f80e..d8b7d41 100644 --- a/tests/jit/jit_tests.rs +++ b/tests/jit/jit_tests.rs @@ -6100,6 +6100,43 @@ fn trace_jit_specializes_regex_builtins_without_call_boundary() { assert!(vm.regex_cache_hit_count() >= 14); } +#[test] +fn trace_jit_scalar_to_string_uses_safe_call_boundary() { + if !native_jit_supported() { + return; + } + let source = r#" + let mut i = 0; + let mut rendered = ""; + while i < 16 { + rendered = i + ""; + i = i + 1; + } + rendered; + "#; + let compiled = compile_source(source).expect("scalar to_string loop should compile"); + let mut vm = Vm::new(compiled.program.with_local_count(compiled.locals)); + vm.set_jit_native_direct_links_enabled(false); + vm.set_jit_config(JitConfig { + enabled: true, + hot_loop_threshold: 1, + max_trace_len: 512, + }); + + assert_eq!( + vm.run().expect("scalar to_string JIT should run"), + VmStatus::Halted + ); + assert_eq!(vm.stack(), &[Value::string("15")]); + let snapshot = vm.jit_snapshot(); + assert!(vm.jit_native_exec_count() > 0, "{}", vm.dump_jit_info()); + assert!( + !snapshot.traces.is_empty(), + "scalar to_string should leave surrounding loop traceable:\n{}", + vm.dump_jit_info() + ); +} + #[test] fn trace_jit_executes_hot_loop_inside_script_callable_frame() { if !native_jit_supported() {