Skip to content

Commit 320419b

Browse files
olwangclaude
andcommitted
feat(reg-vm): native-call ABI slice 4 — mutual recursion end-to-end
Wires the vm-jit group-compilation core (91a0b41) into reg_vm so mutually- recursive scalar-Int functions run natively from source. is_even/is_odd now execute on Cranelift via co-compiled CallGroup, byte-identical to the interpreter. - `native_recursive_int_group`: the call-graph SCC containing a function, if it is a cycle of >= 2 all-scalar-Int members whose every CallKnown targets a member. Refactored the self-recursion candidate into the group-aware `compute_recursive_int_member_inner` (self = a 1-member group). - `translate_to_native_jit_with_calls` gains `group_call_sites` (ip -> group index): a CallKnown to a group member lowers to `JitInstr::CallGroup`; group members use re-run-from-top deopt (precise_resume_safe forced off). - `try_native_mutual_recursive_int`: detect the group, translate each member with its group_call_sites, `compile_recursive_group` once, cache every member's CompiledId (NativeState.mutual_recursive_native), and dispatch the called member. Hooked in the exec.rs CallKnown handler after the self-recursive check; a deopt (incl. the entry depth-cap bail) falls back to the interpreter. INT-only, like self-recursion: Bool-returning mutual recursion declines to native (tco_leaves_mutual_recursion_untouched stays interpreter-only). Verified: differential 33/0, jit_acceptance 87/0 (new native_mutual_recursion_runs_native_and_matches_interpreter), vm-jit 83/0, default workspace build clean. Completes the native-call ABI (slices 1-4). Self-recursion perf: fib(32) 11.44x. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 91a0b41 commit 320419b

5 files changed

Lines changed: 288 additions & 8 deletions

File tree

