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-p3-b3-delete-int32-consumers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Performance and cleanup: compiled code no longer tests for, or decodes, an INT32-tagged Number. Since P3 B4a nothing can produce one, so a Number has one encoding, the plain double, for every operand, proven or not. Removed from generated code: the INT32 arms of `===`/`!==` and of the relational operators against a Number, the per-local decode of a comparison graph, the boxed arm of the shared array-key normalizer, the INT32 half of the typed-entry and direct-closure-call guards and their convert block, the switch discriminant's canonical view (a numeric `switch` now compares the discriminant directly), the Number-parameter entry conversion, and the Number receiver's INT32 test. `PERRY_WP_REP_P3` is gone: it restored a tag test that no longer exists. The runtime's own INT32 readers are unchanged (P3 B4b removes them). `PERRY_VERIFY_FACTS=1` still checks every trusted Number is a plain double (P3 B3).
11 changes: 0 additions & 11 deletions crates/perry-codegen/src/codegen/closure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -698,17 +698,6 @@ pub(super) fn compile_closure(
private_resume_frame,
private_async_resume_frame,
);
// A closure region's Number parameter: an int32 box becomes the
// equal plain double once, here (`split_regions`).
let arg_name = if region_number_params.contains(&p.id) {
let is_int = super::typed_entry::emit_int32_box_test(blk, &arg_name);
let bits = blk.bitcast_double_to_i64(&arg_name);
let low = blk.trunc(I64, &bits, I32);
let as_f64 = blk.sitofp(I32, &low, DOUBLE);
blk.select(I1, &is_int, DOUBLE, &as_f64, &arg_name)
} else {
arg_name
};
let slot = super::arguments::store_param_slot(blk, p, &closure_boxed_vars, &arg_name);
if let Some(slot_idx) = shadow_slot_map
.get(&p.id)
Expand Down
11 changes: 5 additions & 6 deletions crates/perry-codegen/src/codegen/closure/trampoline.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
//! Raw-return branches of typed closure entry trampolines.
use super::super::typed_abi::{TypedFunctionTrampolineKind, TypedParamRep};
use super::super::typed_entry::emit_typed_arg_to_raw_after_entry_tier;
use super::super::typed_abi::{emit_typed_arg_to_raw, TypedFunctionTrampolineKind, TypedParamRep};
use crate::types::{LlvmType, DOUBLE, I1, I32, I64};

pub(super) fn emit_typed_closure_trampoline_fast_value(
Expand All @@ -16,7 +15,7 @@ pub(super) fn emit_typed_closure_trampoline_fast_value(
let raw_args: Vec<String> = arg_names
.iter()
.zip(arg_reps.iter())
.map(|(arg, rep)| emit_typed_arg_to_raw_after_entry_tier(blk, *rep, arg))
.map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg))
.collect();
let mut typed_args: Vec<(LlvmType, &str)> = Vec::with_capacity(raw_args.len() + 1);
typed_args.push((I64, "%this_closure"));
Expand All @@ -33,7 +32,7 @@ pub(super) fn emit_typed_closure_trampoline_fast_value(
let raw_args: Vec<String> = arg_names
.iter()
.zip(arg_reps.iter())
.map(|(arg, rep)| emit_typed_arg_to_raw_after_entry_tier(blk, *rep, arg))
.map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg))
.collect();
let mut typed_args: Vec<(LlvmType, &str)> = Vec::with_capacity(raw_args.len() + 1);
typed_args.push((I64, "%this_closure"));
Expand All @@ -50,7 +49,7 @@ pub(super) fn emit_typed_closure_trampoline_fast_value(
let raw_args: Vec<String> = arg_names
.iter()
.zip(arg_reps.iter())
.map(|(arg, rep)| emit_typed_arg_to_raw_after_entry_tier(blk, *rep, arg))
.map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg))
.collect();
let mut typed_args: Vec<(LlvmType, &str)> = Vec::with_capacity(raw_args.len() + 1);
typed_args.push((I64, "%this_closure"));
Expand All @@ -68,7 +67,7 @@ pub(super) fn emit_typed_closure_trampoline_fast_value(
let raw_args: Vec<String> = arg_names
.iter()
.zip(arg_reps.iter())
.map(|(arg, rep)| emit_typed_arg_to_raw_after_entry_tier(blk, *rep, arg))
.map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg))
.collect();
let mut typed_args: Vec<(LlvmType, &str)> = Vec::with_capacity(raw_args.len() + 1);
typed_args.push((I64, "%this_closure"));
Expand Down
11 changes: 2 additions & 9 deletions crates/perry-codegen/src/codegen/facts_const_length.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use std::cell::Cell;
use perry_hir::Expr;

