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
7 changes: 7 additions & 0 deletions changelog.d/11662-number-local-shape-field-leaf.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
`h = h + o.a` on a shape-proven receiver `o = new C(...)` now lowers as a
Number accumulator: the #10777 shape-field leaf of the function-scope
Number-by-construction rule is unconditional, and the default-off
`PERRY_L14_NBC_ORDER` knob (and its build/object cache keys) is deleted. On the
5L `fpxnum` fixture the loop drops from 25 to 10 instructions per iteration, with
no `js_dynamic_string_or_number_add` call and no root barrier on `h`
(charter step 5L).
5 changes: 5 additions & 0 deletions changelog.d/11662-number-locals-masked-region-scope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
The masked-window region's flow-refined Number locals (#6750) are no longer a
separate set: a fast copy admits a refined local to its own Number-local scope
and withdraws it at the first write it cannot prove Number, so
`type_analysis::local_is_number` is the one query for every Number local and
`FnCtx::masked_region_scalar_locals` is deleted (charter step 5L, P5).
6 changes: 6 additions & 0 deletions changelog.d/11662-number-locals-one-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Codegen now answers "does this local hold a Number here?" with one query over
one scoped set: the function-wide number-by-construction locals plus the locals
a guarded loop clone admitted at its entry. The per-loop-family accumulator
lists are gone, and the answer now also reaches the raw-double store and add
paths, so a reassigned Number accumulator such as `h = h + o.a` no longer takes
a value check on every use.
1 change: 0 additions & 1 deletion crates/perry-codegen/src/codegen/closure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1188,7 +1188,6 @@ pub(super) fn compile_closure(
class_header_images: HashMap::new(),
array_length_snapshots: HashMap::new(),
string_window_array_facts: Vec::new(),
masked_region_scalar_locals: std::collections::HashSet::new(),
suppressed_cleared_shadow_slots: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
region_loops: Vec::new(),
Expand Down
2 changes: 0 additions & 2 deletions crates/perry-codegen/src/codegen/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -775,7 +775,6 @@ pub(super) fn compile_module_entry(
class_header_images: HashMap::new(),
array_length_snapshots: HashMap::new(),
string_window_array_facts: Vec::new(),
masked_region_scalar_locals: std::collections::HashSet::new(),
suppressed_cleared_shadow_slots: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
region_loops: Vec::new(),
Expand Down Expand Up @@ -1639,7 +1638,6 @@ pub(super) fn compile_module_entry(
class_header_images: HashMap::new(),
array_length_snapshots: HashMap::new(),
string_window_array_facts: Vec::new(),
masked_region_scalar_locals: std::collections::HashSet::new(),
suppressed_cleared_shadow_slots: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
region_loops: Vec::new(),
Expand Down
1 change: 0 additions & 1 deletion crates/perry-codegen/src/codegen/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1256,7 +1256,6 @@ pub(super) fn compile_function(
class_header_images: HashMap::new(),
array_length_snapshots: HashMap::new(),
string_window_array_facts: Vec::new(),
masked_region_scalar_locals: std::collections::HashSet::new(),
suppressed_cleared_shadow_slots: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
region_loops: Vec::new(),
Expand Down
1 change: 0 additions & 1 deletion crates/perry-codegen/src/codegen/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,6 @@ pub(super) fn compile_method(
class_header_images: HashMap::new(),
array_length_snapshots: HashMap::new(),
string_window_array_facts: Vec::new(),
masked_region_scalar_locals: std::collections::HashSet::new(),
suppressed_cleared_shadow_slots: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
region_loops: Vec::new(),
Expand Down
1 change: 0 additions & 1 deletion crates/perry-codegen/src/codegen/method_static.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,6 @@ pub(in crate::codegen) fn compile_static_method(
class_header_images: HashMap::new(),
array_length_snapshots: HashMap::new(),
string_window_array_facts: Vec::new(),
masked_region_scalar_locals: std::collections::HashSet::new(),
suppressed_cleared_shadow_slots: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
region_loops: Vec::new(),
Expand Down
5 changes: 1 addition & 4 deletions crates/perry-codegen/src/collectors/hir_facts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -740,10 +740,7 @@ pub(crate) fn collect_type_facts(
// `shape_proven_ptr_locals` yields empty inputs below and the fixpoint then
// computes exactly what it computed before.
let (nbc_shape_members, nbc_shape_numeric_fields) =
super::number_by_construction::shape_numeric_inputs(
&shape_proven_ptr_locals,
super::number_by_construction::nbc_order_enabled(),
);
super::number_by_construction::shape_numeric_inputs(&shape_proven_ptr_locals);
let number_by_construction_locals = super::collect_number_by_construction_locals(
stmts,
params,
Expand Down
28 changes: 3 additions & 25 deletions crates/perry-codegen/src/collectors/number_by_construction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -755,22 +755,6 @@ mod tests {

// ── #10777: shape inputs for the function-scope walk ──────────────────────

/// `PERRY_L14_NBC_ORDER` gate. **Default OFF.** When off this returns empty
/// sets, the fixpoint sees exactly what it saw before, and every emitted byte
/// is identical to the pre-fix build — the reorder in `hir_facts.rs` is pure,
/// so the knob gates the INPUTS, not the position. Keyed into the object cache
/// so a warm cache cannot serve the other arm's object.
pub(crate) fn nbc_order_enabled() -> bool {
use std::sync::OnceLock;
static CACHED: OnceLock<bool> = OnceLock::new();
*CACHED.get_or_init(|| {
matches!(
std::env::var("PERRY_L14_NBC_ORDER").as_deref(),
Ok("1") | Ok("on") | Ok("true")
)
})
}

/// Turn the receiver proofs into the `(members, numeric_fields)` pair the
/// function-scope fixpoint needs.
///
Expand All @@ -790,13 +774,10 @@ pub(crate) fn nbc_order_enabled() -> bool {
///
/// A union would be a WRONG ANSWER, not a weaker one: `a` numeric on `C` and
/// not on `D` would license a bare `fadd` on `D.a`.
/// The gate is passed in so both modes can be tested without changing the
/// process environment shared by parallel unit tests.
pub(crate) fn shape_numeric_inputs(
shape_proven: &HashMap<u32, crate::collectors::ptr_shape::PtrShapeLocal>,
enabled: bool,
) -> (HashSet<u32>, HashSet<String>) {
if !enabled || shape_proven.is_empty() {
if shape_proven.is_empty() {
return (HashSet::new(), HashSet::new());
}
let mut members: HashSet<u32> = HashSet::new();
Expand Down Expand Up @@ -855,7 +836,7 @@ mod shape_input_tests {
}

#[test]
fn nbc_order_intersects_numeric_fields_before_proving_property_locals() {
fn shape_inputs_intersect_numeric_fields_before_proving_property_locals() {
let shape_proven = HashMap::from([
(
10,
Expand All @@ -882,7 +863,7 @@ mod shape_input_tests {
property_local(24, 12, "shared"),
];

let (members, fields) = shape_numeric_inputs(&shape_proven, true);
let (members, fields) = shape_numeric_inputs(&shape_proven);
let numeric = numeric_locals(&stmts, &members, &fields);
assert!(numeric.contains(&20), "shared field on First is numeric");
assert!(numeric.contains(&21), "shared field on Second is numeric");
Expand All @@ -898,8 +879,5 @@ mod shape_input_tests {
!numeric.contains(&24),
"an unproven receiver is not a numeric input"
);

let (off_members, off_fields) = shape_numeric_inputs(&shape_proven, false);
assert!(numeric_locals(&stmts, &off_members, &off_fields).is_empty());
}
}
64 changes: 63 additions & 1 deletion crates/perry-codegen/src/collectors/receiver_regions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,15 @@ enum ActiveReceiverData {
#[derive(Debug, Default)]
pub(crate) struct ReceiverDescriptorTable {
entries: Vec<ActiveReceiverDescriptor>,
/// 5L (step5 DESIGN §4.1): the scoped Number-local sets, innermost last.
/// Each is the set a guarded clone proved for its own body: the locals
/// its entry test admitted and whose every in-clone write is
/// Number-preserving. The function scope is the static
/// `number_by_construction_locals`; `type_analysis::local_is_number` is
/// the one query over both. A scope ends with `dematerialize_scope`, like
/// every other scoped payload here, so the slow clone and post-loop code
/// never see it.
number_locals: Vec<(u32, Vec<u32>)>,
}

impl ReceiverDescriptorTable {
Expand Down Expand Up @@ -712,10 +721,63 @@ impl ReceiverDescriptorTable {
})
}

/// Open the Number-local scope of one guarded clone: `locals` hold a
/// Number at every read inside it. Empty sets are not recorded.
pub(crate) fn materialize_number_locals(&mut self, scope_id: u32, locals: &[u32]) {
if !locals.is_empty() {
self.number_locals.push((scope_id, locals.to_vec()));
}
}

/// Admit `local` to the Number set of the open scope `scope_id` at a flow
/// point inside it: a masked-window refinement lands strictly after the
/// statement that wrote a Number, and holds until a write the region cannot
/// prove Number withdraws it. Returns whether the local was newly admitted.
pub(crate) fn admit_number_local(&mut self, scope_id: u32, local: u32) -> bool {
if let Some((_, locals)) = self
.number_locals
.iter_mut()
.find(|(scope, _)| *scope == scope_id)
{
if locals.contains(&local) {
return false;
}
locals.push(local);
return true;
}
self.number_locals.push((scope_id, vec![local]));
true
}

/// Withdraw `local` from the Number set of scope `scope_id` (the write just
/// lowered is not provably a Number). Returns whether it was a member.
/// Another open scope that proved it by its own fixed point keeps it.
pub(crate) fn withdraw_number_local(&mut self, scope_id: u32, local: u32) -> bool {
let Some((_, locals)) = self
.number_locals
.iter_mut()
.find(|(scope, _)| *scope == scope_id)
else {
return false;
};
let before = locals.len();
locals.retain(|member| *member != local);
before != locals.len()
}

/// Whether an active clone scope proved `local` a Number.
pub(crate) fn local_is_number_in_scope(&self, local: u32) -> bool {
self.number_locals
.iter()
.any(|(_, locals)| locals.contains(&local))
}

/// End every descriptor fact owned by a lexical proof scope. Each Phase 4
/// migration adds its scoped payload here, replacing a separate
/// `retain(scope_id)` discipline at the lowering site.
/// `retain(scope_id)` discipline at the lowering site. Returns the number
/// of receiver entries removed; the scope's Number-local set ends too.
pub(crate) fn dematerialize_scope(&mut self, scope_id: u32) -> usize {
self.number_locals.retain(|(scope, _)| *scope != scope_id);
let before = self.entries.len();
self.entries.retain(|entry| {
let active_scope = match &entry.data {
Expand Down
41 changes: 38 additions & 3 deletions crates/perry-codegen/src/collectors/receiver_regions_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,6 @@ fn representation_payloads_use_the_common_scope_boundary() {
store_side_exit_label: "slow".into(),
array_kind: crate::expr::PackedNumericLoopKind::F64,
allow_holes: false,
numeric_accumulators: vec![NUM2],
window_validated: true,
affine_indices: false,
});
Expand All @@ -327,7 +326,6 @@ fn representation_payloads_use_the_common_scope_boundary() {
.next()
.expect("packed descriptor must be queryable");
assert_eq!(fact.array_local_id, OBJ);
assert_eq!(fact.numeric_accumulators, vec![NUM2]);
assert!(table.has_packed_f64_loop_facts());
table.materialize_masked_window_array(crate::expr::MaskedWindowArrayFact {
array_local_id: OBJ + 1,
Expand All @@ -336,7 +334,6 @@ fn representation_payloads_use_the_common_scope_boundary() {
min_idx: 0,
max_idx_exclusive: 16,
values_i32: false,
numeric_accumulators: Vec::new(),
elem: crate::expr::MaskedWindowElem::PlainF64,
allows_stores: false,
});
Expand Down Expand Up @@ -1218,3 +1215,41 @@ fn the_inventory_covers_every_claim_kind_and_every_boundary_mechanism() {
"inventory size changed — see FnCtx declarations"
);
}

#[test]
fn a_number_local_scope_ends_with_its_clone() {
// 5L: a clone-scoped Number local is visible only while its clone lowers;
// the slow clone and post-loop code must not see it.
let mut table = ReceiverDescriptorTable::default();
table.materialize_number_locals(5, &[NUM]);
table.materialize_number_locals(6, &[NUM2]);
table.materialize_number_locals(7, &[]);
assert!(table.local_is_number_in_scope(NUM));
assert!(table.local_is_number_in_scope(NUM2));
assert_eq!(table.dematerialize_scope(6), 0);
assert!(table.local_is_number_in_scope(NUM));
assert!(!table.local_is_number_in_scope(NUM2));
table.dematerialize_scope(5);
assert!(!table.local_is_number_in_scope(NUM));
}

#[test]
fn a_flow_refined_number_local_joins_and_leaves_its_copy_scope() {
// 5L: a masked-window fast copy admits a local at its refinement point and
// withdraws it at the first write it cannot prove Number. Withdrawal from
// one scope leaves another scope's own proof standing.
let mut table = ReceiverDescriptorTable::default();
assert!(table.admit_number_local(9, NUM));
assert!(!table.admit_number_local(9, NUM));
assert!(table.local_is_number_in_scope(NUM));
assert!(table.withdraw_number_local(9, NUM));
assert!(!table.local_is_number_in_scope(NUM));
assert!(!table.withdraw_number_local(9, NUM));
table.materialize_number_locals(5, &[NUM2]);
assert!(table.admit_number_local(9, NUM2));
assert!(table.withdraw_number_local(9, NUM2));
assert!(table.local_is_number_in_scope(NUM2));
assert!(table.admit_number_local(9, NUM));
table.dematerialize_scope(9);
assert!(!table.local_is_number_in_scope(NUM));
}
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/expr/literals_vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
// arms below keep their calls.
let needs_numeric_coerce = !ctx.integer_locals.contains(id)
&& !ctx.unsigned_i32_locals.contains(id)
&& !ctx.number_by_construction_locals.contains(id);
&& !crate::type_analysis::local_is_number(ctx, *id);
let is_increment_arg = match op {
UpdateOp::Increment => "1",
UpdateOp::Decrement => "0",
Expand Down
Loading
Loading