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
1 change: 1 addition & 0 deletions changelog.d/8783-exact-callback-versioned-loops.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Versioned checked-reader loops now recognize a narrow family of exact additive arrow callbacks, including unused-result `++`/`--` updates to compiler-proven captured boxes, and keep admitted array handles in the loop preheader while the callback stays on its non-collecting path. Property-cache misses, dynamic addition, ToNumeric coercion, TDZ reads, identity or capture-layout mismatches, and local exception scopes fail closed to an exact next-index resume through the ordinary guarded loop, eliminating steady-state callback guards without changing observable fallback behavior.
92 changes: 88 additions & 4 deletions crates/perry-codegen/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,7 @@ impl LlBlock {
callee: "llvm.aarch64.fjcvtzs".to_string(),
args: vec![("double", val.to_string())],
cconv: None,
gc_leaf: false,
});
return r;
}
Expand Down Expand Up @@ -1224,6 +1225,26 @@ impl LlBlock {
}

pub fn call(&mut self, ret_ty: LlvmType, func_name: &str, args: &[(LlvmType, &str)]) -> String {
self.call_with_gc_leaf(ret_ty, func_name, args, false)
}

/// Direct-call counterpart of [`Self::call_indirect_gc_leaf`].
pub fn call_gc_leaf(
&mut self,
ret_ty: LlvmType,
func_name: &str,
args: &[(LlvmType, &str)],
) -> String {
self.call_with_gc_leaf(ret_ty, func_name, args, true)
}

fn call_with_gc_leaf(
&mut self,
ret_ty: LlvmType,
func_name: &str,
args: &[(LlvmType, &str)],
gc_leaf: bool,
) -> String {
// #835 + #846: record this emission against the FFI provenance
// registry. The driver consults the registry after all per-module
// codegen finishes to auto-link the providing crate.
Expand All @@ -1244,9 +1265,10 @@ impl LlBlock {
if let Some((cont, lpad)) = self.eh_invoke_suffix(func_name) {
let arg_str = format_args(args);
let cc = cconv.map(|c| format!("{c} ")).unwrap_or_default();
let leaf_attr = if gc_leaf { " \"gc-leaf-function\"" } else { "" };
self.emit(format!(
"{} = invoke {}{} @{}({}) to label %{} unwind label %{}",
r, cc, ret_ty, func_name, arg_str, cont, lpad
"{} = invoke {}{} @{}({}){} to label %{} unwind label %{}",
r, cc, ret_ty, func_name, arg_str, leaf_attr, cont, lpad
));
self.emit_inline_label(&cont);
} else {
Expand All @@ -1256,6 +1278,7 @@ impl LlBlock {
callee: func_name.to_string(),
args: args.iter().map(|(t, v)| (*t, v.to_string())).collect(),
cconv,
gc_leaf,
});
}
r
Expand Down Expand Up @@ -1285,6 +1308,7 @@ impl LlBlock {
callee: func_name.to_string(),
args: args.iter().map(|(t, v)| (*t, v.to_string())).collect(),
cconv,
gc_leaf: false,
});
}
}
Expand All @@ -1306,15 +1330,38 @@ impl LlBlock {
ret_ty: LlvmType,
fn_ptr: &str,
args: &[(LlvmType, &str)],
) -> String {
self.call_indirect_with_gc_leaf(ret_ty, fn_ptr, args, false)
}

/// Emit an indirect call whose caller-side native GC values need not be
/// relocated across the call. The target may still collect, so this must
/// only be used when every collecting return path makes those values dead.
pub fn call_indirect_gc_leaf(
&mut self,
ret_ty: LlvmType,
fn_ptr: &str,
args: &[(LlvmType, &str)],
) -> String {
self.call_indirect_with_gc_leaf(ret_ty, fn_ptr, args, true)
}

fn call_indirect_with_gc_leaf(
&mut self,
ret_ty: LlvmType,
fn_ptr: &str,
args: &[(LlvmType, &str)],
gc_leaf: bool,
) -> String {
let r = self.reg();
// Indirect targets (closures, method pointers) can always throw.
if let Some(lpad) = self.counter.current_eh_unwind_label() {
let arg_str = format_args(args);
let cont = format!("eh.cont{}", self.counter.next());
let leaf_attr = if gc_leaf { " \"gc-leaf-function\"" } else { "" };
self.emit(format!(
"{} = invoke {} {}({}) to label %{} unwind label %{}",
r, ret_ty, fn_ptr, arg_str, cont, lpad
"{} = invoke {} {}({}){} to label %{} unwind label %{}",
r, ret_ty, fn_ptr, arg_str, leaf_attr, cont, lpad
));
self.emit_inline_label(&cont);
} else {
Expand All @@ -1323,6 +1370,7 @@ impl LlBlock {
ret: ret_ty,
fptr: fn_ptr.to_string(),
args: args.iter().map(|(t, v)| (*t, v.to_string())).collect(),
gc_leaf,
});
}
r
Expand Down Expand Up @@ -1563,6 +1611,17 @@ mod tests {
.contains("call double @js_nanbox_string(i64 %handle)"));
}

