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
14 changes: 14 additions & 0 deletions changelog.d/8872-ecs-cross-module-dispatch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Removed cross-module dispatch and argument-bundle overhead on the ECS command
path. Fourteen general compiler/runtime mechanisms: safe cross-module
free-function graph inlining, resolved Array header reuse across indexed
stores, split dynamic canonical Array read keys, guarded direct calls that
involve synthesized `arguments`, scalarized length-only `arguments` bundles,
direct captureless `Array.some` callbacks, inlined bounded tiny allocation
kernels and function-candidate optimization inside candidate methods,
preserved Map-entry types inside function bodies, trusted validated rooted
iterator headers, on-demand Array element-shape proofs, reused dynamic
all-pointer append proofs, inlined runtime-branded `Map.size`/`Set.size`
reads, and fully inlined equality against exact three-byte string literals.
On the upstream `codehz/ecs` "5k entities: 3 commands each + sync" row the
retained control moved from 8.610 ms to 7.287 ms per operation on the pinned
M1 Mac mini (15/15 paired wins, 30/30 semantic oracles).
43 changes: 43 additions & 0 deletions crates/perry-codegen/src/codegen/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ use crate::expr::{nanbox_pointer_inline, FnCtx};
use crate::nanbox::double_literal;
use crate::types::{DOUBLE, I32, I64, PTR};

/// Internal-only declared type used by the direct-call clone whose trailing
/// synthetic `arguments` slot carries the already boxed argument count.
/// Source HIR can never name this type: the marker is attached only to a
/// cloned method immediately before codegen.
pub(crate) const SYNTHETIC_ARGUMENTS_LENGTH_TYPE: &str = "__perry_arguments_length_scalar";

/// Additive direct-call ABI for methods proved to observe `arguments` only
/// through exact `.length` reads. The public method keeps its ordinary marked
/// Array/Arguments ABI for runtime dispatch and reflection.
pub(crate) fn arguments_length_method_name(public_name: &str) -> String {
format!("{public_name}$arguments_length")
}

