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
1 change: 1 addition & 0 deletions changelog.d/codegen-typed-signed-index-params.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Performance: a method whose parameters feed element keys gets its typed body (`M$sig`) when whole-program facts prove those parameters int32, whatever their sign. Before, the non-negative index clones (`$idx_u31`) of such a method refused the typed signature unless every index parameter's interval started at 0, so a helper like `set(square, piece)` with a possibly negative key or value kept its double-ABI generic body and its guarded clones. Since the unsigned bound an int32 key is bounded by one unsigned compare, which declines a negative key onto the property path. `PERRY_SIG_SIGNED_INDEX=0` restores the old rule. New probes: `# typed_call` lines in `PERRY_FACTS_DUMP` (each call that resolves to a body with a signature, typed or refused with the reason) and `PERRY_SIG_DIAG=1` (the selector's verdict per method).
119 changes: 113 additions & 6 deletions crates/perry-codegen/src/codegen/signature_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,62 @@ pub(crate) struct TypedMethod {
pub typed_trampoline: bool,
}

/// An index clone's parameter is covered by an I32 signature slot of any
/// sign. The clone proves its index parameters non-negative int32s so that an
/// element key built from them needs no sign test. Since the unsigned bound
/// (N10, `index_key::unsigned_bound`) an int32 key is bounded by one unsigned
/// compare that declines every negative key, so the sign of an I32 slot buys
/// the clone nothing a typed body lacks; the typed body also takes the
/// parameter as a word at every site, where the clone needs each site to
/// prove the whole tuple non-negative, or its guarded entry.
/// `PERRY_SIG_SIGNED_INDEX=0` (`off`, `false`) keeps the clone unless the
/// parameter's interval is non-negative.
pub(crate) fn signed_index_params() -> bool {
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ON.get_or_init(|| {
crate::expr::index_key::unsigned_bound()
&& !matches!(
std::env::var("PERRY_SIG_SIGNED_INDEX").as_deref(),
Ok("0") | Ok("off") | Ok("false")
)
})
}

/// A signature slot (`rep`, with its interval `range`) is at least as
/// precise as an index clone's parameter: an I32 whose interval is
/// non-negative, or, with `signed` (`signed_index_params`), any I32.
fn index_param_covered(rep: FactsRep, range: Option<(i64, i64)>, signed: bool) -> bool {
rep == FactsRep::I32 && (signed || range.is_some_and(|(lo, _)| lo >= 0))
}

/// The probe `PERRY_SIG_DIAG=1`: one line per method a `typed_calls` site
/// reaches and the facts give a signature, with the selector's verdict
/// (`typed=1`, or the tier or shape that refuses it).
fn probe(class: &str, method: &str, sig: &FactsSignature, refused: Option<&str>) {
if std::env::var("PERRY_SIG_DIAG").as_deref() != Ok("1") {
return;
}
let params: Vec<String> = sig
.params
.iter()
.zip(&sig.ranges)
.map(|(r, iv)| match iv {
Some((lo, hi)) => format!("{r:?}[{lo},{hi}]"),
None => format!("{r:?}"),
})
.collect();
match refused {
None => eprintln!(
"[perry sig] {class}.{method} typed=1 params=({})",
params.join(",")
),
Some(why) => eprintln!(
"[perry sig] {class}.{method} typed=0 why={why} params=({})",
params.join(",")
),
}
}