#[test]
fn direct_gc_leaf_call_places_the_callsite_attribute_after_arguments() {
let mut b = fresh();
let r = b.call_gc_leaf(DOUBLE, "guarded_reader", &[(I64, "%handle")]);
assert_eq!(r, "%r1");
assert_eq!(
b.to_ir(),
"entry.0:\n %r1 = call double @guarded_reader(i64 %handle) \"gc-leaf-function\""
);
}

#[test]
fn indirect_call_uses_opaque_pointer_syntax() {
let mut b = fresh();
Expand All @@ -1574,6 +1633,18 @@ mod tests {
);
}

#[test]
fn indirect_gc_leaf_call_places_the_callsite_attribute_after_arguments() {
let mut b = fresh();
let r =
b.call_indirect_gc_leaf(DOUBLE, "%callback", &[(I64, "%closure"), (DOUBLE, "%arg")]);
assert_eq!(r, "%r1");
assert_eq!(
b.to_ir(),
"entry.0:\n %r1 = call double %callback(i64 %closure, double %arg) \"gc-leaf-function\""
);
}

#[test]
fn indirect_invoke_uses_opaque_pointer_syntax() {
let mut b = fresh();
Expand All @@ -1586,6 +1657,19 @@ mod tests {
);
}

#[test]
fn indirect_gc_leaf_invoke_places_the_attribute_before_the_successor() {
let mut b = fresh();
b.counter.push_eh_scope("catch.0".to_string());
let r =
b.call_indirect_gc_leaf(DOUBLE, "%callback", &[(I64, "%closure"), (DOUBLE, "%arg")]);
assert_eq!(r, "%r1");
assert_eq!(
b.to_ir(),
"entry.0:\n %r1 = invoke double %callback(i64 %closure, double %arg) \"gc-leaf-function\" to label %eh.cont2 unwind label %catch.0\neh.cont2:"
);
}