pub(crate) enum ArgumentsCallee<'a> {
Undefined,
FunctionWrapper(&'a str),
Expand Down Expand Up @@ -143,6 +156,36 @@ fn arguments_used_only_for_length(body: &[Stmt], arguments_id: u32) -> bool {
length_reads > 0 && total_uses == length_reads
}

/// Whether a method may expose the scalar-count direct-call clone.
///
/// This is deliberately stricter than the materialization elision above. A
/// user rest parameter still needs its own array, and a nested closure may
/// outlive the direct call, so both shapes remain on the public ABI even when
/// every syntactic use happens to be a `.length` read.
pub(crate) fn method_supports_arguments_length_direct_abi(method: &perry_hir::Function) -> bool {
let Some(synth_param) = method
.params
.last()
.filter(|p| p.arguments_object.is_some())
else {
return false;
};
if method
.params
.iter()
.any(|p| p.is_rest && p.arguments_object.is_none())
{
return false;
}
let mut captured = false;
crate::collectors::for_each_expr_in_stmts(&method.body, &mut |expr| {
if let Expr::Closure { captures, .. } = expr {
captured |= captures.contains(&synth_param.id);
}
});
!captured && arguments_used_only_for_length(&method.body, synth_param.id)
}

fn mapped_arguments_params(params: &[Param]) -> Vec<(u32, u32)> {
params
.iter()
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/closure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1086,6 +1086,7 @@ pub(super) fn compile_closure(
method_param_counts: &cross_module.method_param_counts,
method_has_rest: &cross_module.method_has_rest,
method_has_synthetic_arguments: &cross_module.method_has_synthetic_arguments,
method_arguments_length_only: &cross_module.method_arguments_length_only,
imported_func_return_types: &cross_module.imported_func_return_types,
ffi_signatures: &cross_module.ffi_signatures,
ffi_aliases: &cross_module.ffi_aliases,
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/codegen/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,7 @@ pub(super) fn compile_module_entry(
method_param_counts: &cross_module.method_param_counts,
method_has_rest: &cross_module.method_has_rest,
method_has_synthetic_arguments: &cross_module.method_has_synthetic_arguments,
method_arguments_length_only: &cross_module.method_arguments_length_only,
imported_func_return_types: &cross_module.imported_func_return_types,
ffi_signatures: &cross_module.ffi_signatures,
ffi_aliases: &cross_module.ffi_aliases,
Expand Down Expand Up @@ -1561,6 +1562,7 @@ pub(super) fn compile_module_entry(
method_param_counts: &cross_module.method_param_counts,
method_has_rest: &cross_module.method_has_rest,
method_has_synthetic_arguments: &cross_module.method_has_synthetic_arguments,
method_arguments_length_only: &cross_module.method_arguments_length_only,
imported_func_return_types: &cross_module.imported_func_return_types,
ffi_signatures: &cross_module.ffi_signatures,
ffi_aliases: &cross_module.ffi_aliases,
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,7 @@ pub(super) fn compile_function(
method_param_counts: &cross_module.method_param_counts,
method_has_rest: &cross_module.method_has_rest,
method_has_synthetic_arguments: &cross_module.method_has_synthetic_arguments,
method_arguments_length_only: &cross_module.method_arguments_length_only,
imported_func_return_types: &cross_module.imported_func_return_types,
ffi_signatures: &cross_module.ffi_signatures,
ffi_aliases: &cross_module.ffi_aliases,
Expand Down
47 changes: 36 additions & 11 deletions crates/perry-codegen/src/codegen/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ pub(super) fn compile_method(
method.name
)
})?;
let arguments_length_clone = method.params.last().is_some_and(|param| {
matches!(
&param.ty,
perry_hir::types::Type::Named(name)
if name == super::arguments::SYNTHETIC_ARGUMENTS_LENGTH_TYPE
)
});
// Representation-selection Phase 5a: the proven-`this` clone is a SECOND,
// additive body compiled from the same HIR through the same statement
// lowerer. It never replaces the public symbol and never participates in
Expand All @@ -82,14 +89,15 @@ pub(super) fn compile_method(
.get(&(class.name.clone(), method.name.clone()))
})
.flatten();
let guarded_undefined_param = (!is_index_clone && !ptr_array_cache_clone && !pshape_arg_clone)
.then(|| {
cross_module
.guarded_undefined_method_params
.get(&(class.name.clone(), method.name.clone()))
.copied()
})
.flatten();
let guarded_undefined_param =
(!arguments_length_clone && !is_index_clone && !ptr_array_cache_clone && !pshape_arg_clone)
.then(|| {
cross_module
.guarded_undefined_method_params
.get(&(class.name.clone(), method.name.clone()))
.copied()
})
.flatten();
let fast_array_param_ids = if fast_array_handle_clone {
crate::codegen::typed_abi::nonnegative_index_fast_array_params(
method,
Expand All @@ -110,7 +118,9 @@ pub(super) fn compile_method(
debug_assert!(!pshape_arg_clone || !ptr_array_cache_clone);
debug_assert!(!pshape_arg_clone || typed_public_trampoline.is_none());
debug_assert!(!pshape_arg_clone || !force_generic_body);
let family_name = if pshape_arg_clone {
let family_name = if arguments_length_clone {
super::arguments::arguments_length_method_name(&public_llvm_name)
} else if pshape_arg_clone {
crate::collectors::pshape_args_method_name(&public_llvm_name)
} else if ptr_array_cache_clone {
crate::collectors::ptr_array_cache_method_name(&public_llvm_name)
Expand All @@ -119,7 +129,9 @@ pub(super) fn compile_method(
} else {
public_llvm_name.clone()
};
let llvm_name = if fast_array_handle_clone {
let llvm_name = if arguments_length_clone {
family_name.clone()
} else if fast_array_handle_clone {
crate::codegen::nonnegative_index_fast_array_method_name(
&public_llvm_name,
nonnegative_index_params.expect("fast-array clone has index parameters"),
Expand Down Expand Up @@ -174,6 +186,13 @@ pub(super) fn compile_method(
if is_index_clone {
lf.pre_statepoint_inline = true;
}
// #8872: methods participate in the same allocation-hot analysis as
// functions and closures. This must be set before the entry block exists
// because `lower_call/new_alloc.rs` consults it while lowering each `new`
// site. Previously `collect_alloc_hot_functions` could discover a method
// FuncId, but method codegen silently discarded the result, leaving tiny
// cross-module allocation kernels on the outlined runtime allocator.
lf.alloc_hot = cross_module.alloc_hot_functions.contains(&method.id);

// gh #6206 / #6081: methods were compiled WITHOUT a shadow frame — same
// exact-roots liveness hole as closures (see compile_closure). One extra
Expand Down Expand Up @@ -439,6 +458,7 @@ pub(super) fn compile_method(
method_param_counts: &cross_module.method_param_counts,
method_has_rest: &cross_module.method_has_rest,
method_has_synthetic_arguments: &cross_module.method_has_synthetic_arguments,
method_arguments_length_only: &cross_module.method_arguments_length_only,
imported_func_return_types: &cross_module.imported_func_return_types,
ffi_signatures: &cross_module.ffi_signatures,
ffi_aliases: &cross_module.ffi_aliases,
Expand Down Expand Up @@ -1221,7 +1241,11 @@ pub(super) fn compile_method(
// twice.
if let Some(param_index) = guarded_undefined_param.filter(|_| !guarded_undefined_clone) {
emit_guarded_undefined(llmod, method, &family_name, &llvm_name, param_index);
} else if !is_pshape_clone && !is_index_clone && !guarded_undefined_clone {
} else if !arguments_length_clone
&& !is_pshape_clone
&& !is_index_clone
&& !guarded_undefined_clone
{
if let Some(kind) = typed_public_trampoline {
emit_public_typed(llmod, method, &public_llvm_name, &llvm_name, kind);
} else if force_generic_body {
Expand Down Expand Up @@ -1732,6 +1756,7 @@ pub(super) fn compile_static_method(
method_param_counts: &cross_module.method_param_counts,
method_has_rest: &cross_module.method_has_rest,
method_has_synthetic_arguments: &cross_module.method_has_synthetic_arguments,
method_arguments_length_only: &cross_module.method_arguments_length_only,
imported_func_return_types: &cross_module.imported_func_return_types,
ffi_signatures: &cross_module.ffi_signatures,
ffi_aliases: &cross_module.ffi_aliases,
Expand Down
9 changes: 9 additions & 0 deletions crates/perry-codegen/src/codegen/method_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,15 @@ pub(crate) fn build_method_names(
let clone = crate::collectors::pshape_method_name(&llvm_fn);
llmod.declare_function(&clone, DOUBLE, &param_types);
}
if ic
.method_arguments_length_only
.get(method_idx)
.copied()
.unwrap_or(false)
{
let clone = super::arguments::arguments_length_method_name(&llvm_fn);
llmod.declare_function(&clone, DOUBLE, &param_types);
}
}

// Cross-module getters. The dispatch site at
Expand Down
20 changes: 19 additions & 1 deletion crates/perry-codegen/src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1549,6 +1549,8 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
std::collections::HashMap::new();
let mut method_has_synthetic_arguments: std::collections::HashMap<(String, String), bool> =
std::collections::HashMap::new();
let mut method_arguments_length_only: std::collections::HashMap<(String, String), bool> =
std::collections::HashMap::new();
for cls in &hir.classes {
for m in &cls.methods {
let key = (cls.name.clone(), m.name.clone());
Expand All @@ -1561,7 +1563,10 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
.last()
.is_some_and(|param| param.arguments_object.is_some())
{
method_has_synthetic_arguments.insert(key, true);
method_has_synthetic_arguments.insert(key.clone(), true);
}
if arguments::method_supports_arguments_length_direct_abi(m) {
method_arguments_length_only.insert(key, true);
}
}
// Issue #894: track static methods too. Effect's `static pipe()` /
Expand Down Expand Up @@ -1622,6 +1627,18 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
.insert((effective_name.clone(), mname.clone()), true);
}
}
if ic
.method_arguments_length_only
.get(i)
.copied()
.unwrap_or(false)
{
method_arguments_length_only.insert((ic.name.clone(), mname.clone()), true);
if effective_name != ic.name {
method_arguments_length_only
.insert((effective_name.clone(), mname.clone()), true);
}
}
}
for (i, method_name) in ic.static_method_names.iter().enumerate() {
let registry_name = static_method_registry_key(method_name);
Expand Down Expand Up @@ -2333,6 +2350,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
method_param_counts,
method_has_rest,
method_has_synthetic_arguments,
method_arguments_length_only,
class_keys_globals: class_keys_globals_map,
class_field_counts: class_field_counts_map,
class_init_chains: class_init_chains_map,
Expand Down
9 changes: 9 additions & 0 deletions crates/perry-codegen/src/codegen/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,12 @@ pub struct ImportedClass {
/// `arguments`. Unlike a user `...rest` slot, this slot receives every
/// actual argument while the named parameters remain positional.
pub method_has_synthetic_arguments: Vec<bool>,
/// Parallel to `method_names`. `true` is a producer-authored capability:
/// the method has no user rest parameter and observes its synthesized
/// `arguments` binding only through exact `.length` reads. Importers may
/// call the additive `$arguments_length` ABI with a scalar count instead
/// of allocating and filling an argument bundle.
pub method_arguments_length_only: Vec<bool>,
/// Static field names defined on this class. Used to declare the foreign
/// `@perry_static_<src>__<class>__<field>` global with external linkage
/// so cross-module `[Parent.Symbol.X] = …` reads/writes resolve to the
Expand Down Expand Up @@ -839,6 +845,9 @@ pub(crate) struct CrossModuleCtx {
/// synthetic slot receives all actual arguments rather than only the
/// values after the visible parameters.
pub method_has_synthetic_arguments: std::collections::HashMap<(String, String), bool>,
/// Producer-proved scalar-count direct-call capability for synthetic
/// `arguments` methods. Sparse map (only `true` entries stored).
pub method_arguments_length_only: std::collections::HashMap<(String, String), bool>,
/// Per-class `keys_array` global variable names. Each entry maps
/// `class_name → @perry_class_keys_<modprefix>__<sanitized_class>`.
/// Built once in `compile_module` (one entry per class — local
Expand Down
51 changes: 51 additions & 0 deletions crates/perry-codegen/src/codegen/ordinary_method_artifacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,57 @@ pub(super) fn compile_ordinary_method_artifacts(
)
.with_context(|| format!("lowering method '{}::{}'", class.name, method.name))?;

// A separate externally callable body keeps the public/runtime ABI exact:
// dynamic dispatch still receives a marked argument bundle, while a
// guarded direct caller may pass the actual argument count in the same
// trailing tagged-value slot. The internal marker type makes exact
// `arguments.length` reads lower to that scalar without changing source
// HIR or teaching generic property dispatch about the specialized ABI.
if super::arguments::method_supports_arguments_length_direct_abi(method) {
let mut clone = method.clone();
let synth_param = clone
.params
.last_mut()
.expect("length-only arguments method has a synthetic parameter");
synth_param.ty = perry_hir::types::Type::Named(
super::arguments::SYNTHETIC_ARGUMENTS_LENGTH_TYPE.to_string(),
);
compile_method(
llmod,
class,
&clone,
func_names,
strings,
classes,
methods,
module_globals,
module_global_types,
import_function_prefixes,
enums,
static_field_globals,
class_ids,
func_signatures,
func_synthetic_arguments,
module_boxed_vars,
closure_rest_params,
cross_module,
None,
false,
None,
None,
false,
false,
false,
false,
)
.with_context(|| {
format!(
"lowering scalar arguments-length clone of method '{}::{}'",
class.name, method.name
)
})?;
}

if cross_module
.guarded_undefined_method_params
.contains_key(&(class.name.clone(), method.name.clone()))
Expand Down
Loading