/// The typed methods of `hir` (T2), installed after every method selector
/// ran as on main; their subsumed tiers (proven-`this`, the proven-argument clone, the
/// index clones, typed_abi `typed_f64`/`typed_i32`) are dropped for them
Expand Down Expand Up @@ -328,6 +384,7 @@ pub(crate) fn select_methods(
.iter()
.any(|p| p.is_rest || p.arguments_object.is_some())
{
probe(&class.name, &m.name, sig, Some("excluded-shape"));
continue;
}
let key = (class.name.clone(), m.name.clone());
Expand All @@ -340,6 +397,7 @@ pub(crate) fn select_methods(
|| crate::collectors::ptr_array_cached_method(class, m).is_some()
|| super::arguments::method_supports_arguments_length_direct_abi(m)
{
probe(&class.name, &m.name, sig, Some("excluded-tier"));
continue;
}
let mut defaults = Vec::with_capacity(m.params.len());
Expand All @@ -357,9 +415,11 @@ pub(crate) fn select_methods(
});
}
if !ok {
probe(&class.name, &m.name, sig, Some("default"));
continue;
}
let Some(body) = sig_function(m, &defaults) else {
probe(&class.name, &m.name, sig, Some("default"));
continue;
};
let demoted = crate::collectors::callee_demoted_params(&body);
Expand All @@ -385,6 +445,7 @@ pub(crate) fn select_methods(
// routes to the boxed entry.
let tower = cm.pshape_tower_routable.contains(&key);
if tower && proven_this.is_none() {
probe(&class.name, &m.name, sig, Some("tower"));
continue;
}
// CWN 11-09 (T6 as the selector): every tier `M$sig` would
Expand All @@ -394,21 +455,30 @@ pub(crate) fn select_methods(
// proven-`this` layout, and the proven-argument clone's proven argument shapes
// (which no representation carries).
let all = |want: FactsRep| sig.params.iter().all(|r| at_least(*r, want));
let signed_index = signed_index_params();
let index_ok = cm.nonnegative_index_methods.get(&key).is_none_or(|ids| {
m.params
.iter()
.zip(sig.params.iter().zip(sig.ranges.iter()))
.all(|(p, (rep, range))| {
!ids.contains(&p.id)
|| *rep == FactsRep::I32 && range.is_some_and(|(lo, _)| lo >= 0)
!ids.contains(&p.id) || index_param_covered(*rep, *range, signed_index)
})
});
if cm.typed_i32_methods.contains(&key) && !all(FactsRep::I32)
let less_precise = if cm.typed_i32_methods.contains(&key) && !all(FactsRep::I32)
|| cm.typed_f64_methods.contains(&key) && !all(FactsRep::F64)
|| !index_ok
|| cm.pshape_methods.contains_key(&key) && proven_this.is_none()
|| cm.pshape_arg_methods.contains_key(&key)
{
Some("typed-abi-clone")
} else if !index_ok {
Some("index-clone")
} else if cm.pshape_methods.contains_key(&key) && proven_this.is_none() {
Some("proven-this")
} else if cm.pshape_arg_methods.contains_key(&key) {
Some("proven-argument")
} else {
None
};
if less_precise.is_some() {
probe(&class.name, &m.name, sig, less_precise);
continue;
}
// A signature whose every slot is Boxed changes the body only
Expand All @@ -418,8 +488,10 @@ pub(crate) fn select_methods(
&& sig.ret.encoding() == FactsEnc::Boxed
&& sig.params.iter().all(|r| r.encoding() == FactsEnc::Boxed)
{
probe(&class.name, &m.name, sig, Some("all-boxed"));
continue;
}
probe(&class.name, &m.name, sig, None);
typed.push(TypedMethod {
published_pshape: cm.pshape_methods.contains_key(&key),
index_admission: cm.nonnegative_index_methods.contains_key(&key),
Expand Down Expand Up @@ -995,3 +1067,38 @@ pub(crate) fn nonnegative_params(f: &Function, plan: &SpecFnPlan) -> HashSet<u32
.map(|((p, _), _)| p.id)
.collect()
}

#[cfg(test)]
mod tests {
use super::*;

/// An index clone proves its parameters non-negative int32s. A signature
/// covers one with an I32 slot: of any sign when the unsigned bound
/// absorbs the sign test, else only with a non-negative interval. A
/// double or Boxed slot never covers it.
#[test]
fn a_signed_i32_slot_covers_an_index_clone_parameter() {
assert!(index_param_covered(FactsRep::I32, Some((-128, 255)), true));
assert!(index_param_covered(FactsRep::I32, Some((-8, 7)), true));
assert!(index_param_covered(FactsRep::I32, None, true));
assert!(index_param_covered(FactsRep::I32, Some((0, 127)), true));
for signed in [true, false] {
assert!(!index_param_covered(FactsRep::F64, None, signed));
assert!(!index_param_covered(FactsRep::Boxed, None, signed));
assert!(index_param_covered(FactsRep::I32, Some((0, 127)), signed));
}
}

/// With the rule off (`PERRY_SIG_SIGNED_INDEX=0`, or no unsigned bound)
/// the clone stays unless the interval is non-negative: main's rule.
#[test]
fn without_the_rule_a_negative_interval_keeps_the_index_clone() {
assert!(!index_param_covered(
FactsRep::I32,
Some((-128, 255)),
false
));
assert!(!index_param_covered(FactsRep::I32, Some((-1, 7)), false));
assert!(!index_param_covered(FactsRep::I32, None, false));
}
}
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3364,7 +3364,7 @@ pub(crate) mod index_get;
pub(crate) use index_get::guarded_array::emit_spread_stack_admission;
#[cfg(test)]
mod index_get_claim_tests;
mod index_key;
pub(crate) mod index_key;
#[cfg(test)]
mod kind_entry_selection_tests;
#[cfg(test)]
Expand Down
3 changes: 2 additions & 1 deletion crates/perry-wpfacts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1005,7 +1005,7 @@ fn compute_act<'a>(
rest_fields::admit(&g, &mut templates, interp_guard.is_some())
};
rest_fields::export_reads(&g, &templates, &mut bindings);
let rep_probe = if cfg.diagnostics && !g.m.aborted {
let mut rep_probe = if cfg.diagnostics && !g.m.aborted {
let mut p = bindings::rep_probe(&g);
p.extend(rest_lanes);
if g.m.zero_sign {
Expand Down Expand Up @@ -1043,6 +1043,7 @@ fn compute_act<'a>(
&mut bindings,
interp_guard.is_some(),
cfg.sabotage.as_deref(),
cfg.diagnostics.then_some(&mut rep_probe),
);
}
if g.m.aborted {
Expand Down
Loading
Loading