use crate::expr::FnCtx;
use crate::types::{DOUBLE, I32, I64};
use crate::types::{DOUBLE, I32};

pub(crate) fn enabled() -> bool {
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
Expand Down Expand Up @@ -67,14 +67,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> anyhow::Result<Option<S
let actual = crate::expr::property_get::lower(ctx, expr);
WITNESSING.with(|w| w.set(false));
let actual = actual?;
// The ordinary read may hand back an INT32 box.
let blk = ctx.block();
let bits = blk.bitcast_double_to_i64(&actual);
let is_int = super::emit_int32_box_test(blk, &actual);
let low = blk.trunc(I64, &bits, I32);
let widened = blk.sitofp(I32, &low, DOUBLE);
let number = blk.select(crate::types::I1, &is_int, DOUBLE, &widened, &actual);
let same = blk.fcmp("oeq", &number, &constant);
let same = ctx.block().fcmp("oeq", &actual, &constant);
crate::verify_facts::tick(ctx);
let miss = ctx.new_block("const.length.verify_miss");
let next = ctx.new_block("const.length.admitted");
Expand Down
35 changes: 9 additions & 26 deletions crates/perry-codegen/src/codegen/facts_int32_locals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
//! converts with `fptosi` instead of ToInt32 (`store_from_double`).
//!
//! Check mode (`PERRY_VERIFY_FACTS=1`) witnesses every store: the stored value
//! must pass the exact-i32 test (`emit_typed_i32_plain_guard_and_raw`; an INT32
//! box fails it, P3 `facts_rep::canonical_numbers`). Stores the
//! must pass the exact-i32 test (`emit_typed_i32_plain_guard_and_raw`). Stores the
//! ordinary lowering would compute natively in i32 take the double path
//! there, so no store escapes the witness (`checked`).
//!
Expand All @@ -20,7 +19,7 @@ use std::collections::{HashMap, HashSet};

use perry_hir::Stmt;

use crate::types::{DOUBLE, I32, I64};
use crate::types::{DOUBLE, I32};

pub(crate) fn enabled() -> bool {
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
Expand Down Expand Up @@ -87,14 +86,9 @@ pub(crate) fn witness_exact(ctx: &mut crate::expr::FnCtx<'_>, value: &str, id: u
if !crate::verify_facts::enabled() {
return;
}
// A proven Number is a canonical double (P3, `facts_rep`): the witness
// admits an exact int32 double only, never an INT32 box.
let exact = if super::facts_rep::canonical_numbers() {
let bits = ctx.block().bitcast_double_to_i64(value);
super::typed_abi::emit_typed_i32_plain_guard_and_raw(ctx.block(), value, &bits).0
} else {
super::typed_abi::emit_typed_i32_guard_and_raw(ctx.block(), value).0
};
// A Number is a plain double: the witness admits an exact int32 double.
let bits = ctx.block().bitcast_double_to_i64(value);
let exact = super::typed_abi::emit_typed_i32_plain_guard_and_raw(ctx.block(), value, &bits).0;
crate::verify_facts::tick(ctx);
let miss = ctx.new_block("int32.input.verify_miss");
let next = ctx.new_block("int32.input.admitted");
Expand All @@ -121,11 +115,9 @@ pub(crate) fn witness_exact(ctx: &mut crate::expr::FnCtx<'_>, value: &str, id: u
}

/// The i32 of a double stored into proven local `id`, if it is one: an exact
/// int32, so `fptosi`. The value is a proven Number, a canonical double (P3,
/// `facts_rep::canonical_numbers`); with `PERRY_WP_REP_P3=0` an INT32 box
/// converts through its payload. The HIR's release of a temp (`x =
/// undefined` after its last read, which wpfacts skips) keeps the ordinary
/// NaN-safe conversion.
/// int32, so `fptosi`. The value is a proven Number, a plain double. The
/// HIR's release of a temp (`x = undefined` after its last read, which
/// wpfacts skips) keeps the ordinary NaN-safe conversion.
pub(crate) fn store_from_double(
ctx: &mut crate::expr::FnCtx<'_>,
value: &str,
Expand All @@ -136,16 +128,7 @@ pub(crate) fn store_from_double(
return None;
}
witness(ctx, value, id);
if super::facts_rep::canonical_numbers() {
return Some(ctx.block().fptosi(DOUBLE, value, I32));
}
let blk = ctx.block();
let bits = blk.bitcast_double_to_i64(value);
let is_int = super::emit_int32_box_test(blk, value);
let low = blk.trunc(I64, &bits, I32);
let safe = blk.select(crate::types::I1, &is_int, DOUBLE, "0.0", value);
let plain = blk.fptosi(DOUBLE, &safe, I32);
Some(blk.select(crate::types::I1, &is_int, I32, &low, &plain))
Some(ctx.block().fptosi(DOUBLE, value, I32))
}

#[cfg(test)]
Expand Down
26 changes: 3 additions & 23 deletions crates/perry-codegen/src/codegen/facts_number_locals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@
//! build checks every consumed claim, at the parameter entry and at each
//! store into a proven `let` (`accept_let_value`).
//!
//! A parameter the body never rebinds or mutates converts an INT32 box to its
//! plain double at entry, as a split region's Number parameter does, and is
//! then a Number by construction: the entry conversion is its only write. A
//! A parameter the body never rebinds or mutates is a Number by construction:
//! its argument, a plain double, is its only write. A
//! `let` only answers `local_is_number` (`FnCtx::facts_number_locals`). It
//! does not join `number_by_construction_locals`, whose members also promise
//! that every write's operands are Numbers: `total += x.length` keeps
Expand All @@ -29,7 +28,7 @@ use std::collections::{HashMap, HashSet};

use perry_hir::{Param, Stmt};

use crate::types::{DOUBLE, I1, I32, I64};
use crate::types::DOUBLE;

pub(crate) fn enabled() -> bool {
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
Expand Down Expand Up @@ -168,22 +167,3 @@ pub(crate) fn none() -> &'static HashSet<u32> {
static NONE: std::sync::OnceLock<HashSet<u32>> = std::sync::OnceLock::new();
NONE.get_or_init(HashSet::new)
}

/// The plain double of a Number argument: an INT32 box converts, every other
/// Number is its own bits.
///
/// P3 B1, C4 (DESIGN-p3 rev 3.1 §2.3): a Number the facts prove is a plain
/// double (every value reaching the parameter is a program Number or an
/// audited builtin NUM, `perry-wpfacts/NUM_SEEDS.md`), so with
/// `PERRY_FACTS_CANONICAL_NUMBERS` on the argument is taken as it is. The
/// entry witness (`emit_param_witnesses`) checks it in check mode.
pub(crate) fn emit_entry_number(blk: &mut crate::block::LlBlock, arg: &str) -> String {
if crate::expr::facts_canonical::enabled() {
return arg.to_string();
}
let is_int = super::typed_entry::emit_int32_box_test(blk, arg);
let bits = blk.bitcast_double_to_i64(arg);
let low = blk.trunc(I64, &bits, I32);
let as_f64 = blk.sitofp(I32, &low, DOUBLE);
blk.select(I1, &is_int, DOUBLE, &as_f64, arg)
}
21 changes: 6 additions & 15 deletions crates/perry-codegen/src/codegen/facts_number_locals_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,21 +118,12 @@ fn proven_bindings_take_raw_double_ops_and_own_no_root_slot() {
0,
"a proven Number binding never holds a pointer:\n{body}"
);
// P3 B1, C4: a proven Number is a plain double (the NUM fact is audited,
// `perry-wpfacts/NUM_SEEDS.md`), so the parameter takes its argument's bits
// with no INT32 decode at entry; the entry witness checks it in check
// mode. `PERRY_FACTS_CANONICAL_NUMBERS=0` restores the decode.
if crate::expr::facts_canonical::enabled() {
assert!(
!body.contains(crate::nanbox::INT32_TAG_I64),
"a proven parameter takes its argument's bits at entry:\n{body}"
);
} else {
assert!(
body.contains(crate::nanbox::INT32_TAG_I64),
"a proven parameter converts an INT32 box at entry:\n{body}"
);
}
// A Number is a plain double, so the parameter takes its argument's bits
// with no decode at entry; the entry witness checks it in check mode.
assert!(
!body.contains(crate::nanbox::INT32_TAG_I64),
"a proven parameter takes its argument's bits at entry:\n{body}"
);
}

#[test]
Expand Down
28 changes: 6 additions & 22 deletions crates/perry-codegen/src/codegen/facts_rep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,16 @@
//!
//! `PERRY_WP_REP_P1=0` restores the collectors' decision everywhere.
//!
//! Phase P3 (§10.5): a proven Number is a canonical double. Proven code never
//! creates an INT32 box, and an INT32 box from native code reaches only a ⊤
//! node, never a proven one, so a proven Number's reader does not test the
//! INT32 tag. `PERRY_WP_REP_P3=0` restores the tag test.
//! Phase P3 (§10.5): a Number is a canonical double, its one encoding. No
//! reader tests for another.
//!
//! Phase P2 (§10.4), first slice: a parameter is a local like any other, so
//! one the facts give `I32` takes the canonical-i32 slot at entry
//! (`bind_params`). Its arguments are already a plain double there
//! (`facts_number_locals::emit_entry_number`), and the I32 interval is
//! (`bind_params`). Its arguments are already a plain double there, and the I32 interval is
//! exact, so the slot's value is one `fptosi`. The calling convention does
//! not change yet. A method call whose every callee returns only Numbers
//! (wpfacts `ModuleBindings::number_calls`, the call's return
//! representation) is a Number: a canonical double (P3), so its readers do
//! not test the INT32 tag (`number_call`). `PERRY_WP_REP_P2=0` keeps
//! representation) is a Number (`number_call`). `PERRY_WP_REP_P2=0` keeps
//! parameters in their double slots and call results unproven.

use std::collections::HashSet;
Expand All @@ -51,17 +47,6 @@ pub(crate) fn local(ctx: &crate::expr::FnCtx<'_>, id: u32) -> Option<FactsRep> {
ctx.wpfacts.local_reps.get(&id).copied()
}

/// Phase P3: proven Numbers are canonical doubles (module docs).
pub(crate) fn canonical_numbers() -> bool {
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ON.get_or_init(|| {
!matches!(
std::env::var("PERRY_WP_REP_P3").as_deref(),
Ok("0") | Ok("off") | Ok("false")
)
})
}

/// Phase P2 (module docs).
pub(crate) fn typed_params() -> bool {
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
Expand Down Expand Up @@ -94,7 +79,6 @@ fn claimed_call(ctx: &crate::expr::FnCtx<'_>, callee: &perry_hir::Expr) -> Optio
return None;
};
let usable = typed_params()
&& canonical_numbers()
&& !ctx.wpfacts_off
&& !ctx.wpfacts.interp_guard
&& *byte_offset != 0
Expand All @@ -103,8 +87,8 @@ fn claimed_call(ctx: &crate::expr::FnCtx<'_>, callee: &perry_hir::Expr) -> Optio
}

/// Check mode: `value`, the result of the call through `callee` that
/// `number_call` proves a Number, is a canonical double, neither a tagged
/// value nor an INT32 box. A miss reports the false proof and exits 86.
/// `number_call` proves a Number, is a plain double, not a tagged value. A
/// miss reports the false proof and exits 86.
pub(crate) fn witness_number_call(
ctx: &mut crate::expr::FnCtx<'_>,
callee: &perry_hir::Expr,
Expand Down
22 changes: 5 additions & 17 deletions crates/perry-codegen/src/codegen/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,7 @@ use super::typed_abi::{
typed_i1_function_name, typed_i32_function_name, typed_param_reps_for_params,
typed_string_function_name, TypedFunctionTrampolineKind, TypedParamRep,
};
use super::typed_entry::{
emit_tiered_entry_dispatch, emit_typed_arg_to_raw_after_entry_tier, typed_entry_arg_guard,
EntryArgGuard,
};
use super::typed_entry::{emit_tiered_entry_dispatch, typed_entry_arg_guard, EntryArgGuard};

/// Internal body name for a self-recursive allocator whose arena-state pointer
/// is threaded through recursive calls (#8591).
Expand Down Expand Up @@ -236,7 +233,7 @@ fn emit_typed_public_trampoline_fast_value(
let raw_args: Vec<String> = arg_names
.iter()
.zip(arg_reps.iter())
.map(|(arg, rep)| emit_typed_arg_to_raw_after_entry_tier(blk, *rep, arg))
.map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg))
.collect();
let typed_args: Vec<(LlvmType, &str)> = raw_args
.iter()
Expand All @@ -249,7 +246,7 @@ fn emit_typed_public_trampoline_fast_value(
let raw_args: Vec<String> = arg_names
.iter()
.zip(arg_reps.iter())
.map(|(arg, rep)| emit_typed_arg_to_raw_after_entry_tier(blk, *rep, arg))
.map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg))
.collect();
let typed_args: Vec<(LlvmType, &str)> = raw_args
.iter()
Expand All @@ -263,7 +260,7 @@ fn emit_typed_public_trampoline_fast_value(
let raw_args: Vec<String> = arg_names
.iter()
.zip(arg_reps.iter())
.map(|(arg, rep)| emit_typed_arg_to_raw_after_entry_tier(blk, *rep, arg))
.map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg))
.collect();
let typed_args: Vec<(LlvmType, &str)> = raw_args
.iter()
Expand All @@ -278,7 +275,7 @@ fn emit_typed_public_trampoline_fast_value(
let raw_args: Vec<String> = arg_names
.iter()
.zip(arg_reps.iter())
.map(|(arg, rep)| emit_typed_arg_to_raw_after_entry_tier(blk, *rep, arg))
.map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg))
.collect();
let typed_args: Vec<(LlvmType, &str)> = raw_args
.iter()
Expand Down Expand Up @@ -962,15 +959,6 @@ pub(super) fn compile_function(
}
_ => {}
}
// A split region's Number parameter (its only caller proved a
// Number) and a facts-proven one may arrive as an int32 box; the
// body's Number proof means a plain double, so convert once here.
let arg_name =
if region_number_params.contains(&p.id) || facts_number_params.contains(&p.id) {
super::facts_number_locals::emit_entry_number(blk, &arg_name)
} else {
arg_name
};
let slot = super::arguments::store_param_slot(blk, p, &boxed_vars, &arg_name);
param_roots.push((p.id, slot.clone()));
map.insert(p.id, slot);
Expand Down
15 changes: 4 additions & 11 deletions crates/perry-codegen/src/codegen/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,12 +492,7 @@ pub(super) fn compile_method(
format!("%arg{}", p.id)
};
let number = facts_number_params.contains(&p.id);
let value = if number {
super::facts_number_locals::emit_entry_number(blk, &arg_name)
} else {
arg_name.clone()
};
let slot = super::arguments::store_param_slot(blk, p, &method_boxed_vars, &value);
let slot = super::arguments::store_param_slot(blk, p, &method_boxed_vars, &arg_name);
if number {
shadow_slot_map.remove(&p.id);
} else if let Some(slot_idx) = shadow_slot_map.get(&p.id).copied() {
Expand All @@ -509,11 +504,9 @@ pub(super) fn compile_method(
map.insert(p.id, slot);
if index_param_ids.contains(&p.id) {
// Every route proved the signed-i32 value class before this
// body. A raw parameter is that value; otherwise a statically
// proven route normally passes a plain double and the guarded
// stable public entry may also pass Perry's canonical INT32
// NaN-box, so use the shared already-guarded conversion rather
// than `fptosi` (which cannot consume a tagged INT32 value).
// body. A raw parameter is that value; otherwise every route
// passes an exact int32 double, which the shared
// already-guarded conversion reads with one `fptosi`.
let raw_i32 = if raw_index {
super::index_entry::raw_param_name(p.id)
} else {
Expand Down
Loading
Loading