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
18 changes: 18 additions & 0 deletions changelog.d/7818-opt-report-no-call-sites.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
**`--opt-report` now says `spec_no_call_sites` where it used to say nothing** (#7111).

A function whose call sites were all removed by an earlier pass — the inliner, then `unroll_static_loops` constant-folding what it produced — has no entry in `spec_facts.call_sites`, which is built by walking `hir.init` and every body for direct `Call` expressions. The spec-ABI decision loop `continue`d on that **before** constructing any `TypedCloneRejectionReason`, so nothing reached `record_typed_clone_rejection` and nothing reached `opt_report::deny_named`.

The result was silence, and silence is the one answer this report cannot afford: it is indistinguishable from "not analysed" and from "analysed and denied". A reader now sees that the loop was reached and had nothing to decide.

Measured on a four-line fixture whose only call to `tiny()` is inlined and folded away:

| | entries | rules |
|---|--:|---|
| before | 1 | `not_index_used_or_bounded` |
| after | 2 | `not_index_used_or_bounded`, **`spec_no_call_sites`** |

The new `TypedCloneRejectionReason::SpecNoCallSites` is deliberately not framed as a denial — its doc says so — because there is nothing to specialise *for*; it is reported so the absence is legible rather than inferred.

Worth recording for the next person: `--opt-report` writes to **stderr**, deliberately, so it cannot contaminate a `--format json` stdout payload or piped program output. Three of my attempts to observe it came back empty because they were running under `2>/dev/null`. That is also the likeliest reason a report looks like it "did not render".

`cargo test -p perry-codegen --lib` 851 passed / 0 failed; fmt and file-size clean.
17 changes: 14 additions & 3 deletions crates/perry-codegen/src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2183,9 +2183,6 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
let spec_budget = spec_abi::spec_abi_max();
let mut spec_emitted = 0usize;
for f in &hir.functions {
let Some(sites) = spec_facts.call_sites.get(&f.id) else {
continue;
};
let reject =
|reason: typed_abi::TypedCloneRejectionReason,
records: &mut Vec<crate::native_value::NativeRepRecord>| {
Expand All @@ -2201,6 +2198,20 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
],
);
};
// #7111: a function whose call sites were all inlined away — and
// then constant-folded by `unroll_static_loops` — has no entry in
// `spec_facts.call_sites`, which is built by walking `hir.init` and
// every body for direct `Call` expressions. This used to `continue`
// BEFORE any rejection was constructed, so `--opt-report` said
// nothing at all about the function: indistinguishable from "not
// analysed" and from "analysed and denied". Say "moot" instead.
let Some(sites) = spec_facts.call_sites.get(&f.id) else {
reject(
typed_abi::TypedCloneRejectionReason::SpecNoCallSites,
&mut typed_clone_rejection_records,
);
continue;
};
Comment on lines +2208 to +2214

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

renderer="$(rg --files | rg '(^|/)typed_abi_opt_report\.rs$' | head -n1)"
test -n "$renderer"
rg -n -C 8 'opt_report_reason|SpecNoCallSites|specialized-ABI precondition' "$renderer"

Repository: PerryTS/perry

Length of output: 1713


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== enum and report mapping =='
rg -n -C 12 'enum TypedCloneRejectionReason|SpecNoCallSites|fn opt_report_reason' crates/perry-codegen/src

echo '== rejection recording and report consumers =='
rg -n -C 8 'record_typed_clone_rejection|opt_report_reason|typed_clone_rejection_records' crates/perry-codegen/src

echo '== regression fixtures and report assertions =='
rg -n -C 8 'SpecNoCallSites|SpecTupleUnproven|opt-report|specialized-ABI precondition|boxed entry ABI' .

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== complete report-reason mapping =='
sed -n '1,115p' crates/perry-codegen/src/codegen/typed_abi_opt_report.rs

echo '== opt-report test files =='
git ls-files | rg '(^|/)(tests?|fixtures?|snapshots?)(/|$)|opt_report|opt-report'

echo '== exact reason assertions =='
rg -n -C 6 'spec_no_call_sites|SpecNoCallSites|spec_tuple_unproven|SpecTupleUnproven' --glob '!target/**' --glob '!node_modules/**' .

Repository: PerryTS/perry

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== report tiers and rendering =='
rg -n -C 10 'enum Tier|Tier::|fn render|explanation|issue' crates/perry-codegen/src/opt_report crates/perry-codegen/src/codegen/typed_abi_opt_report.rs

echo '== no-call-sites change documentation =='
cat -n changelog.d/7818-opt-report-no-call-sites.md

echo '== focused opt-report references =='
rg -n -C 8 'opt.report|opt_report|--opt-report|spec_no_call_sites|SpecNoCallSites' \
  changelog.d crates/perry-codegen/src crates/perry-codegen/tests tests \
  --glob '!**/target/**'

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Tier definition =='
rg -n -A 35 -B 5 'pub\(crate\)? enum Tier|enum Tier' crates/perry-codegen/src/opt_report

echo '== no-call-sites changelog =='
cat -n changelog.d/7818-opt-report-no-call-sites.md

echo '== focused report tests =='
rg -n -C 10 'opt_report|OptReport|--opt-report|deny_named|Analysis::SpecAbi' \
  crates/perry-codegen/src/codegen/*test* \
  crates/perry-codegen/src/opt_report \
  crates/perry-codegen/tests \
  tests --glob '*.rs' --glob '*.py' --glob '*.sh'

Repository: PerryTS/perry

Length of output: 29939


Add an explicit SpecNoCallSites report explanation.

The wildcard text incorrectly describes a failed specialized-ABI precondition and a boxed entry. SpecNoCallSites means that all direct call sites were removed, so specialization was moot. Add a dedicated explanation and assert it in the regression fixture.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/src/codegen/mod.rs` around lines 2208 - 2214, Update the
rejection-report generation around the `spec_facts.call_sites.get(&f.id)`
handling to provide a dedicated explanation for
`TypedCloneRejectionReason::SpecNoCallSites`, stating that all direct call sites
were removed and specialization was moot. Do not reuse the wildcard explanation
for specialized-ABI preconditions or boxed entries, and add an assertion for
this explanation in the relevant regression fixture.

Source: MCP tools

if f.is_async || f.is_generator || f.was_plain_async {
reject(
typed_abi::TypedCloneRejectionReason::AsyncOrGenerator,
Expand Down
12 changes: 12 additions & 0 deletions crates/perry-codegen/src/codegen/typed_abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,17 @@ pub(crate) enum TypedCloneRejectionReason {
ReceiverFieldNotOwn,
ReceiverFieldNotF64,
ThisEscape,
/// #7111 — Spec-ABI: the function has NO direct call sites left to analyse,
/// because an earlier pass removed them all (the inliner, then
/// `unroll_static_loops` constant-folding what it produced).
///
/// This is not a denial in the usual sense: there is nothing to specialise
/// FOR. It is reported anyway because the alternative is silence, and
/// silence here is indistinguishable from "not analysed" and from "analysed
/// and denied" — which is the whole complaint the opt-report exists to
/// answer. A reader seeing `spec_no_call_sites` knows the decision loop was
/// reached and had nothing to decide.
SpecNoCallSites,
/// Spec-ABI (Phase 2): no call site proved a viable representation tuple.
SpecTupleUnproven,
/// Spec-ABI: the function already carries a typed_abi clone family —
Expand Down Expand Up @@ -292,6 +303,7 @@ impl TypedCloneRejectionReason {
Self::ReceiverFieldNotOwn => "receiver_field_not_own",
Self::ReceiverFieldNotF64 => "receiver_field_not_f64",
Self::ThisEscape => "this_escape",
Self::SpecNoCallSites => "spec_no_call_sites",
Self::SpecTupleUnproven => "spec_tuple_unproven",
Self::SpecTypedCloneOverlap => "spec_typed_clone_overlap",
Self::SpecBudgetExceeded => "spec_budget_exceeded",
Expand Down
Loading