Skip to content

Commit 6fdcfd8

Browse files
olwangclaude
andcommitted
feat(native-jit): generalize mutual recursion to scalar Float (Int/Bool/Float)
Applies the Phase-2 self-recursion treatment to the mutual-recursion group path, removing the last narrow-Int recursion remnant. Mutual recursion's fallback is already the full interpreter, so this is purely a widening — no fallback-selection logic. - native_recursive_int_group → native_recursive_group: keeps SCC/cycle discovery (>= 2 members) but DROPS the bespoke per-member Int/Bool analysis. Per-member eligibility is now the general translate_to_native_jit_with_calls verdict in the compile loop — so the group admits Int/Bool/Float bodies (and members that also call inlinable leaves), not just the Int-arith whitelist. - try_native_mutual_recursive_int: caches (CompiledId, param_tys, ret) per member (was (CompiledId, returns_bool)); accepts scalar Int/Bool/Float params+return (non-scalar declines); marshals each arg by its compiled NativeTy (Float via to_bits) and wraps the i64 result per ret (Float via from_bits) — identical to try_native_self_recursive. - mutual_recursive_native cache type updated accordingly. Verify (Docker dev): differential 33/0, runtime 396/0 (new native_mutual_recursion_float_runs_native + all 15 recursion tests green), reg_vm lib 31/0 default, default+native builds and clippy clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d54f2c5 commit 6fdcfd8

3 files changed

Lines changed: 102 additions & 53 deletions

File tree

crates/rsscript/src/reg_vm/mod.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2208,12 +2208,14 @@ struct NativeState {
22082208
/// not natively self-recursion-compilable (fall back to the tier-0 i64 executor
22092209
/// for i64-only bodies, or the full interpreter for non-i64 bodies).
22102210
self_recursive_native: HashMap<usize, Option<(vm_jit::CompiledId, Vec<NativeTy>, NativeTy)>>,
2211-
/// Native mutual-recursion cache (native-call-ABI slice 4): per-function
2212-
/// (`*const RegFunction` key) compiled group-member `(CompiledId, returns_bool)`,
2213-
/// where `returns_bool` wraps the native `i64` result as `Bool` vs `Int`.
2214-
/// Compiling any member of a recursive cycle compiles+caches the whole group.
2215-
/// `None` = known not a natively-compilable mutual-recursion member (interpreter).
2216-
mutual_recursive_native: HashMap<usize, Option<(vm_jit::CompiledId, bool)>>,
2211+
/// Native mutual-recursion cache (native-call-ABI slice 4; generalized to scalar
2212+
/// Float in the Phase 2 follow-up): per-function (`*const RegFunction` key)
2213+
/// compiled group-member `(CompiledId, param_tys, ret)`. The dispatcher marshals
2214+
/// each scalar arg (Int/Bool/Float) and wraps the `i64` result per `ret`, exactly
2215+
/// like the self-recursion cache. Compiling any member of a recursive cycle
2216+
/// compiles+caches the whole group. `None` = known not a natively-compilable
2217+
/// mutual-recursion member (interpreter).
2218+
mutual_recursive_native: HashMap<usize, Option<(vm_jit::CompiledId, Vec<NativeTy>, NativeTy)>>,
22172219
/// Reusable per-call marshalling scratch buffers (TV2 arg/len words and the
22182220
/// flat-list `Rc` keep-alive set). Held here and `mem::take`n into the call
22192221
/// frame so a hot per-iteration native dispatch (e.g. a tiny leaf/closure

crates/rsscript/src/reg_vm/tier.rs

Lines changed: 56 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2679,12 +2679,15 @@ impl RegVm {
26792679
}
26802680
}
26812681