crates/rsscript/src/reg_vm/exec.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1319,6 +1319,19 @@ impl RegVm {
13191319
self.set_reg(base + *dst, value);
13201320
continue;
13211321
}
1322+
// Native mutual recursion (native-call-ABI slice 4): a
1323+
// member of a mutually-recursive scalar-Int cycle runs via
1324+
// the co-compiled native group; deep recursion falls back.
1325+
#[cfg(feature = "native-jit")]
1326+
if self.jit_enabled
1327+
&& mut_args.is_empty()
1328+
&& self.limits.mem_budget.is_none()
1329+
&& let Some(value) =
1330+
self.try_native_mutual_recursive_int(unit, *callee_id, base, args)
1331+
{
1332+
self.set_reg(base + *dst, value);
1333+
continue;
1334+
}
13221335
let callee = Rc::clone(&unit.functions[*callee_id]);
13231336
self.prepare_frame(next_base, callee.regs)?;
13241337
for (index, reg) in args.iter().enumerate() {

crates/rsscript/src/reg_vm/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2183,6 +2183,11 @@ struct NativeState {
21832183
/// (`*const RegFunction` key) compiled `CallSelf` entry. `None` = known not
21842184
/// natively self-recursion-compilable (fall back to the tier-0 scalar executor).
21852185
self_recursive_native: HashMap<usize, Option<vm_jit::CompiledId>>,
2186+
/// Native mutual-recursion cache (native-call-ABI slice 4): per-function
2187+
/// (`*const RegFunction` key) compiled group-member `CompiledId`. Compiling any
2188+
/// member of a recursive cycle compiles+caches the whole group. `None` = known
2189+
/// not a natively-compilable mutual-recursion member (fall back to interpreter).
2190+
mutual_recursive_native: HashMap<usize, Option<vm_jit::CompiledId>>,
21862191
/// Reusable per-call marshalling scratch buffers (TV2 arg/len words and the
21872192
/// flat-list `Rc` keep-alive set). Held here and `mem::take`n into the call
21882193
/// frame so a hot per-iteration native dispatch (e.g. a tiny leaf/closure
@@ -5730,6 +5735,7 @@ impl NativeState {
57305735
osr_enabled,
57315736
osr_cache: HashMap::new(),
57325737
self_recursive_native: HashMap::new(),
5738+
mutual_recursive_native: HashMap::new(),
57335739
scratch_args: Vec::new(),
57345740
scratch_lens: Vec::new(),
57355741
scratch_flat_owned: Vec::new(),

crates/rsscript/src/reg_vm/native/translate.rs

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,20 +168,24 @@ pub(in crate::reg_vm) fn translate_to_native_jit_with_compiled_callees(
168168
Vec<Rc<String>>,
169169
bool,
170170
)> {
171-
translate_to_native_jit_with_calls(unit, func, compiled_callees, &HashSet::new())
171+
translate_to_native_jit_with_calls(unit, func, compiled_callees, &HashSet::new(), &HashMap::new())
172172
}
173173

174174
/// Like [`translate_to_native_jit_with_compiled_callees`], but `self_call_sites`
175175
/// names the original ips of `CallKnown` instructions that call `func` itself —
176176
/// emitted as `JitInstr::CallSelf` for native self-recursion (native-call-ABI
177-
/// slice 3). Such functions use re-run-from-top deopt (`precise_resume_safe` forced
178-
/// off), so a bail anywhere in the recursion unwinds to the interpreter.
177+
/// slice 3) — and `group_call_sites` maps the original ip of a `CallKnown` to a
178+
/// *mutually-recursive group member* to that member's group index, emitted as
179+
/// `JitInstr::CallGroup` (slice 4). Such functions use re-run-from-top deopt
180+
/// (`precise_resume_safe` forced off), so a bail anywhere in the recursion unwinds
181+
/// to the interpreter.
179182
#[cfg(feature = "native-jit")]
180183
pub(in crate::reg_vm) fn translate_to_native_jit_with_calls(
181184
unit: &RegUnit,
182185
func: &RegFunction,
183186
compiled_callees: &HashMap<usize, NativeCompiledCallee>,
184187
self_call_sites: &HashSet<usize>,
188+
group_call_sites: &HashMap<usize, u32>,
185189
) -> Option<(
186190
vm_jit::JitFunction,
187191
NativeTy,
@@ -202,6 +206,7 @@ pub(in crate::reg_vm) fn translate_to_native_jit_with_calls(
202206
.keys()
203207
.copied()
204208
.chain(self_call_sites.iter().copied())
209+
.chain(group_call_sites.keys().copied())
205210
.collect();
206211
let (code, n_regs, mut ip_map) = native_inline_leaf_calls_preserving_known_calls(
207212
unit,
@@ -255,7 +260,8 @@ pub(in crate::reg_vm) fn translate_to_native_jit_with_calls(
255260
for (i, instr) in code.iter().enumerate() {
256261
let compiled_call = matches!(instr, RegInstr::CallKnown { .. })
257262
&& (compiled_callees.contains_key(&ip_map[i])
258-
|| self_call_sites.contains(&ip_map[i]));
263+
|| self_call_sites.contains(&ip_map[i])
264+
|| group_call_sites.contains_key(&ip_map[i]));
259265
if reachable[i] && !compiled_call && !native_subset_instruction(instr) {
260266
return None;
261267
}
@@ -338,6 +344,17 @@ pub(in crate::reg_vm) fn translate_to_native_jit_with_calls(
338344
}
339345
ok && native_set_ty(ty, *dst, NativeTy::Int, c)
340346
}
347+
RegInstr::CallKnown {
348+
dst, args, mut_args, ..
349+
} if group_call_sites.contains_key(&ip_map[i]) => {
350+
// Mutually-recursive group call (native-call-ABI slice 4): the
351+
// mutual-recursive int group is all-scalar-Int, so args/result Int.
352+
let mut ok = mut_args.is_empty();
353+
for arg in args {
354+
ok = ok && native_set_ty(ty, *arg, NativeTy::Int, c);
355+
}
356+
ok && native_set_ty(ty, *dst, NativeTy::Int, c)
357+
}
341358
RegInstr::CallKnown {
342359
dst,
343360
args,
@@ -922,6 +939,26 @@ pub(in crate::reg_vm) fn translate_to_native_jit_with_calls(
922939
args: args.iter().map(|arg| r(*arg)).collect(),
923940
}
924941
}
942+
RegInstr::CallKnown {
943+
dst,
944+
args,
945+
mut_args,
946+
..
947+
} if group_call_sites.contains_key(&ip_map[i]) => {
948+
// Mutually-recursive native call (native-call-ABI slice 4): lower to
949+
// `CallGroup` targeting the member's group index. Scalar params only.
950+
let group_index = group_call_sites[&ip_map[i]];
951+
require(mut_args.is_empty())?;
952+
require(ty[*dst].is_some())?;
953+
for arg in args {
954+
require(ty[*arg].is_some())?;
955+
}
956+
JitInstr::CallGroup {
957+
group_index,
958+
dst: r(*dst),
959+
args: args.iter().map(|arg| r(*arg)).collect(),
960+
}
961+
}
925962
RegInstr::CallKnown {
926963
dst,
927964
args,
@@ -1696,6 +1733,7 @@ pub(in crate::reg_vm) fn translate_to_native_jit_with_calls(
16961733
// non-chaining and its native frame chain has no bounded deopt payload), so it is
16971734
// never precise-resumable regardless of ip-map identity.
16981735
let precise_resume_safe = self_call_sites.is_empty()
1736+
&& group_call_sites.is_empty()
16991737
&& n_regs == func.regs
17001738
&& ip_map.iter().enumerate().all(|(ip, &orig)| ip == orig);
17011739
Some((

crates/rsscript/src/reg_vm/tier.rs

Lines changed: 193 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2608,6 +2608,7 @@ impl RegVm {
26082608
func,
26092609
&std::collections::HashMap::new(),
26102610
&self_call_sites,
2611+
&std::collections::HashMap::new(),
26112612
)
26122613
.and_then(|(jit_fn, ret, _params, _literals, _precise)| {
26132614
// This scalar fast path returns an `Int`; other return
@@ -2647,6 +2648,135 @@ impl RegVm {
26472648
}
26482649
}
26492650

2651+
/// Native mutual recursion (native-call-ABI slice 4): if `function_id` is part of
2652+
/// a mutually-recursive scalar-`Int` cycle, compile the whole group together
2653+
/// (declare the cycle, then define each) and dispatch the called member natively.
2654+
/// Returns the result on a clean completion, or `None` to fall back to the
2655+
/// interpreter (not eligible, or a deopt incl. the entry depth-cap bail). The
2656+
/// group is compiled once and every member cached.
2657+
#[cfg(feature = "native-jit")]
2658+
pub(super) fn try_native_mutual_recursive_int(
2659+
&mut self,
2660+
unit: &RegUnit,
2661+
function_id: usize,
2662+
caller_base: usize,
2663+
args: &[usize],
2664+
) -> Option<VmValue> {
2665+
let func = unit.functions.get(function_id)?;
2666+
if args.len() != func.params {
2667+
return None;
2668+
}
2669+
let key = Rc::as_ptr(func) as usize;
2670+
let id = {
2671+
let native = self.native.as_mut()?;
2672+
if native.force_bail
2673+
|| native.forced_safepoint.is_some()
2674+
|| native.force_all_safepoints
2675+
{
2676+
return None;
2677+
}
2678+
if let Some(cached) = native.mutual_recursive_native.get(&key) {
2679+
(*cached)?
2680+
} else {
2681+
let group = native_recursive_int_group(unit, function_id);
2682+
let compiled = group.as_ref().and_then(|scc| {
2683+
let index_of: std::collections::HashMap<usize, u32> = scc
2684+
.iter()
2685+
.enumerate()
2686+
.map(|(i, &fid)| (fid, i as u32))
2687+
.collect();
2688+
let mut jit_funcs = Vec::with_capacity(scc.len());
2689+
for &member in scc {
2690+
let mfunc = unit.functions.get(member)?;
2691+
let group_call_sites: std::collections::HashMap<usize, u32> = mfunc
2692+
.code
2693+
.iter()
2694+
.enumerate()
2695+
.filter_map(|(ip, instr)| match instr {
2696+
RegInstr::CallKnown {
2697+
function, mut_args, ..
2698+
} if mut_args.is_empty() && index_of.contains_key(function) => {
2699+
Some((ip, index_of[function]))
2700+
}
2701+
_ => None,
2702+
})
2703+
.collect();
2704+
let (jit_fn, ret, _p, _l, _pr) = translate_to_native_jit_with_calls(
2705+
unit,
2706+
mfunc,
2707+
&std::collections::HashMap::new(),
2708+
&std::collections::HashSet::new(),
2709+
&group_call_sites,
2710+
)?;
2711+
if ret != NativeTy::Int {
2712+
return None;
2713+
}
2714+
jit_funcs.push(jit_fn);
2715+
}
2716+
native.module.compile_recursive_group(&jit_funcs).ok()
2717+
});
2718+
match (group, compiled) {
2719+
(Some(scc), Some(ids)) if ids.len() == scc.len() => {
2720+
let mut my_id = None;
2721+
for (i, &member) in scc.iter().enumerate() {
2722+
let mkey = Rc::as_ptr(&unit.functions[member]) as usize;
2723+
native.mutual_recursive_native.insert(mkey, Some(ids[i]));
2724+
if member == function_id {
2725+
my_id = Some(ids[i]);
2726+
}
2727+
}
2728+
my_id?
2729+
}
2730+
(group, _) => {
2731+
// Cache ineligibility (for the whole detected group, or this key).
2732+
match group {
2733+
Some(scc) => {
2734+
for member in scc {
2735+
let mkey = Rc::as_ptr(&unit.functions[member]) as usize;
2736+
native.mutual_recursive_native.insert(mkey, None);
2737+
}
2738+
}
2739+
None => {
2740+
native.mutual_recursive_native.insert(key, None);
2741+
}
2742+
}
2743+
return None;
2744+
}
2745+
}
2746+
}
2747+
};
2748+
let mut int_args = Vec::with_capacity(args.len());
2749+
for &arg in args {
2750+
let VmValue::Int(value) = self.reg(caller_base + arg) else {
2751+
return None;
2752+
};
2753+
int_args.push(*value);
2754+
}
2755+
let lens = vec![0i64; int_args.len()];
2756+
let mut heap_tx = JitNativeCallFrame::begin();
2757+
let outcome = {
2758+
let native = self.native.as_ref()?;
2759+
native
2760+
.module
2761+
.call_with_host_ctx(id, &int_args, &lens, heap_tx.host_ctx())
2762+
};
2763+
match outcome {
2764+
vm_jit::NativeOutcome::Completed(bits) => {
2765+
heap_tx.commit_scalar_with_writebacks(&[]);
2766+
if let Some(native) = self.native.as_mut()
2767+
&& native.collect_stats
2768+
{
2769+
native.stats.native_calls += 1;
2770+
}
2771+
Some(VmValue::Int(bits))
2772+
}
2773+
_ => {
2774+
heap_tx.abort();
2775+
None
2776+
}
2777+
}
2778+
}
2779+
26502780
pub(super) fn run_jit_self_recursive_int(
26512781
&mut self,
26522782
unit: &RegUnit,
@@ -2931,12 +3061,18 @@ fn propagate_same_kind(kinds: &mut [ScalarSlotKind], dst: usize, src: usize) ->
29313061
}
29323062

29333063
fn compute_self_recursive_int_jit_candidate(unit: &RegUnit, function_id: usize) -> bool {
2934-
compute_self_recursive_int_jit_candidate_inner(unit, function_id).unwrap_or(false)
3064+
let group: std::collections::HashSet<usize> = std::iter::once(function_id).collect();
3065+
compute_recursive_int_member_inner(unit, function_id, &group).unwrap_or(false)
29353066
}
29363067

2937-
fn compute_self_recursive_int_jit_candidate_inner(
3068+
/// Scalar-`Int` recursion analysis for one member of a recursive `group`: the body
3069+
/// must be all-scalar-`Int`, return `Int`, and every `CallKnown` must target a group
3070+
/// member (self for self-recursion, or any sibling for mutual recursion) with scalar
3071+
/// args and matching arity. `group = {function_id}` is the self-recursive case.
3072+
fn compute_recursive_int_member_inner(
29383073
unit: &RegUnit,
29393074
function_id: usize,
3075+
group: &std::collections::HashSet<usize>,
29403076
) -> Option<bool> {
29413077
let Some(func) = unit.functions.get(function_id) else {
29423078
return Some(false);
@@ -3011,9 +3147,9 @@ fn compute_self_recursive_int_jit_candidate_inner(
30113147
function,
30123148
args,
30133149
mut_args,
3014-
} if *function == function_id
3150+
} if group.contains(function)
30153151
&& mut_args.is_empty()
3016-
&& args.len() == func.params =>
3152+
&& unit.functions.get(*function).is_some_and(|f| f.params == args.len()) =>
30173153
{
30183154
saw_self_call = true;
30193155
let mut local_changed =
@@ -3049,6 +3185,59 @@ fn compute_self_recursive_int_jit_candidate_inner(
30493185
)
30503186
}
30513187

3188+
/// The mutually-recursive group (call-graph SCC) containing `function_id`, if it is
3189+
/// a cycle of >= 2 all-scalar-`Int` functions whose every `CallKnown` targets a
3190+
/// group member (native-call-ABI slice 4). Returned sorted; `None` for non-cyclic
3191+
/// functions and pure self-recursion (handled by the self-recursive path).
3192+
#[cfg(feature = "native-jit")]
3193+
fn native_recursive_int_group(unit: &RegUnit, function_id: usize) -> Option<Vec<usize>> {
3194+
use std::collections::HashSet;
3195+
let callees = |fid: usize| -> Vec<usize> {
3196+
unit.functions.get(fid).map_or_else(Vec::new, |f| {
3197+
f.code
3198+
.iter()
3199+
.filter_map(|instr| match instr {
3200+
RegInstr::CallKnown { function, .. } => Some(*function),
3201+
_ => None,
3202+
})
3203+
.collect()
3204+
})
3205+
};
3206+
// Forward-reachable from `function_id` via CallKnown edges.
3207+
let mut fwd = HashSet::new();
3208+
let mut stack = vec![function_id];
3209+
while let Some(f) = stack.pop() {
3210+
if fwd.insert(f) {
3211+
stack.extend(callees(f));
3212+
}
3213+
}
3214+
// Backward-reachable: functions that can transitively reach `function_id`.
3215+
let mut bwd = HashSet::new();
3216+
let mut stack = vec![function_id];
3217+
while let Some(target) = stack.pop() {
3218+
if bwd.insert(target) {
3219+
for caller in 0..unit.functions.len() {
3220+
if callees(caller).contains(&target) {
3221+
stack.push(caller);
3222+
}
3223+
}
3224+
}
3225+
}
3226+
// The SCC is the intersection (mutually reachable with `function_id`).
3227+
let mut scc: Vec<usize> = fwd.intersection(&bwd).copied().collect();
3228+
scc.sort_unstable();
3229+
if scc.len() < 2 {
3230+
return None;
3231+
}
3232+
let group: HashSet<usize> = scc.iter().copied().collect();
3233+
for &member in &scc {
3234+
if !compute_recursive_int_member_inner(unit, member, &group).unwrap_or(false) {
3235+
return None;
3236+
}
3237+
}
3238+
Some(scc)
3239+
}
3240+
30523241
fn scalar_reachable_instructions(code: &[RegInstr]) -> Vec<bool> {
30533242
let mut reachable = vec![false; code.len()];
30543243
let mut stack = vec![0usize];

0 commit comments

Comments
 (0)