Skip to content
Draft
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
104 changes: 104 additions & 0 deletions cc-perf-campaign/codex/REPORT_yf6_admission.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# YF6 typed-i1 admission report

## SHA

Implementation commit: `ae8e15aa28491a63312a59050b14b70d4831a82c`.

The implementation commit was pushed to
`fork/perf/typed-numeric-predicate`; `git ls-remote` resolved that branch to
the same SHA before this report-only child commit was created.

## Failing condition

The failure is return-type inference, not the erased-parameter proof, capture
analysis, typed-body safety walk, comparison floor, typed lowering, or a clone
instruction budget.

`crates/perry-hir/src/lower_types.rs:315` caps recursive expression-type
inference at 48 levels, and `infer_type_from_expr` returns `Type::Any` at
`crates/perry-hir/src/lower_types.rs:343-345`. The old `LogicalAnd |
LogicalOr` rule recursively inferred both children. YF6's 135 left-associated
`||` joins exceed that depth, so an inner left subtree became `Any`; the
logical unification propagated `Any` to the root. `infer_body_return_type`
then rejected that `Any` at `crates/perry-hir/src/lower_types.rs:923-933`, and
`crates/perry-hir/src/lower_decl/fn_decl.rs:372-383` consequently left
`Function.return_type` at its initial `Any` fallback. The smaller probe's 22
`||` joins remain below 48 and therefore infer `Boolean`.

At codegen admission, that metadata produces
`TypedCloneRejectionReason::ReturnTypeNotI1` at
`crates/perry-codegen/src/codegen/typed_abi.rs:1277-1279`.
`typed_i1_function_rejection_reason` only tries erased-predicate admission when
the declared-path reason is `ParamNotI1`
(`crates/perry-codegen/src/codegen/typed_abi.rs:839-842`), so YF6 never reaches
the override. Calling `erased_numeric_predicate_param_reps` directly would
also fail its Boolean-return rule at
`crates/perry-codegen/src/codegen/typed_abi.rs:1210-1220`.

All later rules pass once the return type is Boolean:

- `typed_param_rep_for_type` deliberately does not type `Any`
(`crates/perry-codegen/src/codegen/typed_abi.rs:144-155`), after which the
erased rule sees referenced parameter `q` and assigns guarded `F64`
(`crates/perry-codegen/src/codegen/typed_abi.rs:1223-1239`). The real
top-level function has no captures.
- `typed_i1_body_rejection_reason` accepts the one-return straight-line body
(`crates/perry-codegen/src/codegen/typed_abi.rs:1705-1739`): every logical
node is `And`/`Or`, and every comparison has `q` plus an integer literal as
f64-safe operands.
- `numeric_comparisons_in_typed_i1_expr`
(`crates/perry-codegen/src/codegen/typed_abi.rs:1191-1207`) counts 212, which
clears `ERASED_NUMERIC_PREDICATE_MIN_COMPARISONS = 4` at line 1189.
- There is no per-function instruction budget in the typed top-level clone
selection or typed-i1 lowering path.

## Change

`LogicalAnd`/`LogicalOr` return inference now flattens logical joins onto an
explicit worklist (`crates/perry-hir/src/lower_types.rs:357-388`, selected at
line 500). It applies the same sound rule as before: every leaf must infer to
the same non-`Any` type. The general depth-48 guard remains intact. A separate
512-node work cap bounds repeated inference cost and prevents the unbounded
O(n²) behavior that the original guard protects against, while covering YF6's
423 logical/comparison AST nodes.

`crates/perry-codegen/tests/yf6_admission.rs` parses and lowers the exact
2,274-byte YF6 fixture verbatim, appends a typed-string `codePointAt` caller,
and asserts:

- the inferred YF6 return type is `Boolean`;
- `YF6$typed_i1` exists with 76 `fcmp oge`, 76 `fcmp ole`, and 60 `fcmp oeq`;
- the clone has no `@js_rel_` calls;
- the generic fallback remains present;
- the public wrapper guards and calls the clone while retaining the fallback;
- the possibly-`undefined` `codePointAt` result enters through that guarded
public wrapper.

Restoring recursive logical inference makes the inferred-return assertion and
clone assertions fail.

## Gates

`df -g /` reported 0 GB available, below the binding 12 GB threshold. No cargo
invocation was made, so neither required cargo gate was run:

- NOT RUN: `cargo test -p perry-codegen`
- NOT RUN: `cargo build --release -p perry`

Non-cargo checks completed: `rustfmt --check`, `git diff --check`, exact fixture
`cmp`, source operator counts (76 `>=`, 76 `<=`, 60 `===`, 76 `&&`, 135
`||`), and SHA-256 equality with the supplied source
(`ffa564fc9612a0a1011f60d343d43777d082437547af2679ecb1568f478d1051`).

