From cf30379251607819ae5190d504d69044c5900f9b Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Sat, 22 Aug 2026 23:32:51 -0400 Subject: [PATCH] fix(compile): --report-size false positive from std-internal crate names The "duplicate crate instance" finding could misreport a coincidental name collision as a fixable build duplication. Root-caused by demangling the real symbols behind a suspicious second gimli instance found on a real compiled binary: every one was gimli::read::cfi::{EhFrame, CommonInformationEntry, Augmentation, PartialFrameDescriptionEntry, UnwindSection, ...} -- DWARF exception- handling-frame parsing, the narrow surface std's own unwinder uses. That copy is baked into the prebuilt std shipped with the toolchain and was never a resolvable Cargo dependency of this build at all -- confirmed via both `cargo tree -i gimli --duplicates` (empty) and `cargo build --unit-graph -Z unstable-options` (exactly one gimli unit), neither of which can see something baked into std's own prebuilt rlib. A real application dependency on gimli reads debug info instead (read::abbrev, read::line, read::rnglists, ...), so classify a hash as std's internal copy when its symbols carry at least one CFI-specific marker and zero debug-info-specific ones (requiring "at least one" CFI marker, not "all symbols", matters: the CFI build's own shared reader/ utility code -- EndianSlice, Reader::read_uleb128, error types -- carries no CFI-specific name of its own but compiles alongside it in the same unit). Excluded hashes are not silently dropped: they're listed under a new "Excluded: std-internal copies" section instead. Verified end to end: recompiling the same program that previously reported "gimli linked 2 times, 42.7 KiB" now reports zero duplicate crate instances and one excluded std-internal copy (11.9 KiB, 11 symbols). --- CLAUDE.md | 2 +- Cargo.lock | 154 ++++++------ Cargo.toml | 2 +- .../8673-report-size-std-internal-filter.md | 3 + .../perry/src/commands/compile/size_report.rs | 237 ++++++++++++++++++ 5 files changed, 319 insertions(+), 79 deletions(-) create mode 100644 changelog.d/8673-report-size-std-internal-filter.md diff --git a/CLAUDE.md b/CLAUDE.md index 61bf8dcebb..94d8b53bf4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1519 +**Current Version:** 0.5.1520 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index ebcce9bbe9..2689c5821c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5598,7 +5598,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "base64 0.22.1", @@ -5660,7 +5660,7 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-dispatch", "serde", @@ -5668,7 +5668,7 @@ dependencies = [ [[package]] name = "perry-audio-miniaudio" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "cc", "libc", @@ -5677,7 +5677,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "inkwell", @@ -5694,7 +5694,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "perry-hir", @@ -5702,7 +5702,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "perry-hir", @@ -5710,7 +5710,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "perry-dispatch", @@ -5719,7 +5719,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "perry-hir", @@ -5727,7 +5727,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "base64 0.22.1", @@ -5739,7 +5739,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "perry-hir", @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "async-trait", @@ -5776,14 +5776,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "serde", "serde_json", @@ -5791,7 +5791,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1519" +version = "0.5.1520" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5802,7 +5802,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "clap", @@ -5817,7 +5817,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "block2", "objc2", @@ -5827,7 +5827,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "argon2", "perry-ffi", @@ -5836,7 +5836,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "reqwest", @@ -5845,7 +5845,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "bcrypt", "perry-ffi", @@ -5853,7 +5853,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "rusqlite", @@ -5861,7 +5861,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "scraper", @@ -5869,7 +5869,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "perry-runtime", @@ -5877,7 +5877,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "chrono", "cron", @@ -5887,7 +5887,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "chrono", "perry-ffi", @@ -5895,7 +5895,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "rust_decimal", @@ -5903,7 +5903,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "serde_json", @@ -5911,7 +5911,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5919,7 +5919,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "perry-runtime", @@ -5927,14 +5927,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "bytes", "http-body-util", @@ -5952,7 +5952,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "bytes", "lazy_static", @@ -5965,7 +5965,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "base64 0.22.1", "bytes", @@ -5990,7 +5990,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "lazy_static", "perry-ffi", @@ -6000,7 +6000,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "base64 0.22.1", "jsonwebtoken", @@ -6011,7 +6011,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "lru", "perry-ffi", @@ -6020,7 +6020,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "chrono", "perry-ffi", @@ -6028,7 +6028,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "bson", "futures-util", @@ -6040,7 +6040,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "chrono", "perry-ffi", @@ -6050,7 +6050,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "nanoid", "perry-ffi", @@ -6059,7 +6059,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "bytes", "perry-ffi", @@ -6072,7 +6072,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "const-oid 0.10.2", "der 0.8.0", @@ -6091,7 +6091,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "lettre", "perry-ffi", @@ -6101,7 +6101,7 @@ dependencies = [ [[package]] name = "perry-ext-parcel-watcher" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "fancy-regex", "notify", @@ -6113,7 +6113,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "printpdf", @@ -6121,7 +6121,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "sqlx", @@ -6130,7 +6130,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "governor", "perry-ffi", @@ -6138,7 +6138,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "fast_image_resize", "image", @@ -6148,7 +6148,7 @@ dependencies = [ [[package]] name = "perry-ext-streams" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "lazy_static", "perry-ffi", @@ -6157,7 +6157,7 @@ dependencies = [ [[package]] name = "perry-ext-typescript" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "serde", @@ -6173,7 +6173,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "perry-runtime", @@ -6182,7 +6182,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "uuid", @@ -6190,7 +6190,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "regex", @@ -6200,7 +6200,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "futures-util", "lazy_static", @@ -6213,7 +6213,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "brotli", "flate2", @@ -6223,7 +6223,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "dashmap", "once_cell", @@ -6232,7 +6232,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "perry-api-manifest", @@ -6250,7 +6250,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "perry-diagnostics", @@ -6262,7 +6262,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "base64 0.22.1", @@ -6304,14 +6304,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6406,14 +6406,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "perry-hir", @@ -6422,14 +6422,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "base64 0.22.1", "itoa", @@ -6447,7 +6447,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "rand 0.10.1", "serde", @@ -6457,7 +6457,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "base64 0.22.1", "cairo-rs 0.22.0", @@ -6480,7 +6480,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "base64 0.22.1", "block2", @@ -6497,7 +6497,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "base64 0.22.1", "block2", @@ -6513,7 +6513,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1519" +version = "0.5.1520" [[package]] name = "perry-ui-test" @@ -6524,11 +6524,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1519" +version = "0.5.1520" [[package]] name = "perry-ui-tvos" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "base64 0.22.1", "block2", @@ -6545,7 +6545,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "base64 0.22.1", "block2", @@ -6562,7 +6562,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "block2", "libc", @@ -6576,7 +6576,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "base64 0.22.1", "libc", @@ -6595,14 +6595,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "base64 0.22.1", @@ -6618,7 +6618,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 2623e8a948..f6b934f2fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -316,7 +316,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1519" +version = "0.5.1520" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/8673-report-size-std-internal-filter.md b/changelog.d/8673-report-size-std-internal-filter.md new file mode 100644 index 0000000000..b6aeff1b63 --- /dev/null +++ b/changelog.d/8673-report-size-std-internal-filter.md @@ -0,0 +1,3 @@ +### Fixed + +- `perry compile --report-size`: the "duplicate crate instance" finding no longer reports a false positive when a crate name collides with one Rust's own standard library vendors internally for `std::backtrace`/panic-unwinding support (`gimli`, `addr2line`, `miniz_oxide`, `object`, `rustc_demangle`). Root-caused by demangling the real symbols behind a suspicious second `gimli` instance: every one was `gimli::read::cfi::{EhFrame, CommonInformationEntry, Augmentation, ...}` — DWARF exception-handling-frame parsing, the narrow surface `std`'s own unwinder uses, baked into the prebuilt `std` shipped with the toolchain and never a resolvable Cargo dependency of the build at all (invisible to `cargo tree`/`cargo build --unit-graph`, both of which correctly show only one real `gimli` unit). A real application dependency on `gimli` reads debug info instead (`read::abbrev`, `read::line`, ...), so the report now classifies a hash as `std`'s internal copy when its symbols carry at least one CFI-specific marker and zero debug-info-specific ones, excludes it from the duplicate finding, and lists it separately under a new "Excluded: std-internal copies" section instead of silently dropping it. diff --git a/crates/perry/src/commands/compile/size_report.rs b/crates/perry/src/commands/compile/size_report.rs index 9a7874878a..00a2cda757 100644 --- a/crates/perry/src/commands/compile/size_report.rs +++ b/crates/perry/src/commands/compile/size_report.rs @@ -20,6 +20,11 @@ //! made it into the final link — code/data attribution, duplicate function //! bodies, duplicate crate instances, generic-monomorphization cost, and a //! few named cost patterns (panics, `Debug`/`Display` formatting, vtables). +//! The duplicate-crate-instance check also screens out a real false-positive +//! class: a coincidental name collision with a crate `std` itself vendors +//! internally for backtrace support (see `STD_INTERNAL_BACKTRACE_CRATES`) +//! is not a build duplication Perry produced, so it is excluded from the +//! finding and reported separately instead. use std::collections::BTreeMap; use std::fs; @@ -83,6 +88,18 @@ struct DuplicateCrateInstance { total_bytes: u64, } +/// A crate instance excluded from `duplicate_crate_instances` because its +/// symbols look like Rust's own standard library's internal, toolchain- +/// baked copy of that crate name (see `STD_INTERNAL_BACKTRACE_CRATES`) — +/// not a real second build Perry's own compilation produced. +#[derive(Serialize)] +struct ExcludedStdInternalCopy { + crate_name: String, + hash: String, + bytes: u64, + symbol_count: usize, +} + #[derive(Serialize)] struct PatternTotal { name: &'static str, @@ -111,6 +128,7 @@ struct SizeReport { generic_families: Vec, duplicate_bodies: Vec, duplicate_crate_instances: Vec, + std_internal_excluded: Vec, patterns: Vec, suggestions: Vec, } @@ -277,6 +295,10 @@ fn build_report(exe_path: &Path) -> anyhow::Result { let mut family_totals: BTreeMap<(String, String), (usize, u64)> = BTreeMap::new(); let mut crate_hashes: BTreeMap> = BTreeMap::new(); let mut crate_hash_bytes: BTreeMap<(String, String), u64> = BTreeMap::new(); + // Only populated for `STD_INTERNAL_BACKTRACE_CRATES` — the full symbol + // list per (crate, hash) is only needed to classify those few crate + // names, so this stays cheap regardless of binary size. + let mut crate_hash_symbols: BTreeMap<(String, String), Vec> = BTreeMap::new(); let mut body_bytes: BTreeMap<&[u8], Vec<(String, u64)>> = BTreeMap::new(); // exact bytes -> [(symbol, size)] let mut pattern_totals: BTreeMap<&'static str, (u64, usize)> = BTreeMap::new(); @@ -303,6 +325,12 @@ fn build_report(exe_path: &Path) -> anyhow::Result { *crate_hash_bytes .entry((crate_name.clone(), hash.clone())) .or_insert(0) += sym.size; + if STD_INTERNAL_BACKTRACE_CRATES.contains(&crate_name.as_str()) { + crate_hash_symbols + .entry((crate_name.clone(), hash.clone())) + .or_default() + .push(demangled.clone()); + } } if crate_name != "native/other" { @@ -380,6 +408,45 @@ fn build_report(exe_path: &Path) -> anyhow::Result { duplicate_bodies.sort_by_key(|a| std::cmp::Reverse(a.wasted_bytes)); duplicate_bodies.truncate(REPORT_TOP_DUPLICATES); + // Exclude hash-variants that look like `std`'s own internal, toolchain- + // baked copy of a crate it vendors for backtrace support, before + // deciding whether a crate name has more than one REAL instance — + // otherwise a coincidental name collision with a std-internal component + // (confirmed for `gimli`: std's own DWARF unwinder) gets reported as a + // fixable build duplication it is not. + let mut std_internal_excluded: Vec = Vec::new(); + for (crate_name, hashes) in crate_hashes.iter_mut() { + if !STD_INTERNAL_BACKTRACE_CRATES.contains(&crate_name.as_str()) { + continue; + } + let suspect: Vec = hashes + .iter() + .filter(|h| { + crate_hash_symbols + .get(&(crate_name.clone(), (*h).clone())) + .is_some_and(|syms| looks_like_std_internal_backtrace_copy(crate_name, syms)) + }) + .cloned() + .collect(); + for hash in suspect { + hashes.remove(&hash); + std_internal_excluded.push(ExcludedStdInternalCopy { + crate_name: crate_name.clone(), + bytes: crate_hash_bytes + .get(&(crate_name.clone(), hash.clone())) + .copied() + .unwrap_or(0), + symbol_count: crate_hash_symbols + .get(&(crate_name.clone(), hash.clone())) + .map(Vec::len) + .unwrap_or(0), + hash, + }); + } + } + crate_hashes.retain(|_, hashes| !hashes.is_empty()); + std_internal_excluded.sort_by_key(|e| std::cmp::Reverse(e.bytes)); + let mut duplicate_crate_instances: Vec = crate_hashes .into_iter() .filter(|(_, hashes)| hashes.len() > 1) @@ -439,6 +506,7 @@ fn build_report(exe_path: &Path) -> anyhow::Result { generic_families, duplicate_bodies, duplicate_crate_instances, + std_internal_excluded, patterns, suggestions, }) @@ -569,6 +637,92 @@ const PATTERNS: &[PatternMatcher] = &[ }), ]; +/// Crate names Rust's own standard library vendors internally for +/// `std::backtrace`/panic-unwinding support (`library/std/Cargo.toml` in +/// rust-lang/rust). A build can end up with a second, unrelated instance of +/// one of these names: the real Cargo-resolved dependency (if the program +/// also uses it directly, e.g. for its own symbolication), plus a +/// completely separate copy baked into the prebuilt `std` rlib shipped with +/// the toolchain. That second copy is invisible to `cargo tree`/the unit +/// graph (it was never a resolvable dependency of THIS build at all — Rust +/// itself's release process built and shipped it), so there is nothing +/// Perry's build can deduplicate. +const STD_INTERNAL_BACKTRACE_CRATES: &[&str] = &[ + "gimli", + "addr2line", + "miniz_oxide", + "object", + "rustc_demangle", +]; + +/// Symbol-path markers specific to DWARF CFI/EH-frame unwinding — the +/// narrow slice of `gimli`'s API surface `std`'s own unwinder uses +/// (confirmed by demangling real symbols under a suspect hash: every +/// gimli-rooted one was `gimli::read::cfi::{EhFrame, +/// CommonInformationEntry, Augmentation, PartialFrameDescriptionEntry, +/// UnwindSection, UnwindTable, ...}` or the `common`/`eh_walker` types that +/// exist only to support that path). +const CFI_MARKERS: &[&str] = &[ + "::cfi::", + "EhFrame", + "CommonInformationEntry", + "PartialFrameDescriptionEntry", + "UnwindSection", + "UnwindTable", + "UnwindContext", + "FrameDescriptionEntry", + "Augmentation", + "CfaRule", + "RegisterRule", + "EhFrameOffset", + "eh_walker", +]; + +/// Symbol-path markers specific to ordinary DWARF debug-info reading — the +/// surface a real application dependency on `gimli` actually uses +/// (confirmed against the OTHER, real hash in the same binary: every +/// symbol was `gimli::read::abbrev::Abbreviation`). `std`'s CFI-only +/// internal copy never touches this surface, so any of these appearing +/// under a hash rules out "this is std's internal copy" even though that +/// hash's OWN shared reader/utility code (`EndianSlice`, `Reader:: +/// read_uleb128`, …) has no CFI marker of its own. +const DEBUG_INFO_MARKERS: &[&str] = &[ + "::abbrev::", + "Abbreviation", + "::line::", + "LineProgram", + "::rnglists::", + "::loclists::", + "::unit::", + "UnitHeader", + "::dwarf::", + "DebugAbbrev", + "DebugInfo", + "DebugLine", + "DebugStr", + "DebugRanges", +]; + +/// Whether a (crate, hash) group's symbols look like `std`'s own internal +/// backtrace-support copy rather than a real second build of the +/// application's dependency: at least one CFI-specific marker present, and +/// zero debug-info-specific markers. Requiring "at least one" rather than +/// "all" matters because the CFI path's own shared reader/utility code +/// (`EndianSlice`, `Reader::read_uleb128`, error types, …) carries no CFI- +/// specific name of its own but is compiled alongside it in the same unit. +fn looks_like_std_internal_backtrace_copy(crate_name: &str, symbols: &[String]) -> bool { + if !STD_INTERNAL_BACKTRACE_CRATES.contains(&crate_name) || symbols.is_empty() { + return false; + } + let has_cfi_marker = symbols + .iter() + .any(|s| CFI_MARKERS.iter().any(|marker| s.contains(marker))); + let has_debug_info_marker = symbols + .iter() + .any(|s| DEBUG_INFO_MARKERS.iter().any(|marker| s.contains(marker))); + has_cfi_marker && !has_debug_info_marker +} + /// Demangle a Rust symbol name. `rustc_demangle` returns non-Rust input /// unchanged — the normal case for libc/system symbols — and `crate_of` /// below buckets those as `native/other`. @@ -736,6 +890,28 @@ fn render_markdown(report: &SizeReport) -> String { } } + if !report.std_internal_excluded.is_empty() { + out.push_str("\n## Excluded: std-internal copies\n\n"); + out.push_str( + "Crate instances that looked like a duplicate above but were excluded: their \ + symbols matched the narrow API surface Rust's own standard library uses \ + internally for `std::backtrace`/panic-unwinding support (e.g. `gimli`'s DWARF \ + CFI/EH-frame reader). That copy is baked into the prebuilt `std` shipped with the \ + toolchain — it was never a resolvable dependency of this build, so there is \ + nothing here for Perry (or Cargo) to deduplicate; the name collision alone would \ + otherwise misreport it as a fixable duplicate.\n\n", + ); + out.push_str("| Bytes | Symbols | Crate |\n|---|---|---|\n"); + for excluded in &report.std_internal_excluded { + out.push_str(&format!( + "| {} | {} | `{}` |\n", + human_bytes(excluded.bytes), + excluded.symbol_count, + excluded.crate_name, + )); + } + } + if !report.generic_families.is_empty() { out.push_str("\n## Generic monomorphization\n\n"); out.push_str("| Total | Instantiations | Crate | Family |\n|---|---|---|---|\n"); @@ -809,6 +985,67 @@ fn human_bytes(bytes: u64) -> String { mod tests { use super::*; + #[test] + fn std_internal_backtrace_copy_detected_from_real_cfi_symbols() { + // Real demangled symbols pulled from a compiled binary's second + // gimli instance — all gimli::read::cfi::*. + let symbols = vec![ + "> as gimli::read::cfi::UnwindSection>>::cie_from_offset".to_string(), + ", usize>>::parse".to_string(), + "::parse".to_string(), + ]; + assert!(looks_like_std_internal_backtrace_copy("gimli", &symbols)); + } + + #[test] + fn std_internal_backtrace_copy_not_detected_for_real_dependency_usage() { + // A real application dependency on gimli reads debug info + // (abbrev/line/rnglists), not CFI/EH-frame data. + let symbols = vec![ + "gimli::read::abbrev::Abbreviation::new_internal".to_string(), + "gimli::read::line::LineProgram::header".to_string(), + ]; + assert!(!looks_like_std_internal_backtrace_copy("gimli", &symbols)); + } + + #[test] + fn std_internal_backtrace_copy_not_detected_outside_the_known_crate_list() { + // A crate outside STD_INTERNAL_BACKTRACE_CRATES never gets excluded, + // even if its symbols happen to mention "EhFrame" in passing. + let symbols = vec!["some_crate::EhFrame::wrapper".to_string()]; + assert!(!looks_like_std_internal_backtrace_copy( + "some_crate", + &symbols + )); + } + + #[test] + fn std_internal_backtrace_copy_tolerates_shared_reader_utility_symbols() { + // Real finding: the CFI build's own shared reader/utility code + // (EndianSlice, Reader::read_uleb128, the CapacityFull error type) + // carries no CFI-specific name of its own, but is compiled + // alongside gimli::read::cfi in the SAME hash group — "at least one + // CFI marker" (not "all symbols") is what correctly includes it. + let symbols = vec![ + "gimli::read::cfi::EhFrame::parse".to_string(), + " as gimli::read::reader::Reader>::read_uleb128".to_string(), + "::fmt".to_string(), + ]; + assert!(looks_like_std_internal_backtrace_copy("gimli", &symbols)); + } + + #[test] + fn std_internal_backtrace_copy_a_single_debug_info_marker_vetoes_exclusion() { + // A debug-info marker anywhere in the group is enough to keep it as + // a (possibly real) finding rather than excluding it, even + // alongside a CFI-looking symbol. + let symbols = vec![ + "gimli::read::cfi::EhFrame::parse".to_string(), + "gimli::read::abbrev::Abbreviation::new_internal".to_string(), + ]; + assert!(!looks_like_std_internal_backtrace_copy("gimli", &symbols)); + } + #[test] fn crate_of_extracts_the_first_path_segment() { assert_eq!(