2682-
/// Native mutual recursion (native-call-ABI slice 4): if `function_id` is part of
2683-
/// a mutually-recursive scalar-`Int` cycle, compile the whole group together
2684-
/// (declare the cycle, then define each) and dispatch the called member natively.
2685-
/// Returns the result on a clean completion, or `None` to fall back to the
2686-
/// interpreter (not eligible, or a deopt incl. the entry depth-cap bail). The
2687-
/// group is compiled once and every member cached.
2682+
/// Native mutual recursion (native-call-ABI slice 4; generalized to scalar Float):
2683+
/// if `function_id` is part of a mutually-recursive cycle of scalar functions,
2684+
/// compile the whole group together (declare the cycle, then define each) and
2685+
/// dispatch the called member natively. Arg/return marshalling follows each
2686+
/// member's compiled scalar parameter/return `NativeTy`s (Int/Bool/Float — Float
2687+
/// via `to_bits`/`from_bits`), exactly like `try_native_self_recursive`. Returns
2688+
/// the result on a clean completion, or `None` to fall back to the interpreter
2689+
/// (not eligible, non-scalar param/return, or a deopt incl. the depth-cap bail).
2690+
/// The group is compiled once and every member cached.
26882691
#[cfg(feature = "native-jit")]
26892692
pub(super) fn try_native_mutual_recursive_int(
26902693
&mut self,
@@ -2698,10 +2701,10 @@ impl RegVm {
26982701
return None;
26992702
}
27002703
let key = Rc::as_ptr(func) as usize;
2701-
// Resolve the called member's native id and its return kind (Int → wrap the
2702-
// result `i64` as `VmValue::Int`, Bool → as `VmValue::Bool`). Cached per
2703-
// member; compiling any member compiles the whole group.
2704-
let (id, returns_bool) = {
2704+
// Resolve the called member's native id, its parameter types (to marshal each
2705+
// scalar arg) and its return type (to wrap the i64 result). Cached per member;
2706+
// compiling any member compiles the whole group.
2707+
let (id, param_tys, ret) = {
27052708
let native = self.native.as_mut()?;
27062709
if native.force_bail
27072710
|| native.forced_safepoint.is_some()
@@ -2710,17 +2713,19 @@ impl RegVm {
27102713
return None;
27112714
}
27122715
if let Some(cached) = native.mutual_recursive_native.get(&key) {
2713-
(*cached)?
2716+
cached.clone()?
27142717
} else {
2715-
let group = native_recursive_int_group(unit, function_id);
2718+
let group = native_recursive_group(unit, function_id);
2719+
let is_scalar =
2720+
|t: &NativeTy| matches!(t, NativeTy::Int | NativeTy::Bool | NativeTy::Float);
27162721
let compiled = group.as_ref().and_then(|scc| {
27172722
let index_of: std::collections::HashMap<usize, u32> = scc
27182723
.iter()
27192724
.enumerate()
27202725
.map(|(i, &fid)| (fid, i as u32))
27212726
.collect();
27222727
let mut jit_funcs = Vec::with_capacity(scc.len());
2723-
let mut member_returns_bool = Vec::with_capacity(scc.len());
2728+
let mut member_sigs = Vec::with_capacity(scc.len());
27242729
for &member in scc {
27252730
let mfunc = unit.functions.get(member)?;
27262731
let group_call_sites: std::collections::HashMap<usize, u32> = mfunc
@@ -2736,32 +2741,34 @@ impl RegVm {
27362741
_ => None,
27372742
})
27382743
.collect();
2739-
let (jit_fn, ret, _p, _l, _pr) = translate_to_native_jit_with_calls(
2744+
let (jit_fn, ret, param_tys, _l, _pr) = translate_to_native_jit_with_calls(
27402745
unit,
27412746
mfunc,
27422747
&std::collections::HashMap::new(),
27432748
&std::collections::HashSet::new(),
27442749
&group_call_sites,
27452750
)?;
2746-
// i64-scalar returns only: Int or Bool (both wrap from i64).
2747-
let returns_bool = match ret {
2748-
NativeTy::Int => false,
2749-
NativeTy::Bool => true,
2750-
_ => return None,
2751-
};
2752-
member_returns_bool.push(returns_bool);
2751+
// Scalar-only ABI: params and return must be Int/Bool/Float
2752+
// (each wraps to/from an i64 slot). Heap params/returns decline.
2753+
if !is_scalar(&ret) || !param_tys.iter().all(is_scalar) {
2754+
return None;
2755+
}
2756+
member_sigs.push((param_tys, ret));
27532757
jit_funcs.push(jit_fn);
27542758
}
27552759
let ids = native.module.compile_recursive_group(&jit_funcs).ok()?;
2756-
Some((ids, member_returns_bool))
2760+
Some((ids, member_sigs))
27572761
});
27582762
match (group, compiled) {
2759-
(Some(scc), Some((ids, member_returns_bool))) if ids.len() == scc.len() => {
2763+
(Some(scc), Some((ids, member_sigs))) if ids.len() == scc.len() => {
27602764
let mut mine = None;
27612765
for (i, &member) in scc.iter().enumerate() {
27622766
let mkey = Rc::as_ptr(&unit.functions[member]) as usize;
2763-
let entry = (ids[i], member_returns_bool[i]);
2764-
native.mutual_recursive_native.insert(mkey, Some(entry));
2767+
let (param_tys, ret) = member_sigs[i].clone();
2768+
let entry = (ids[i], param_tys, ret);
2769+
native
2770+
.mutual_recursive_native
2771+
.insert(mkey, Some(entry.clone()));
27652772
if member == function_id {
27662773
mine = Some(entry);
27672774
}
@@ -2786,12 +2793,20 @@ impl RegVm {
27862793
}
27872794
}
27882795
};
2796+
if param_tys.len() != args.len() {
2797+
return None;
2798+
}
27892799
let mut int_args = Vec::with_capacity(args.len());
2790-
for &arg in args {
2791-
// Scalar value args marshal to `i64` (Bool as 0/1, like the native ABI).
2792-
let bits = match self.reg(caller_base + arg) {
2793-
VmValue::Int(value) => *value,
2794-
VmValue::Bool(value) => *value as i64,
2800+
for (&arg, pty) in args.iter().zip(param_tys.iter()) {
2801+
// Scalar value args marshal to an i64 slot, driven by the compiled
2802+
// parameter type (Float reinterpreted via `to_bits`; an Int value for a
2803+
// Float param converted first) — identical to the self-recursion path.
2804+
let bits = match (pty, self.reg(caller_base + arg)) {
2805+
(NativeTy::Float, VmValue::Float(f)) => f.to_bits() as i64,
2806+
(NativeTy::Float, VmValue::Int(i)) => (*i as f64).to_bits() as i64,
2807+
(_, VmValue::Int(i)) => *i,
2808+
(_, VmValue::Bool(b)) => i64::from(*b),
2809+
(_, VmValue::Float(f)) => f.to_bits() as i64,
27952810
_ => return None,
27962811
};
27972812
int_args.push(bits);
@@ -2812,10 +2827,10 @@ impl RegVm {
28122827
{
28132828
native.stats.native_calls += 1;
28142829
}
2815-
Some(if returns_bool {
2816-
VmValue::Bool(bits != 0)
2817-
} else {
2818-
VmValue::Int(bits)
2830+
Some(match ret {
2831+
NativeTy::Float => VmValue::Float(f64::from_bits(bits as u64)),
2832+
NativeTy::Bool => VmValue::Bool(bits != 0),
2833+
_ => VmValue::Int(bits),
28192834
})
28202835
}
28212836
_ => {
@@ -3304,11 +3319,14 @@ fn compute_recursive_int_member_inner(
33043319
}
33053320

33063321
/// The mutually-recursive group (call-graph SCC) containing `function_id`, if it is
3307-
/// a cycle of >= 2 all-scalar-`Int` functions whose every `CallKnown` targets a
3308-
/// group member (native-call-ABI slice 4). Returned sorted; `None` for non-cyclic
3309-
/// functions and pure self-recursion (handled by the self-recursive path).
3322+
/// a cycle of >= 2 functions (native-call-ABI slice 4). Returned sorted; `None` for
3323+
/// non-cyclic functions and pure self-recursion (handled by the self-recursive
3324+
/// path). Per-member native eligibility (scalar params/return, native-subset body)
3325+
/// is NOT decided here — the caller's `translate_to_native_jit_with_calls` per member
3326+
/// is the single eligibility gate, so the group admits any cycle the general native
3327+
/// path can compile (Int/Bool/Float bodies, members that also call inlinable leaves).
33103328
#[cfg(feature = "native-jit")]
3311-
fn native_recursive_int_group(unit: &RegUnit, function_id: usize) -> Option<Vec<usize>> {
3329+
fn native_recursive_group(unit: &RegUnit, function_id: usize) -> Option<Vec<usize>> {
33123330
use std::collections::HashSet;
33133331
let callees = |fid: usize| -> Vec<usize> {
33143332
unit.functions.get(fid).map_or_else(Vec::new, |f| {
@@ -3347,15 +3365,6 @@ fn native_recursive_int_group(unit: &RegUnit, function_id: usize) -> Option<Vec<
33473365
if scc.len() < 2 {
33483366
return None;
33493367
}
3350-
let group: HashSet<usize> = scc.iter().copied().collect();
3351-
for &member in &scc {
3352-
// Mutual recursion falls back to the full interpreter (not the i64 tier-0
3353-
// executor), so any scalar value return kind (Int or Bool) is admissible.
3354-
match compute_recursive_int_member_inner(unit, member, &group) {
3355-
Some(ScalarSlotKind::Int | ScalarSlotKind::Bool) => {}
3356-
_ => return None,
3357-
}
3358-
}
33593368
Some(scc)
33603369
}
33613370

crates/rsscript/tests/jit_acceptance.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4759,6 +4759,44 @@ fn main() -> Unit {
47594759
);
47604760
}
47614761

4762+
/// Mutual recursion generalized to scalar Float (the Phase-2 treatment applied to
4763+
/// the group path): a FLOAT-returning mutually-recursive cycle now runs natively via
4764+
/// the co-compiled group, with Float params/return marshalled via to_bits/from_bits.
4765+
/// Before, the group analysis admitted only Int/Bool members. Byte-identical to the
4766+
/// interpreter (incl. float formatting) and `native_calls > 0`.
4767+
#[cfg(feature = "native-jit")]
4768+
#[test]
4769+
fn native_mutual_recursion_float_runs_native() {
4770+
let source = "\
4771+
fn fa(n: Int) -> Float {
4772+
if n <= 0 { return 1.0 }
4773+
return 1.5 + fb(n: n - 1)
4774+
}
4775+
fn fb(n: Int) -> Float {
4776+
if n <= 0 { return 2.0 }
4777+
return 0.5 + fa(n: n - 1)
4778+
}
4779+
fn main() -> Unit {
4780+
Log.write(message: read Float.to_string(value: read fa(n: 11)))
4781+
Log.write(message: read Float.to_string(value: read fb(n: 10)))
4782+
return Unit
4783+
}
4784+
";
4785+
let interp = common::run_vm_source("native-mutual-float.rss", source, &[]).expect("interp");
4786+
let exe = rsscript::reg_vm_compile_source("native-mutual-float.rss", source).expect("compile");
4787+
let (out, stats) = exe
4788+
.eval_main_with_args_native_with_stats(std::iter::empty::<String>())
4789+
.expect("native run");
4790+
assert_eq!(
4791+
interp.stdout, out.stdout,
4792+
"Float mutual recursion native must be byte-identical to the interpreter"
4793+
);
4794+
assert!(
4795+
stats.native_calls > 0,
4796+
"Float mutual recursion should run via the native group path: {stats:?}",
4797+
);
4798+
}
4799+
47624800
/// Native-call ABI (slice 4): MUTUAL recursion `is_even`/`is_odd` runs NATIVELY via
47634801
/// the co-compiled group (CallGroup), byte-identical to the interpreter.
47644802
/// `native_calls > 0` proves the native group path executed.

0 commit comments

Comments
 (0)