## Perrymaster request

Rebuild the compiler from implementation SHA
`ae8e15aa28491a63312a59050b14b70d4831a82c` on the I7-view tree, compile the
cc bundle with I7-view's configuration, and verify:

```sh
nm <cc-binary> | grep -c 'perry_fn_cli_2_1_112_js__YF6\$typed_i1'
```

The identity must be `1`. Then collect the rows versus I7-view and the perf
draw, using marker `YF6` self from the 9.46% baseline.
4 changes: 4 additions & 0 deletions changelog.d/9921-typed-numeric-predicate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### Changed

- Boolean helpers with erased numeric parameters now use a guarded typed clone
for comparison-heavy predicates, avoiding repeated generic relational calls.
1 change: 1 addition & 0 deletions crates/perry-codegen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,4 @@ llvm-sys = { version = "221", features = ["no-llvm-linking"], optional = true }
# dist` still compile the library with `testing` off.
[dev-dependencies]
perry-codegen = { path = ".", features = ["testing"] }
perry-parser.workspace = true
19 changes: 13 additions & 6 deletions crates/perry-codegen/src/codegen/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ use super::spec_abi::{
};
use super::typed_abi::{
emit_typed_arg_guard, emit_typed_arg_to_raw, generic_function_body_name, lower_typed_f64_body,
lower_typed_i1_body, lower_typed_i32_body, lower_typed_string_body, typed_f64_function_name,
typed_i1_function_name, typed_i32_function_name, typed_param_reps_for_params,
typed_string_function_name, TypedFunctionTrampolineKind, TypedParamRep,
lower_typed_i1_body_with_seed_locals, lower_typed_i32_body, lower_typed_string_body,
typed_f64_function_name, typed_i1_function_name, typed_i1_function_param_reps,
typed_i32_function_name, typed_param_reps_for_params, typed_string_function_name,
TypedFunctionTrampolineKind, TypedParamRep,
};

