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
17 changes: 17 additions & 0 deletions changelog.d/6911-repsel-p3b-shape-proven-objects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
perf(codegen): representation-selection Phase 3b — shape-proven object locals (`Ptr<Shape>`)

For a function-local proven to hold exactly one `new C(...)` object with a
statically-immutable shape (provenance + containment + `this`-flow + dispatch
stability, with a module-wide first-increment kill on any
defineProperty/delete/setPrototypeOf/Proxy/mutating-Reflect site), field
accesses lower to the bare fixed-offset form — no per-access guard diamond, no
volatile gate, no fallback arm, no phi — and method calls dispatch directly
with no shape guard. Anon-shape record literals and extends chains qualify;
the typed-receiver f64 method clone is widened from extends-free classes to
fully-modeled chains with chain-global field indexes. Raw-f64 stores keep the
plain-finite check with a boxed-setter downgrade side exit; boxed stores keep
the generational write barrier; the local's slot stays a tagged-at-rest,
shadow-bound GC root (raw pointers never stored at rest; the mark/rewrite
raw-asymmetry is tracked as #6910). Gated by `PERRY_PTR_SHAPE_LOCALS`
(default on, object-cache keyed). Implements Phase 3b of
`docs/representation-selection-rfc.md`.
17 changes: 13 additions & 4 deletions crates/perry-codegen/src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1340,6 +1340,13 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
let mut typed_string_methods = std::collections::HashSet::new();
let mut typed_i1_method_param_reps = std::collections::HashMap::new();
let mut typed_f64_receiver_methods = std::collections::HashMap::new();
// Phase 3b typed-receiver widening: chain-global field indexes need the
// full class table — and it must be the SAME table dynamic dispatch's
// call-site gating consults (`class_table`, incl. class-expression
// aliases), or a chain resolvable only through an alias would gate a
// clone call the emission loop never produced (undefined symbol at
// link).
let receiver_class_table = &class_table;
for class in &hir.classes {
for method in &class.methods {
let source_function = format!("{}::{}", class.name, method.name);
Expand All @@ -1364,15 +1371,17 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
],
),
}
match typed_abi::typed_f64_receiver_method_info(class, method) {
match typed_abi::typed_f64_receiver_method_info(class, method, receiver_class_table) {
Some(info) => {
typed_f64_receiver_methods
.insert((class.name.clone(), method.name.clone()), info);
}
None => {
if let Some(reason) =
typed_abi::typed_f64_receiver_method_rejection_reason(class, method)
{
if let Some(reason) = typed_abi::typed_f64_receiver_method_rejection_reason(
class,
method,
&receiver_class_table,
) {
record_typed_clone_rejection(
&mut typed_clone_rejection_records,
source_function.clone(),
Expand Down
151 changes: 112 additions & 39 deletions crates/perry-codegen/src/codegen/typed_abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@ pub(crate) enum TypedCloneRejectionReason {
ReceiverClassHasAccessor,
ReceiverClassHasComputedMember,
ReceiverClassHasComputedField,
/// A subclass re-declares a parent chain field name: the flattened slot
/// layout would be ambiguous (Phase 3b chain widening).
ReceiverFieldShadowed,
ReceiverFieldNotOwn,
ReceiverFieldNotF64,
ThisEscape,
Expand Down Expand Up @@ -276,6 +279,7 @@ impl TypedCloneRejectionReason {
Self::ReceiverClassHasAccessor => "receiver_class_has_accessor",
Self::ReceiverClassHasComputedMember => "receiver_class_has_computed_member",
Self::ReceiverClassHasComputedField => "receiver_class_has_computed_field",
Self::ReceiverFieldShadowed => "receiver_field_shadowed",
Self::ReceiverFieldNotOwn => "receiver_field_not_own",
Self::ReceiverFieldNotF64 => "receiver_field_not_f64",
Self::ThisEscape => "this_escape",
Expand Down Expand Up @@ -468,15 +472,17 @@ pub(crate) fn typed_f64_method_rejection_reason(
pub(crate) fn typed_f64_receiver_method_rejection_reason(
class: &perry_hir::Class,
method: &Function,
classes: &HashMap<String, &perry_hir::Class>,
) -> Option<TypedCloneRejectionReason> {
typed_f64_receiver_method_candidate(class, method).err()
typed_f64_receiver_method_candidate(class, method, classes).err()
}

pub(crate) fn typed_f64_receiver_method_info(
class: &perry_hir::Class,
method: &Function,
classes: &HashMap<String, &perry_hir::Class>,
) -> Option<TypedReceiverMethodInfo> {
typed_f64_receiver_method_candidate(class, method).ok()
typed_f64_receiver_method_candidate(class, method, classes).ok()
}

pub(crate) fn typed_i1_method_rejection_reason(
Expand Down Expand Up @@ -887,29 +893,92 @@ fn integer_literal_fits_i32(n: i64) -> bool {
(i64::from(i32::MIN)..=i64::from(i32::MAX)).contains(&n)
}

fn typed_receiver_own_field_index(
class: &perry_hir::Class,
property: &str,
) -> Result<u32, TypedCloneRejectionReason> {
let mut index = 0u32;
for field in &class.fields {
if field.key_expr.is_some() {
/// Flattened, chain-global field view of a receiver class: `(global slot
/// index, field)` in allocation order — parent-chain fields FIRST, matching
/// `class_field_global_index` / `js_object_alloc_with_parent` slot layout.
pub(crate) struct TypedReceiverChainFields<'a> {
fields: Vec<(u32, &'a perry_hir::ClassField)>,
}

/// Build the chain-global field view, applying the receiver-shape admission
/// checks to EVERY link of the chain (representation-selection Phase 3b
/// widening of the original `extends_name.is_none()` restriction): each link
/// must be a modeled, statically-extended user class with no accessors, no
/// computed members, and no computed field keys; duplicate field names across
/// the chain (a subclass shadowing a parent field) are rejected — the slot
/// layout would be ambiguous.
fn typed_receiver_chain_fields<'a>(
classes: &HashMap<String, &'a perry_hir::Class>,
class: &'a perry_hir::Class,
) -> Result<TypedReceiverChainFields<'a>, TypedCloneRejectionReason> {
// Chain, self first.
let mut chain: Vec<&perry_hir::Class> = Vec::new();
let mut current: Option<&perry_hir::Class> = Some(class);
let mut seen: HashSet<&str> = HashSet::new();
while let Some(link) = current {
if !seen.insert(link.name.as_str()) || chain.len() > 64 {
return Err(TypedCloneRejectionReason::ReceiverClassExtends);
}
if link.extends_expr.is_some()
|| link.native_extends.is_some()
|| link.heritage_lexically_shadowed
|| (link.extends.is_some() && link.extends_name.is_none())
{
return Err(TypedCloneRejectionReason::ReceiverClassExtends);
}
if !link.getters.is_empty() || !link.setters.is_empty() {
return Err(TypedCloneRejectionReason::ReceiverClassHasAccessor);
}
if !link.computed_members.is_empty() {
return Err(TypedCloneRejectionReason::ReceiverClassHasComputedMember);
}
if link.fields.iter().any(|field| field.key_expr.is_some()) {
return Err(TypedCloneRejectionReason::ReceiverClassHasComputedField);
}
chain.push(link);
current = match link.extends_name.as_deref() {
Some(parent) => match classes.get(parent) {
Some(parent_class) => Some(*parent_class),
None => return Err(TypedCloneRejectionReason::ReceiverClassExtends),
},
None => None,
};
}
// Parent fields first: walk root-most ancestor down to self.
let mut fields: Vec<(u32, &perry_hir::ClassField)> = Vec::new();
let mut names: HashSet<&str> = HashSet::new();
let mut index = 0u32;
for link in chain.iter().rev() {
for field in &link.fields {
if !names.insert(field.name.as_str()) {
return Err(TypedCloneRejectionReason::ReceiverFieldShadowed);
}
fields.push((index, field));
index += 1;
}
}
Ok(TypedReceiverChainFields { fields })
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

fn typed_receiver_chain_field_index(
chain_fields: &TypedReceiverChainFields<'_>,
property: &str,
) -> Result<u32, TypedCloneRejectionReason> {
for (index, field) in &chain_fields.fields {
if field.name == property {
if crate::typed_shape::type_is_raw_f64_candidate(&field.ty) {
return Ok(index);
return Ok(*index);
}
return Err(TypedCloneRejectionReason::ReceiverFieldNotF64);
}
index += 1;
}
Err(TypedCloneRejectionReason::ReceiverFieldNotOwn)
}

fn typed_f64_receiver_method_candidate(
class: &perry_hir::Class,
method: &Function,
classes: &HashMap<String, &perry_hir::Class>,
) -> Result<TypedReceiverMethodInfo, TypedCloneRejectionReason> {
if method.is_async || method.is_generator || method.was_plain_async {
return Err(TypedCloneRejectionReason::AsyncOrGenerator);
Expand All @@ -920,21 +989,9 @@ fn typed_f64_receiver_method_candidate(
if !is_f64_type(&method.return_type) {
return Err(TypedCloneRejectionReason::ReturnTypeNotF64);
}
// Keep this first slice exact: only methods on a final known receiver shape
// with own string-keyed fields. Parent field offsets and inherited method
// resolution remain on the generic ABI until the proof is widened.
if class.extends_name.is_some() || class.extends.is_some() || class.extends_expr.is_some() {
return Err(TypedCloneRejectionReason::ReceiverClassExtends);
}
if !class.getters.is_empty() || !class.setters.is_empty() {
return Err(TypedCloneRejectionReason::ReceiverClassHasAccessor);
}
if !class.computed_members.is_empty() {
return Err(TypedCloneRejectionReason::ReceiverClassHasComputedMember);
}
if class.fields.iter().any(|field| field.key_expr.is_some()) {
return Err(TypedCloneRejectionReason::ReceiverClassHasComputedField);
}
// Phase 3b widening: extends chains are admitted when every link passes
// the receiver-shape checks; field indexes are chain-global.
let chain_fields = typed_receiver_chain_fields(classes, class)?;

let mut locals = HashMap::new();
for param in &method.params {
Expand All @@ -956,7 +1013,7 @@ fn typed_f64_receiver_method_candidate(
let mut used_fields = Vec::new();
let mut used_field_names = HashSet::new();
typed_f64_receiver_body_rejection_reason(
class,
&chain_fields,
&method.body,
locals,
&mut used_fields,
Expand All @@ -971,7 +1028,7 @@ fn typed_f64_receiver_method_candidate(
}

fn typed_f64_receiver_body_rejection_reason(
class: &perry_hir::Class,
chain_fields: &TypedReceiverChainFields<'_>,
body: &[Stmt],
mut locals: HashMap<u32, TypedParamRep>,
used_fields: &mut Vec<TypedReceiverField>,
Expand All @@ -990,7 +1047,7 @@ fn typed_f64_receiver_body_rejection_reason(
..
} if is_f64_type(ty)
&& receiver_expr_is_typed_f64_safe(
class,
chain_fields,
expr,
&locals,
used_fields,
Expand All @@ -1007,17 +1064,21 @@ fn typed_f64_receiver_body_rejection_reason(
}
}
match last {
Stmt::Return(Some(expr)) => {
receiver_expr_is_typed_f64_safe(class, expr, &locals, used_fields, used_field_names)
.map(|_| ())
.map_err(|_| TypedCloneRejectionReason::ReturnExprNotTypedF64Safe)
}
Stmt::Return(Some(expr)) => receiver_expr_is_typed_f64_safe(
chain_fields,
expr,
&locals,
used_fields,
used_field_names,
)
.map(|_| ())
.map_err(|_| TypedCloneRejectionReason::ReturnExprNotTypedF64Safe),
_ => Err(TypedCloneRejectionReason::BodyNotSingleReturn),
}
}

fn receiver_expr_is_typed_f64_safe(
class: &perry_hir::Class,
chain_fields: &TypedReceiverChainFields<'_>,
expr: &Expr,
locals: &HashMap<u32, TypedParamRep>,
used_fields: &mut Vec<TypedReceiverField>,
Expand All @@ -1030,7 +1091,7 @@ fn receiver_expr_is_typed_f64_safe(
Expr::PropertyGet {
object, property, ..
} if matches!(object.as_ref(), Expr::This) => {
let index = typed_receiver_own_field_index(class, property)?;
let index = typed_receiver_chain_field_index(chain_fields, property)?;
if used_field_names.insert(property.clone()) {
used_fields.push(TypedReceiverField {
name: property.clone(),
Expand All @@ -1043,7 +1104,7 @@ fn receiver_expr_is_typed_f64_safe(
Expr::Unary { op, operand } => {
if matches!(op, UnaryOp::Pos | UnaryOp::Neg) {
receiver_expr_is_typed_f64_safe(
class,
chain_fields,
operand,
locals,
used_fields,
Expand All @@ -1060,8 +1121,20 @@ fn receiver_expr_is_typed_f64_safe(
) {
return Err(TypedCloneRejectionReason::ReturnExprNotTypedF64Safe);
}
receiver_expr_is_typed_f64_safe(class, left, locals, used_fields, used_field_names)?;
receiver_expr_is_typed_f64_safe(class, right, locals, used_fields, used_field_names)
receiver_expr_is_typed_f64_safe(
chain_fields,
left,
locals,
used_fields,
used_field_names,
)?;
receiver_expr_is_typed_f64_safe(
chain_fields,
right,
locals,
used_fields,
used_field_names,
)
}
_ => Err(TypedCloneRejectionReason::ReturnExprNotTypedF64Safe),
}
Expand Down
25 changes: 25 additions & 0 deletions crates/perry-codegen/src/collectors/hir_facts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@ pub(crate) struct ShapeStabilityFacts {
// codegen consumer reads it yet.
#[allow(dead_code)]
pub scalar_replaceable_object_locals: HashSet<u32>,
/// Representation-selection Phase 3b: function-locals proven to hold
/// exactly one object of a statically-immutable shape for their entire
/// lifetime (`collectors/ptr_shape.rs`). Consumers: guard-free fixed-
/// offset field access (`expr/property_get.rs`, `expr/property_set.rs`)
/// and unguarded direct method dispatch
/// (`lower_call/property_get/dynamic_dispatch.rs`).
pub shape_proven_ptr_locals: HashMap<u32, super::PtrShapeLocal>,
}

#[derive(Debug, Clone, Default)]
Expand Down Expand Up @@ -296,6 +303,12 @@ impl TypeFacts {
&self.shape_stability.scalar_replaceable_object_locals
}

/// Representation-selection Phase 3b: the shape-proof fact for a local,
/// when it is a proven `Ptr<Shape>` local (`collectors/ptr_shape.rs`).
pub(crate) fn shape_proven_ptr_local(&self, local_id: u32) -> Option<&super::PtrShapeLocal> {
self.shape_stability.shape_proven_ptr_locals.get(&local_id)
}

pub(crate) fn proves_scalar_replacement(&self, local_id: u32) -> bool {
self.shape_stability
.scalar_replaceable_object_locals
Expand Down Expand Up @@ -423,6 +436,17 @@ pub(crate) fn collect_type_facts(
.chain(non_escaping_object_literals.keys())
.copied()
.collect();
// Representation-selection Phase 3b: shape-proven pointer locals. Gated
// on `PERRY_PTR_SHAPE_LOCALS` and the module-wide §5.2 barrier scan
// inside the collector.
let shape_proven_ptr_locals = super::ptr_shape::collect_shape_proven_ptr_locals(
stmts,
boxed_vars,
module_globals,
classes,
module_dispatch,
&not_bigint_locals,
);
let graph = TypeFacts {
representation: RepresentationFacts {
integer_locals: integer_locals.clone(),
Expand Down Expand Up @@ -460,6 +484,7 @@ pub(crate) fn collect_type_facts(
},
shape_stability: ShapeStabilityFacts {
scalar_replaceable_object_locals,
shape_proven_ptr_locals,
},
materialization_hazards,
};
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/collectors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ mod local_refs;
mod mutation;
mod not_bigint_locals;
mod pointer_locals;
mod ptr_shape;
mod refs;
mod scalar_method_dispatch;
mod scalar_methods;
Expand Down Expand Up @@ -60,6 +61,7 @@ pub(crate) use integer_locals::{
pub(crate) use local_refs::{expr_contains_local_get, mark_all_candidate_refs_in_expr};
pub(crate) use mutation::has_any_mutation;
pub(crate) use pointer_locals::collect_pointer_typed_locals;
pub(crate) use ptr_shape::PtrShapeLocal;
pub(crate) use refs::{
collect_let_ids, collect_ref_ids_in_expr, collect_ref_ids_in_stmts, is_clamp_call,
};
Expand Down
Loading
Loading