Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog.d/8290-tier-b-self-recursion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### Performance

- Guarded `$spec_b` clones now call themselves directly when their recursive
arguments constructively produce Numbers, avoiding the public parameter
guard on every recursive edge. The proof is limited to canonical numeric
constructions, so BigInt-capable arithmetic and annotation-only claims keep
the guarded fallback.
163 changes: 163 additions & 0 deletions crates/perry-codegen/src/codegen/spec_self_recursion_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,99 @@ fn recursive_module(lhs: Expr, rhs: Expr) -> Module {
module
}

/// The same recursive body reached through an ordinary `number` parameter
/// guard. The `Number(...)` construction gives the outer call a runtime
/// Number proof without creating a viable raw-i32 tuple, so the emitted clone
/// is the boxed `$spec_b` shape whose recursive routing #8169 exercises.
fn guarded_recursive_module(lhs: Expr, rhs: Expr) -> Module {
let mut module = recursive_module(lhs, rhs);
module.init.clear();
module.init.push(Stmt::Let {
id: 20,
name: "k".to_string(),
ty: Type::Any,
mutable: false,
init: Some(Expr::NumberCoerce(Box::new(Expr::Undefined))),
});
module.init.push(Stmt::Expr(Expr::Call {
callee: Box::new(Expr::FuncRef(1)),
args: vec![Expr::LocalGet(20)],
type_args: Vec::new(),
byte_offset: 0,
}));
module
}

/// A guarded Number parameter plus an unconstrained value whose arithmetic
/// may produce a BigInt. The recursive first argument must keep the public
/// guard: when `x` is a BigInt, `x * x` is a BigInt too.
fn bigint_capable_guarded_recursive_module() -> Module {
let f = Function {
id: 1,
name: "f".to_string(),
type_params: Vec::new(),
params: vec![
Param {
id: 10,
name: "n".to_string(),
ty: Type::Number,
default: None,
decorators: Vec::new(),
is_rest: false,
arguments_object: None,
},
Param {
id: 11,
name: "x".to_string(),
ty: Type::Any,
default: None,
decorators: Vec::new(),
is_rest: false,
arguments_object: None,
},
],
return_type: Type::Number,
body: vec![Stmt::Return(Some(Expr::Conditional {
condition: Box::new(Expr::Compare {
op: CompareOp::Lt,
left: Box::new(Expr::LocalGet(10)),
right: Box::new(Expr::Integer(1)),
}),
then_expr: Box::new(Expr::LocalGet(10)),
else_expr: Box::new(Expr::Call {
callee: Box::new(Expr::FuncRef(1)),
args: vec![
Expr::Binary {
op: BinaryOp::Mul,
left: Box::new(Expr::LocalGet(11)),
right: Box::new(Expr::LocalGet(11)),
},
Expr::LocalGet(11),
],
type_args: Vec::new(),
byte_offset: 0,
}),
}))],
is_async: false,
is_generator: false,
is_strict: true,
is_exported: false,
captures: Vec::new(),
decorators: Vec::new(),
was_plain_async: false,
was_unrolled: false,
};
let mut module = Module::new("spec_self_recursion_bigint.ts");
module.functions.push(f);
module.init.push(Stmt::Expr(Expr::Call {
callee: Box::new(Expr::FuncRef(1)),
args: vec![Expr::Undefined, Expr::Undefined],
type_args: Vec::new(),
byte_offset: 0,
}));
module
}

fn compile_ir(module: &Module) -> String {
let opts = CompileOptions {
emit_ir_only: true,
Expand Down Expand Up @@ -234,3 +327,73 @@ fn an_unproven_local_recursive_argument_keeps_the_boxed_call() {
"the unprovable edge plus the in-range arm's fallback:\n{clone}"
);
}

#[test]
fn derived_recursive_number_argument_re_enters_the_guarded_clone() {
let ir = compile_ir(&guarded_recursive_module(
arith(BinaryOp::Sub, 1),
arith(BinaryOp::Sub, 2),
));
let public = function_ir(&ir, "@perry_fn_spec_self_recursion_ts__f(");
let clone = function_ir(&ir, "$spec_b(");

// Keep both halves of the subject live: this must be the ordinary boxed
// clone selected by the public Number guard, not the raw-i32 Tier-A path.
assert!(public.contains("call i32 @js_typed_f64_arg_guard("));
assert!(
clone.starts_with("define internal")
&& clone.contains("double @perry_fn_spec_self_recursion_ts__f$spec_b(double"),
"expected a guarded boxed clone to specialize:\n{clone}"
);

// #8203 gives recursion-participating clones `preserve_nonecc`, which lands
// between `call` and the return type, so match the call LINE rather than a
// fixed prefix.
assert_eq!(
clone
.lines()
.filter(|l| l.contains("call")
&& l.contains("@perry_fn_spec_self_recursion_ts__f$spec_b(double"))
.count(),
2,
"both derived Number arguments must re-enter the guarded clone directly:\n{clone}"
);
assert_eq!(
clone
.lines()
.filter(
|l| l.contains("call") && l.contains("@perry_fn_spec_self_recursion_ts__f(double")
)
.count(),
0,
"a constructively numeric recursive argument must not re-run the public guard:\n{clone}"
);
}

#[test]
fn bigint_capable_recursive_argument_keeps_the_public_guard() {
let ir = compile_ir(&bigint_capable_guarded_recursive_module());
let public = function_ir(&ir, "@perry_fn_spec_self_recursion_bigint_ts__f(");
let clone = function_ir(&ir, "$spec_b_b(");

assert!(public.contains("call i32 @js_typed_f64_arg_guard("));
assert!(
clone.starts_with("define internal")
&& clone.contains("double @perry_fn_spec_self_recursion_bigint_ts__f$spec_b_b(double"),
"expected a guarded boxed clone to specialize:\n{clone}"
);
assert_eq!(
clone
.matches("call double @perry_fn_spec_self_recursion_bigint_ts__f$spec_b_b(double")
.count(),
0,
"BigInt-capable arithmetic must not bypass the Number guard:\n{clone}"
);
assert_eq!(
clone
.matches("call double @perry_fn_spec_self_recursion_bigint_ts__f(double")
.count(),
1,
"the unproven recursive edge must retain the public guarded ABI:\n{clone}"
);
}
12 changes: 12 additions & 0 deletions crates/perry-codegen/src/lower_call/func_ref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,18 @@ pub(crate) fn guarded_path_type(ctx: &FnCtx<'_>, expr: &Expr) -> Option<perry_hi
Expr::Null => Some(Type::Null),
Expr::Undefined | Expr::Void(_) => Some(Type::Void),
Expr::Call { .. } => guarded_call_return_proof(ctx, expr),
// #8169: a Tier-B clone's entry guard gives its boxed Number
// parameters real runtime proofs, and arithmetic derived from those
// parameters constructs another Number. Let a recursive `f(n - 1)`
// therefore re-enter `$spec_b` instead of paying the public guard on
// every edge.
//
// Use the canonical-value predicate rather than `is_numeric_expr`
// alone. The latter deliberately admits some dynamic/BigInt-capable
// arithmetic for lowering decisions; this proof is used to BYPASS a
// runtime type guard, so it must exclude values that can still be a
// boxed BigInt or arise only from an unenforced annotation.
_ if crate::type_analysis::expr_produces_canonical_raw_f64(ctx, expr) => Some(Type::Number),
_ => None,
}
}
Expand Down
Loading