# `min-specialization` โ Audit & Resolution
Audit of the `#[specialization]` proc-macro (`src/lib.rs`, `src/normalize.rs`,
`src/substitute.rs`), originally at commit `918cfcc`, plus the fixes applied on top.
Verified on `rustc 1.90.0` (stable) and `nightly` Miri (x86_64-unknown-linux-gnu).
Every finding was reproduced empirically; reproducers and regression tests live in
`tests/` (see [Tests](#tests)).
## How the crate works
For a default (`default fn`) impl and one or more concrete specializing impls, the
macro rewrites each default method into a runtime dispatcher that selects a branch
by comparing type identity and then reinterprets `self`/args/return for that branch.
## Direct answer to "does it handle all Rust syntax, or error cleanly when it can't?"
Originally **no on both counts** โ unsupported constructs produced a killed compiler
(`SIGABRT`), macro panics, or silently-wrong code. **After the fixes**, every
unsupported construct now produces a clean, spanned diagnostic, and several
previously-unsupported constructs are now handled correctly.
| nested associated-type bound (`Item: Iterator<Item = u8>`) | ๐ฅ `SIGABRT`, compiler killed | โ
clean error "nested associated type bindings are not supported" |
| assoc-type bound in param position (`impl<X: Iterator<Item=u8>>`) | ๐ฅ macro panic | โ
**supported** (normalized like a `where` clause) |
| `mut`/tuple/wildcard argument patterns | ๐ฅ macro panic / `E0642` | โ
**supported** (dispatch correct) |
| generic methods (`fn f<G>()`, `fn f<const N: usize>()`) | โ ๏ธ opaque `E0282` | โ
**supported** (generics forwarded as a turbofish) |
| overriding a method absent from the default impl | โ ๏ธ silently dropped | โ
clean error "โฆ not defined in the default impl" |
| overlapping specializations (`impl Tr for i32` twice) | โ ๏ธ silently accepted, nondeterministic winner | โ
clean error "conflicting specializations" |
| default impl with no matching special | โ ๏ธ leaked `default` โ `E0658` | โ
emitted as an ordinary impl |
| assoc `type`/`const` in a special impl | โ
clean error | โ
clean error (unchanged) |
## Findings & resolutions
Severity: ๐ด critical ยท ๐ high ยท ๐ก medium ยท โช low โ Status: โ
fixed ยท ๐ข improved ยท ๐ documented limitation
### Soundness
- ๐ด **S1/S2 โ function-pointer-address type identity + size/align-only `transmute`** โ
โ
**FIXED.** Dispatch now compares a **lifetime-erased `TypeId`**
(`src/lib.rs`, `__min_specialization_type_id`): the `TypeId` of the type with its
lifetimes erased, obtained through a trait object whose existential lifetime is
widened to `'static` (the value is a ZST `PhantomData`, so no non-`'static` data
is accessed). `TypeId` is collision-free, so there are **no** identical-code-folding
false positives (S1) and **no** false negatives (S3); after a match the two types
are identical up to lifetimes, so the transmute is an identity conversion (S2).
*Crucially this needs no `'static` bound, so borrowed types like `&str` are still
specializable* โ verified in debug, `--release`, and Miri, including on genuinely
borrowed (non-`'static`) values.
- ๐ **S3 โ false negatives (specializations silently skipped under Miri / split
codegen)** โ โ
**FIXED** by the same change (`TypeId` equality is reflexive).
- ๐ก **S4 โ release-only `assert_eq!` size/align guard** โ ๐ข the assertions are now
pure (always-true) defense-in-depth, since the matched types are provably identical.
Specializing on a *concrete* lifetime (e.g. `impl Tr for Cell<&'static str>`) is the
one case the lifetime-erased identity cannot police; ๐ like real `min_specialization`,
do not specialize on a specific lifetime.
### Robustness โ panics/aborts โ clean diagnostics
- ๐ด **P1 โ `std::process::abort()` (SIGABRT) on nested assoc-type bounds** โ
โ
**FIXED.** The unsafe `catch_unwind`/`process::abort()` machinery in
`src/normalize.rs` is gone (replaced with a safe `mem::take`); nested bindings now
`abort!` with a spanned message.
- ๐ **P2 โ panic on assoc-type bound in parameter position** โ โ
**FIXED + supported**
(`normalize_generic_param` now delegates to the `where`-clause normalizer).
- ๐ **P3/P4 โ macro panic / `E0642` on `mut`/tuple argument patterns** โ โ
**FIXED**:
outer dispatcher arguments are freshly named and forwarded; inner-trait *declarations*
are made pattern-free while the implementing method keeps the original patterns/body.
- ๐ก **P5 โ generic methods โ opaque `E0282`** โ โ
**FIXED + supported**: a method's
own type/const generic parameters are forwarded to the inner dispatch as an explicit
turbofish, which pins them across the transmute (verified debug/release/Miri).
- ๐ก **P6 โ `parse2(...).unwrap()` catch-all panic** โ โ
now `abort!`s with the parse
error, a span, and a "please report" note.
- ๐ก **P7 โ `_ => panic!()` on unsupported `where`-predicate** โ โ
spanned `abort!`.
- โช **P8 โ `or_insert_with(|| unreachable!())`** โ โ
removed (dead after the rewrite).
### Algorithm & determinism
- ๐ด **A1/A6 โ non-reproducible builds / nondeterministic dispatch** (`HashSet<ItemImpl>`
+ `HashMap` + `min_by_key` tie-break) โ โ
**FIXED**: impls are kept in source order
(`Vec`s); ties go to the first matching default. Codegen is now byte-for-byte
deterministic (verified across repeated expansions).
- ๐ด **A2/A3 โ no overlap detection** โ ๐ข **IMPROVED**: literally overlapping
specializations (same trait + same self type) are now rejected with a clean error.
๐ a full specialization *lattice* (e.g. ordering `Vec<i32>` before `Vec<T>`) is still
not modelled (A5).
- ๐ก **A4 โ silent dedup of identical impls** โ โ
no longer deduped (now distinct
impls โ a normal `E0119` if truly identical).
### Feature gaps
- ๐ **F1 โ specialization must restate the default's bounds** โ ๐ข **IMPROVED**: no
longer leaks `default`/`E0658`; surfaces as a clean `E0119` coherence error. ๐
inferring/relaxing bound restatement remains a limitation.
- ๐ **F3 โ specialized method absent from the default impl silently dropped** โ
โ
clean error.
- โ
**Lifetime-bearing specializations** (`E0261` on the specialization's own
lifetimes) โ **FIXED + supported**: a trait lifetime parameter (`trait Tr<'a>`), a
specialized self type carrying a lifetime (`impl<'a> .. for Foo<'a>`), and a trait
type-parameter fixed to a borrowed type (`impl<'a> Tr<&'a str> for ..`) all work.
The dispatcher erases lifetimes to `'static` for the (lifetime-agnostic) type-id
check and uses inferred (`'_`) lifetimes at the call so the transmute stays an
identity; the inner impl declares self-type lifetimes and promotes method-only
lifetimes to method-level generics.
- โ
**Generic associated types (GATs)** in the specialized trait โ supported (no
extra code beyond the lifetime/generic-method handling above): a GAT with a
lifetime parameter (`type Ref<'a>`), a type parameter (`type Wrap<X>`), or both
with `where` bounds, declared by the default impl and with the *method* overridden
in specializations. Verified debug/release/Miri. (Overriding the GAT itself in a
specialization falls under the assoc-type-specialization limitation below.)
- ๐ const-generic specialization, fully-generic special impls (`impl<U> Tr for Vec<U>`),
a reference *blanket* default specialized by a concrete reference
(`impl<'a, T> Tr for &'a T` + `impl Tr for &'a str`), impl-level `default impl`, and
`default type`/`default const` remain unsupported (compile errors, not silent
miscompiles).
### Testing & CI
- ๐ด **T1 โ integration tests ran zero `#[test]`s** โ โ
`tests/runtime_dispatch.rs`
asserts real dispatch (debug/release/Miri).
- ๐ก **CI** โ ๐ข `.github/workflows/rust.yml` now also runs `--release`, `clippy`, and a
Miri job.
## Tests
| `tests/runtime_dispatch.rs` | correct dispatch incl. `&str`, `mut`/tuple/wildcard args; passes debug/release/Miri |
| `tests/audit_soundness.rs` | soundness regression: distinct same-layout types never confused; sound `&str` specialization |
| `tests/compile_fail.rs` + `tests/compile_fail/` (`trybuild`) | every unsupported construct emits a clean, spanned diagnostic |
```sh
cargo test
cargo test --release
cargo +nightly miri test --test audit_soundness --test runtime_dispatch
```