/// Internal body name for a self-recursive allocator whose arena-state pointer
Expand Down Expand Up @@ -144,7 +145,7 @@ pub(super) fn compile_typed_i1_function(
.cloned()
.ok_or_else(|| anyhow!("function name not resolved for {}", f.name))?;
let llvm_name = typed_i1_function_name(&generic_name);
let param_reps = typed_param_reps_for_params(&f.params)
let param_reps = typed_i1_function_param_reps(f)
.ok_or_else(|| anyhow!("typed-i1 function '{}' has unsupported parameter", f.name))?;
let params: Vec<(LlvmType, String)> = f
.params
Expand All @@ -159,7 +160,13 @@ pub(super) fn compile_typed_i1_function(

let value = {
let blk = lf.block_mut(0).unwrap();
lower_typed_i1_body(blk, &f.params, &f.body)?
let seed_reps = f
.params
.iter()
.zip(param_reps.iter().copied())
.map(|(param, rep)| (param.id, rep))
.collect();
lower_typed_i1_body_with_seed_locals(blk, &f.params, &f.body, HashMap::new(), seed_reps)?
};
lf.block_mut(0).unwrap().ret(I1, &value);
Ok(())
Expand Down Expand Up @@ -288,7 +295,7 @@ fn emit_public_typed_function_trampoline(
.unwrap_or_else(|| vec![TypedParamRep::F64; f.params.len()]),
TypedFunctionTrampolineKind::I32 => typed_param_reps_for_params(&f.params)
.unwrap_or_else(|| vec![TypedParamRep::I32; f.params.len()]),
TypedFunctionTrampolineKind::I1 => typed_param_reps_for_params(&f.params)
TypedFunctionTrampolineKind::I1 => typed_i1_function_param_reps(f)
.unwrap_or_else(|| vec![TypedParamRep::I1; f.params.len()]),
TypedFunctionTrampolineKind::StringRef => typed_param_reps_for_params(&f.params)
.unwrap_or_else(|| vec![TypedParamRep::StringRef; f.params.len()]),
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1831,7 +1831,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
match typed_abi::typed_i1_function_rejection_reason(f) {
None => {
typed_i1_functions.insert(f.id);
if let Some(reps) = typed_abi::typed_param_reps_for_params(&f.params) {
if let Some(reps) = typed_abi::typed_i1_function_param_reps(f) {
typed_i1_function_param_reps.insert(f.id, reps);
}
}
Expand Down
106 changes: 104 additions & 2 deletions crates/perry-codegen/src/codegen/typed_abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,22 @@ pub(crate) fn typed_param_reps_for_params(
.collect()
}

/// Parameter representations for a Boolean-returning top-level function.
///
/// In addition to declared scalar types, admit erased (`Any`/`Unknown`)
/// parameters when the complete straight-line body proves that they are used
/// as numbers. The public JSValue entry guards every inferred F64 parameter
/// once and retains the unchanged generic body for guard failure. Four
/// comparisons is the deliberately small profitability floor: below it the
/// entry guard and duplicate body can cost as much as the generic relational
/// fast paths they replace.
pub(crate) fn typed_i1_function_param_reps(function: &Function) -> Option<Vec<TypedParamRep>> {
if typed_i1_function_rejection_reason_impl(function).is_none() {
return typed_param_reps_for_params(&function.params);
}
erased_numeric_predicate_param_reps(function)
}

pub(crate) fn typed_f64_closure_capture_reps(
expr: &Expr,
module_local_types: &HashMap<u32, Type>,
Expand Down Expand Up @@ -782,7 +798,7 @@ pub(crate) fn is_typed_i32_function_candidate(function: &Function) -> bool {

#[allow(dead_code)]
pub(crate) fn is_typed_i1_function_candidate(function: &Function) -> bool {
typed_i1_function_rejection_reason_impl(function).is_none()
typed_i1_function_rejection_reason(function).is_none()
}

#[allow(dead_code)]
Expand Down Expand Up @@ -820,7 +836,14 @@ pub(crate) fn typed_i32_function_rejection_reason(
pub(crate) fn typed_i1_function_rejection_reason(
function: &Function,
) -> Option<TypedCloneRejectionReason> {
typed_i1_function_rejection_reason_impl(function)
let declared_reason = typed_i1_function_rejection_reason_impl(function);
if matches!(declared_reason, Some(TypedCloneRejectionReason::ParamNotI1))
&& erased_numeric_predicate_param_reps(function).is_some()
{
None
} else {
declared_reason
}
}

pub(crate) fn typed_string_function_rejection_reason(
Expand Down Expand Up @@ -1163,6 +1186,85 @@ pub(crate) fn typed_string_closure_rejection_reason_with_types(
typed_string_body_rejection_reason(body, locals)
}

const ERASED_NUMERIC_PREDICATE_MIN_COMPARISONS: usize = 4;

fn numeric_comparisons_in_typed_i1_expr(
expr: &Expr,
locals: &HashMap<u32, TypedParamRep>,
) -> usize {
match expr {
Expr::Compare { left, right, .. }
if expr_is_typed_f64_safe(left, locals) && expr_is_typed_f64_safe(right, locals) =>
{
1
}
Expr::Logical { left, right, .. } => {
numeric_comparisons_in_typed_i1_expr(left, locals)
+ numeric_comparisons_in_typed_i1_expr(right, locals)
}
Expr::Unary { operand, .. } => numeric_comparisons_in_typed_i1_expr(operand, locals),
_ => 0,
}
}

fn erased_numeric_predicate_param_reps(function: &Function) -> Option<Vec<TypedParamRep>> {
if function.is_async
|| function.is_generator
|| function.was_plain_async
|| !function.captures.is_empty()
|| !matches!(function.return_type, Type::Boolean)
|| function.params.iter().any(|param| {
param.default.is_some() || param.is_rest || param.arguments_object.is_some()
})
{
return None;
}

let mut referenced = HashSet::new();
crate::collectors::collect_ref_ids_in_stmts(&function.body, &mut referenced);
let mut inferred_erased = false;
let reps: Vec<TypedParamRep> = function
.params
.iter()
.map(|param| {
if let Some(rep) = typed_param_rep_for_type(&param.ty) {
return Some(rep);
}
if matches!(param.ty, Type::Any | Type::Unknown) && referenced.contains(&param.id) {
inferred_erased = true;
return Some(TypedParamRep::F64);
}
None
})
.collect::<Option<_>>()?;
if !inferred_erased {
return None;
}

let locals: HashMap<u32, TypedParamRep> = function
.params
.iter()
.zip(reps.iter().copied())
.map(|(param, rep)| (param.id, rep))
.collect();
if typed_i1_body_rejection_reason(&function.body, locals.clone()).is_some() {
return None;
}
let comparisons = function
.body
.iter()
.filter_map(|stmt| match stmt {
Stmt::Let {
init: Some(expr), ..
}
| Stmt::Return(Some(expr)) => Some(expr),
_ => None,
})
.map(|expr| numeric_comparisons_in_typed_i1_expr(expr, &locals))
.sum::<usize>();
(comparisons >= ERASED_NUMERIC_PREDICATE_MIN_COMPARISONS).then_some(reps)
}

fn typed_i1_function_rejection_reason_impl(
function: &Function,
) -> Option<TypedCloneRejectionReason> {
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/tests/fixtures/yf6_real.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading