Skip to content
Closed
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
16 changes: 16 additions & 0 deletions crates/perry-codegen/src/lower_call/func_ref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,22 @@ pub fn try_lower_func_ref_call(
let (values, guard) = super::lower_call_args_rooted(ctx, args)?;
arg_group = guard;
lowered.extend(values);
// #8770: pad missing trailing args with TAG_UNDEFINED, exactly like
// the cross-module twin (`extern_func.rs`, issue #608 arm). The callee
// is compiled with `declared_count` double parameters and its
// default-parameter lowering tests each for `undefined`; an
// under-applied same-module call site that emits only the provided
// args leaves the remaining FP argument registers holding caller-saved
// garbage, which the callee then reads as JS values. On the Claude
// Code bundle (one giant module, so EVERY direct call resolves here)
// `aP([q])` for `function aP(q, K = !1, _)` handed `K`/`_` whatever
// d1/d2 held after `js_array_from_values` — the #8770 poison values
// (0xffffffffffffffff receivers → shape_is_url_search_params /
// js_is_truthy faults, corrupted async iteration → unsettled awaits).
let undefined_lit = double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED));
while lowered.len() < declared_count {
lowered.push(undefined_lit.clone());
}
}
let arg_slices: Vec<(crate::types::LlvmType, &str)> =
lowered.iter().map(|s| (DOUBLE, s.as_str())).collect();
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-codegen/src/lower_call/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ mod field_init;
mod func_ref;
#[cfg(test)]
mod pipeline_call_tests;
/// #8770: an under-applied same-module direct call must pad the missing
/// trailing parameters with `TAG_UNDEFINED` (the cross-module arm always did).
#[cfg(test)]
mod underapply_pad_tests;
pub(crate) use func_ref::{
guarded_call_return_proof, guarded_discriminant_branch_proofs, guarded_expr_proof,
guarded_path_type,
Expand Down
108 changes: 108 additions & 0 deletions crates/perry-codegen/src/lower_call/underapply_pad_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//! #8770 regression: a SAME-MODULE direct call with fewer arguments than the
//! callee's declared parameter count must pad the missing trailing parameters
//! with `TAG_UNDEFINED` — exactly like the cross-module twin
//! (`extern_func.rs`, issue #608 arm) always has.
//!
//! Without the padding, the callee (compiled with `declared_count` double
//! parameters, its default-parameter lowering testing each for `undefined`)
//! reads whatever the caller-saved FP argument registers happen to hold. On
//! the Claude Code bundle — one giant module, so every direct call resolves
//! through the same-module arm — `aP([q])` for `function aP(q, K = !1, _)`
//! handed `K`/`_` the leftovers of `js_array_from_values`' internals:
//! impossible-NaN bit patterns (`0xffffffffffffffff`) that flowed into
//! truthiness tests and method receivers (`_.get(A)`) and crashed in
//! `shape_is_url_search_params` / `js_is_truthy`, or silently corrupted the
//! async iteration ("Detected unsettled top-level await").

use crate::{compile_module, CompileOptions};
use perry_hir::types::Type;
use perry_hir::{Expr, Function, Module, Param, Stmt};

fn param(id: u32, name: &str) -> Param {
Param {
id,
name: name.to_string(),
ty: Type::Any,
default: None,
decorators: Vec::new(),
is_rest: false,
arguments_object: None,
}
}

fn function(id: u32, name: &str, params: Vec<Param>, body: Vec<Stmt>) -> Function {
Function {
id,
name: name.to_string(),
type_params: Vec::new(),
params,
return_type: Type::Any,
body,
is_async: false,
is_generator: false,
is_strict: true,
was_plain_async: false,
was_unrolled: false,
is_exported: false,
captures: Vec::new(),
decorators: Vec::new(),
}
}

/// `function callee(a, b, c) { return b; }` called as `callee(7)`.
fn underapplied_call_ir() -> String {
let callee = function(
1,
"callee",
vec![param(10, "a"), param(11, "b"), param(12, "c")],
vec![Stmt::Return(Some(Expr::LocalGet(11)))],
);
let caller = function(
2,
"caller",
Vec::new(),
vec![Stmt::Return(Some(Expr::Call {
callee: Box::new(Expr::FuncRef(1)),
args: vec![Expr::Number(7.0)],
type_args: Vec::new(),
byte_offset: 0,
}))],
);
let mut module = Module::new("underapply_pad_test.ts");
module.functions = vec![callee, caller];
let opts = CompileOptions {
emit_ir_only: true,
..Default::default()
};
String::from_utf8(compile_module(&module, opts).expect("call fixture must compile"))
.expect("LLVM IR is UTF-8")
}

#[test]
fn an_underapplied_direct_call_pads_missing_params_with_undefined() {
let ir = underapplied_call_ir();
let undefined_lit = crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED));
// The direct call must carry all three declared parameters…
let call_line = ir
.lines()
.find(|line| {
line.contains("call double @perry_fn_underapply_pad_test_ts__callee(")
})
.unwrap_or_else(|| panic!("expected a direct call to the callee:\n{ir}"));
let args = call_line
.split("callee(")
.nth(1)
.map(|tail| tail.matches("double").count())
.unwrap_or(0);
assert!(
args >= 3,
"an under-applied direct call must pass every declared parameter \
(got {args} double args): {call_line}\n{ir}"
);
// …and the missing trailing two must be the TAG_UNDEFINED literal.
assert!(
call_line.matches(undefined_lit.as_str()).count() >= 2,
"the two omitted parameters must be padded with the undefined literal \
{undefined_lit}: {call_line}\n{ir}"
);
}
Loading