#[test]
fn terminator_blocks_further_emits() {
let mut b = fresh();
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/artifact_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ pub(super) struct ModuleArtifactsCtx<'a> {
pub closure_lengths: &'a HashMap<u32, u32>,
pub closure_arrow_functions: &'a HashSet<u32>,
pub trusted_box_closures: &'a HashMap<u32, super::closure_collect::TrustedBoxClosure>,
pub versioned_loop_callbacks: &'a HashSet<u32>,
pub closures: &'a [(perry_hir::types::FuncId, perry_hir::Expr)],
pub class_keys_init_data: &'a [(String, String, u32, Vec<u64>, Vec<u64>)],
/// Keys global to `(class id, packed GcHeader word)` for inline `new`.
Expand Down
31 changes: 31 additions & 0 deletions crates/perry-codegen/src/codegen/artifacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> {
closure_lengths,
closure_arrow_functions,
trusted_box_closures,
versioned_loop_callbacks,
closures,
class_keys_init_data,
class_header_image_inits,
Expand Down Expand Up @@ -159,6 +160,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> {
closure_rest_params,
cross_module,
false,
false,
)
.with_context(|| format!("lowering closure func_id={}", func_id))?;
if trusted_box_closures.contains_key(func_id) {
Expand All @@ -184,9 +186,37 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> {
closure_rest_params,
cross_module,
true,
false,
)
.with_context(|| format!("lowering trusted-box closure func_id={}", func_id))?;
}
if versioned_loop_callbacks.contains(func_id) {
compile_closure(
llmod,
*func_id,
closure_expr,
func_names,
strings,
class_table,
method_names,
module_globals,
opts.import_function_prefixes,
enum_table,
static_field_globals,
class_ids,
func_signatures,
func_synthetic_arguments,
module_prefix,
module_boxed_vars,
module_receiver_types,
&module_reassigned_locals,
closure_rest_params,
cross_module,
true,
true,
)
.with_context(|| format!("lowering versioned-loop closure func_id={}", func_id))?;
}
let done = closure_index + 1;
if done == closures.len() || done % closure_progress_step == 0 {
progress.items("closure bodies", done, closures.len(), closure_started);
Expand Down Expand Up @@ -1977,6 +2007,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> {
closure_lengths,
closure_arrow_functions,
trusted_box_closures,
versioned_loop_callbacks,
&user_fn_wrapper_rest,
closure_synthetic_arguments,
&user_fn_wrapper_synthetic_arguments,
Expand Down
49 changes: 40 additions & 9 deletions crates/perry-codegen/src/codegen/closure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,7 @@ pub(super) fn compile_closure(
closure_rest_params: &HashMap<u32, usize>,
cross_module: &CrossModuleCtx,
trusted_box_captures: bool,
versioned_loop_callback: bool,
) -> Result<()> {
// Destructure the closure expression. We trust that the caller
// passes only `Expr::Closure` here (from `collect_closures_*`).
Expand Down Expand Up @@ -568,13 +569,18 @@ pub(super) fn compile_closure(
} else {
public_llvm_name.clone()
};
let llvm_name = if trusted_box_captures {
let llvm_name = if versioned_loop_callback {
format!("{ordinary_body_name}$trusted_boxes$versioned_loop")
} else if trusted_box_captures {
format!("{ordinary_body_name}$trusted_boxes")
} else {
ordinary_body_name
};

// Param list: i64 this_closure, then each param as double.
// Param list: i64 this_closure, then each param as double. The private
// versioned-loop clone reuses its proven-unused first callback parameter
// for the caller's stack context, so its ABI and register footprint stay
// identical to the ordinary trusted clone.
let mut llvm_params: Vec<(LlvmType, String)> = Vec::with_capacity(params.len() + 1);
llvm_params.push((I64, "%this_closure".to_string()));
for p in params {
Expand Down Expand Up @@ -634,6 +640,16 @@ pub(super) fn compile_closure(

let _ = lf.create_block("entry");

let versioned_loop_deopt_context = versioned_loop_callback.then(|| {
let scratch_param = params
.first()
.expect("versioned-loop callback selection requires a scratch parameter");
let scratch_arg = format!("%arg{}", scratch_param.id);
let blk = lf.block_mut(0).expect("closure body has an entry block");
let context_bits = blk.bitcast_double_to_i64(&scratch_arg);
blk.inttoptr(I64, &context_bits)
});

let mut closure_boxed_vars: HashSet<u32> = closure_relevant_ids
.iter()
.filter(|id| module_boxed_vars.contains(id))
Expand Down Expand Up @@ -665,8 +681,19 @@ pub(super) fn compile_closure(
// their types available inside the body. Without this, closures
// that capture an array `items` and do `items.length` miss the
// typed fast path and return undefined.
let mut local_types: HashMap<u32, perry_hir::types::Type> =
params.iter().map(|p| (p.id, p.ty.clone())).collect();
let mut local_types: HashMap<u32, perry_hir::types::Type> = params
.iter()
.map(|p| {
(
p.id,
if versioned_loop_callback {
perry_hir::types::Type::Any
} else {
p.ty.clone()
},
)
})
.collect();
for id in &closure_relevant_ids {
if let Some(ty) = module_receiver_types.get(id) {
local_types.entry(*id).or_insert_with(|| ty.clone());
Expand Down Expand Up @@ -832,11 +859,13 @@ pub(super) fn compile_closure(
&cross_module.compile_time_constants,
&cross_module.module_dispatch,
);
if let Some(callback_shapes) = cross_module.array_callback_shapes.get(&func_id) {
native_facts
.shape_stability
.shape_proven_ptr_locals
.extend(callback_shapes.clone());
if !versioned_loop_callback {
if let Some(callback_shapes) = cross_module.array_callback_shapes.get(&func_id) {
native_facts
.shape_stability
.shape_proven_ptr_locals
.extend(callback_shapes.clone());
}
}

// Representation-selection context gates (see codegen/function.rs).
Expand Down Expand Up @@ -1034,7 +1063,9 @@ pub(super) fn compile_closure(
local_closure_func_ids: HashMap::new(),
local_closure_param_counts: HashMap::new(),
resolved_arrow_callback_targets: HashMap::new(),
resolved_versioned_loop_callback_targets: HashMap::new(),
trusted_box_captures,
versioned_loop_deopt_context,
trusted_box_capture_ptrs,
local_func_ref_ids: HashMap::new(),
option_object_locals: HashMap::new(),
Expand Down
Loading
Loading