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
10 changes: 5 additions & 5 deletions docs/hunt.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ with `-Zthreads=8`. `rustc/check.sh wide` runs the ordinary checks.
| 16 | with `-Zcache-proc-macros=yes -Zmetadata-crate-hash=no`, an incremental rebuild of a crate using derives gets a different crate hash (SVH) from a clean build after an edit upstream | unstable options (one "potentially unsound"); found by the fuzzer under walk configurations; not root-caused; [facts](hunt/cached-proc-macros-crate-hash.md); excluded from the models |
| 17 | with `-Zunleash-the-miri-inside-of-you`, the "skipping const checks" warning is not shown again on an incremental rebuild | testing-only option; found by the UI-test fuzzer ([`coverage.md`](coverage.md)); since at least 1.60; [facts](hunt/unleash-warning-lost.md); tests using the option skipped |
| 18 | after a fatal error (a missing lang item), an incremental rebuild reports fewer errors than a clean build: the fatal error is reached in a different query order | diagnostics only; found by the UI-test fuzzer; stock nightly; [facts](hunt/fatal-error-order.md); labelled known in `mirth-lab ui-fuzz` |
| 19 | on riscv64 and loongarch64, an `extern "C"` call passes an `i32` (or narrower integer) that lands on the stack without sign-extending it; a clang-compiled callee reads the slot as already extended | **looks new**; ABI, stable code; found by the ABI differential against clang ([`checks.md`](checks.md)); since at least 1.80; cause found (extension only `if *avail_gprs >= 1` in `callconv/riscv.rs`, same in `loongarch.rs`); [facts](hunt/riscv-stack-arg-extension.md) |
| 20 | on RISC-V and LoongArch hard-float targets, a `repr(C)` struct of one float and one pointer is passed in a floating-point and an integer register; clang passes it by the integer convention, so C and Rust disagree on where it is | **looks new**; ABI, stable code; found by the ABI differential; since at least 1.80; cause found (`Primitive::Pointer` counted as an integer in `should_use_fp_conv_helper`, `callconv/riscv.rs` and `loongarch.rs`); [facts](hunt/riscv-float-pointer-struct.md) |
| 19 | on riscv64 and loongarch64, an `extern "C"` call passes an `i32` (or narrower integer) that lands on the stack without sign-extending it; a clang-compiled callee reads the slot as already extended | **looks new**; ABI, stable code; found by the ABI differential against clang ([`checks.md`](checks.md)); since at least 1.80; cause found (extension only `if *avail_gprs >= 1` in `callconv/riscv.rs`, same in `loongarch.rs`); [facts](hunt/riscv-stack-arg-extension.md), [repro script](hunt/repro/19-riscv-stack-arg-extension.sh) |
| 20 | on RISC-V and LoongArch hard-float targets, a `repr(C)` struct of one float and one pointer is passed in a floating-point and an integer register; clang passes it by the integer convention, so C and Rust disagree on where it is | **looks new**; ABI, stable code; found by the ABI differential; since at least 1.80; cause found (`Primitive::Pointer` counted as an integer in `should_use_fp_conv_helper`, `callconv/riscv.rs` and `loongarch.rs`); [facts](hunt/riscv-float-pointer-struct.md), [repro script](hunt/repro/20-riscv-float-pointer-struct.sh) |
| 21 | `-Zvalidate-mir` rejects MIR the compiler builds from accepted code: projections into `#[repr(simd)]` types (banned by MCP#838) in 9 SIMD tests, and an unsize coercion to `Pin<Box<dyn Future + Send>>` in `async-await/issue-86507.rs` | found by the internal-checks sweep (`mirth-lab crash-diff`); stock nightly with `-Zvalidate-mir`; compiletest does not validate UI tests; [facts](hunt/internal-checks.md) |
| 22 | the new trait solver trips a debug assertion in region outlives (`regions.rs:37`, `!type_outlives.has_non_rigid_aliases()`) on 5 UI tests | debug-assertion builds with nightly's default solver; hidden in CI by compiletest's solver pin; a sibling of closed #160206; [facts](hunt/internal-checks.md) |
| 23 | an `attempt to add with overflow` in `ty/instance.rs:421` compiling `recursion/issue-83150.rs` under the new solver | overflow-checked builds; hidden by the solver pin; [facts](hunt/internal-checks.md) |
Expand All @@ -55,8 +55,8 @@ with `-Zthreads=8`. `rustc/check.sh wide` runs the ordinary checks.
| 29 | machine-applicable lint fixes (what `cargo fix` applies unasked) break builds: `unused_variables` turns `ref b` into a moving `_b` and renames only the declaration of variables mentioned elsewhere, `unused_mut` changes one or-pattern alternative or a variable a `move` closure assigns, `unused_imports` removes a glob that resolution needs | **looks new**; stable 1.98; found by the suggestions-apply check (111 lint fixes in UI tests); six 3–7 line reductions; [facts](hunt/lint-fixes-break-builds.md) |
| 30 | compiler-internal debug output in user-facing diagnostics: under the default (new) solver, E0308 help suggests `as fn(?0t) -> ?0t`; an E0391 cycle note prints `Binder { value: ConstEvaluatable(AliasConst(… DefId(0:7 ~ …` (blessed in `offset-of/inside-array-length.stderr`) | low, diagnostics; found by the diagnostic-invariants check over 18,374 UI tests (excluding tests that ask for verbose output); the first not in CI because of the solver pin ([`solver-triage.md`](solver-triage.md) item I) |
| 31 | `#[rustc_main]` on a struct, impl, trait or module, on stable: after the expected E0658 and "cannot be used on structs", rustc ICEs ("unexpected sort of node in fn_sig()", `collect.rs`): the item is still taken as the entry point | **looks new**, low (error recovery, internal attribute); regression between 1.91.0 and 1.93.0; found by the feature-gate check; [repro](hunt/tests/rustc-main-on-struct.rs) |
| 32 | new-solver compile-time regression: a chain of N `.map()` calls type-checks in 4.5 s / 520 MB at N=200 on nightly-2026-08-03 and 37–58 s / 2.0–2.9 GB from nightly-2026-08-04, with a new "overflow evaluating the requirement `Map<…<Map<_, …>>: Iterator`" future-compat warning; nightly's default solver is the new one, so default builds regressed from 2.2 s (old solver, July) to 53 s | **looks new**, medium (compile time, realistic code shape); bisected over nightlies to #160254 (the only solver PR in the range); found by the scaling check; [facts](hunt/iter-chain-solver-regression.md) |
| 33 | rustdoc panics on an empty nested `use` group (`use {{}};`, `use {{}, {}};`) that rustc accepts: `Option::unwrap()` on `None` in `clean_use_statement_inner` (`clean/mod.rs:3224`) | **looks new**, medium (`cargo doc` crash on valid code); regression on nightly-2026-09-26, range contains #161349 ("Unflatten `use` statements in HIR"); still in nightly-2026-10-10; found by the rustdoc check; [facts](hunt/rustdoc-empty-nested-use.md) |
| 32 | new solver: rejecting an iterator chain over the recursion limit (200 `.map()` calls; every toolchain rejects it) takes ~8× the CPU time and ~4.7× the memory since nightly-2026-08-04 (40 s / 2.4 GB vs 4.9 s / 0.5 GB on 08-03), and reports 74 E0320 errors and 219 overflow warnings where older compilers report one E0275; chains under the limit are not slower. Corrected 2026-10-10: first reported as a regression on valid code | **looks new**, low (error path: slow, noisy rejection of an invalid program); bisected over nightlies to #160254 (the only solver PR in the range, not confirmed by a revert); found by the scaling check; [facts](hunt/iter-chain-solver-regression.md), [repro script](hunt/repro/32-iter-chain-solver-regression.sh) |
| 33 | rustdoc panics on an empty nested `use` group (`use {{}};`, `use {{}, {}};`) that rustc accepts: `Option::unwrap()` on `None` in `clean_use_statement_inner` (`clean/mod.rs:3224`) | **looks new**, medium (`cargo doc` crash on valid code); regression on nightly-2026-09-26, range contains #161349 ("Unflatten `use` statements in HIR"); still in nightly-2026-10-10; found by the rustdoc check; [facts](hunt/rustdoc-empty-nested-use.md), [repro script](hunt/repro/33-rustdoc-empty-nested-use.sh) |
| 34 | rustdoc builds its session options without rustc's post-parse adjustments: `-Ccodegen-units=1` is ignored (`-Zsanitizer=cfi -Clto` rejected), and `-Zassumptions-on-binders` does not switch on the next solver (ICE: `assertion failed: self.next_trait_solver()`) | **looks new**, low (unstable flags); since at least 1.80.0 (codegen units); found by the rustdoc check; [facts](hunt/rustdoc-session-options.md) |
| 35 | rustdoc rejects an associated-const binding rustc accepts (generic const items): "anonymous constants referencing generics are not yet supported"; `clean_hir_term` types the constant with identity arguments (its own FIXME) | low (incomplete features); found by the rustdoc check; [facts](hunt/rustdoc-gca-anon-const.md) |
| 36 | rustdoc panics on an associated-const binding through a supertrait (`T: C<CONST = 2>` with `CONST` in `C`'s supertrait): the lookup searches only `C`'s own items, then `assoc_item.unwrap()` (`clean/mod.rs:539`) | **looks new**, low (incomplete features); found by the rustdoc check; [facts](hunt/rustdoc-supertrait-assoc-const.md) |
Expand All @@ -65,7 +65,7 @@ with `-Zthreads=8`. `rustc/check.sh wide` runs the ordinary checks.
| 39 | rustdoc JSON (even `--document-private-items`): an impl inside a function body names a type local to that body, and its id is in neither `index` nor `paths` (jsondoclint would reject the output) | low (JSON consumers); since at least 1.98.0; related to the open stripped-item dangling-id issues (#113674 family) but nothing is stripped here; found by the rustdoc check; [facts](hunt/rustdoc-json-body-local.md) |
| 40 | `let_underscore_drop` (allow-by-default): fires on `let _ = x;` with `x` a place, which neither moves nor drops it, and its "drop" fix moves the drop (output changes); its two machine-applicable fixes break builds: binding keeps borrowed temporaries alive (E0716), `drop(…)` loses the `let`'s type annotation (E0283) and expression attributes (`#[coroutine]`), and inside a macro rewrites the macro body (`drop()`, `drop($expr;`) | **looks new**, low (allow-by-default; `cargo fix` skips alternative suggestions, #104910); 1.98.0 and nightly; 26 UI tests; found by the lint-oracle check; [facts](hunt/lint-check.md) |
| 41 | lifetime-lint fixes that change meaning: `single_use_lifetimes` deletes a `#[may_dangle]` lifetime but not its attribute, which moves the unsafe promise to the next parameter and still compiles; it turns a derive field's `for<'a> fn(T::A<'a>)` into `'_` (E0637); `unused_lifetimes` removes the `for<'a>` that kept `where for<'a> Inherent: Clone` from being checked (E0277) | **looks new**, low (allow-by-default lints; `may_dangle` is unstable); 1.98.0 and nightly; found by the lint-oracle check; [facts](hunt/lint-check.md) |
| 42 | `dead_code` reports needed items as never used: a trait used only in the where-clause or a projection in the self type of an impl whose methods are called (stable since at least 1.80.0); the `#[define_opaque]` function that is an opaque type's only defining use (removing it: "unconstrained opaque type") | **looks new**; trait cases on stable (warn-by-default), opaque case nightly-only; 3 + 24 UI tests (one blesses the warning); found by the lint-oracle check; [facts](hunt/lint-check.md) |
| 42 | `dead_code` reports needed items as never used: a trait used only in the where-clause or a projection in the self type of an impl whose methods are called (stable since at least 1.80.0); the `#[define_opaque]` function that is an opaque type's only defining use (removing it: "unconstrained opaque type") | **looks new**; trait cases on stable (warn-by-default), opaque case nightly-only; 3 + 24 UI tests (one blesses the warning); found by the lint-oracle check; [facts](hunt/lint-check.md), [repro script](hunt/repro/42-dead-code-used-trait.sh) |
| 43 | `trivial_numeric_casts` calls `5 as i16` an `i16`-to-`i16` cast, but the cast is what makes the literal `i16`: without it the program uses `i32` (prints 4, not 2; a `transmute` size mismatch in a UI test) | **looks new**, low (allow-by-default); 1.98.0 and nightly; found by the lint-oracle check; [repro](hunt/tests/lint-check/trivial-numeric-cast-literal.rs) |
| 44 | mips64 (n64): narrow integer `extern "C"` arguments lose `signext`/`zeroext`, in registers too; a Rust caller passes `x as i32` / `x as i8` without `sll`/`seb`, where the convention and clang/GCC callees expect sign extension | **looks new**, high for the targets (silent wrong values in C callees), tier 3; regression from #163653 (merged 2026-10-04): present in nightly-2026-10-06, absent in nightly-2026-07-18 and 1.98.0; found by `abi-diff --asm`; [facts](hunt/mips64-narrow-int-extension.md) |
| 45 | thumbv7a-{pc,uwp}-windows-msvc: homogeneous float aggregates (e.g. `struct { float a, b; }`) are passed in core registers and returned through memory, where clang uses s0/s1 (AAPCS VFP rules); rustc applies the VFP aggregate rules only to `eabihf` targets | **looks new**, tier 3; since at least 1.80.0; found by `abi-diff --asm`; [facts](hunt/windows-arm32-vfp-aggregates.md) |
Expand Down
58 changes: 30 additions & 28 deletions docs/hunt/iter-chain-solver-regression.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,35 @@
# New solver: long iterator chains take 10× longer and 4× the memory since nightly-2026-08-04
# New solver: rejecting an over-long iterator chain takes 10× longer and gives 75 errors

Facts for finding 32. Found by the scaling check (`mirth-lab scale-check`, shape `iter-chain`:
growth exponent of compile time 2.6–3.5 at N ≤ 100).
Facts for finding 32. Found by the scaling check (`mirth-lab scale-check`, shape `iter-chain`),
which timed out at N=200. Reproduce with
[`repro/32-iter-chain-solver-regression.sh`](repro/32-iter-chain-solver-regression.sh).

**Correction (2026-10-10).** The first version of this finding reported a compile-time
regression on valid code. That was wrong: the 200-map program is over the recursion limit and
is rejected by every toolchain; the earlier timings did not check the exit status. Programs
under the limit are not slower under the new solver (it is faster there). What changed is how
the rejection behaves.

## What happens

[`tests/iter-chain-200.rs`](tests/iter-chain-200.rs) is one statement:
`(0u64..10).map(|x| x.wrapping_add(0)) … .map(|x| x.wrapping_add(199)).sum()`, 200 `map`
calls. User time and peak memory of `rustc iter-chain-200.rs` (-Copt-level=0):
`(0u64..10).map(|x| x.wrapping_add(0)) … .sum()` with N `map` calls
([`tests/iter-chain-200.rs`](tests/iter-chain-200.rs) is N = 200). From N = 128 the
`Map<Map<…>>` type exceeds the default recursion limit and every toolchain rejects the program.

| toolchain | default | `-Znext-solver=coherence` (old solver) | `-Znext-solver=globally` |
| N | toolchain (solver) | result | user time, peak memory |
|---|---|---|---|
| 1.80.0 | 5.7 s, 115 MB | | |
| 1.90.0 | 7.0 s, 125 MB | | |
| 1.98.0 | 2.3 s, 118 MB | | |
| nightly-2026-07-18 | 2.2 s, 119 MB | 2.2 s, 118 MB | 4.6 s, 517 MB |
| nightly-2026-08-03 | | | 4.5 s, 519 MB |
| nightly-2026-08-04 | | | 37.3 s, 2.42 GB |
| nightly-2026-10-06 | 53.4 s, 2.04 GB | 6.1 s, 131 MB | 52.8 s, 2.04 GB |

At the mirth pin (default solver): N=50 0.19 s, N=100 1.2 s, N=200 68.6 s and 2.0 GB.
`-Ztime-passes` puts 65 of 73 s in `type_check_crate`. From nightly-2026-08-04 the build also
warns once, "overflow evaluating the requirement `Map<Map<…<Map<_, {closure@…}>…>>: Iterator`"
("this was previously accepted by the compiler but is being phased out"); earlier nightlies
do not warn.

Two separate changes show in the table: the new-solver slowdown between 2026-08-03 and
2026-08-04 (this finding), and the default switching to the new solver between July and
October ([`solver.md`](../solver.md)). The old solver also went from 2.2 s to 6.1 s over the
same period; not bisected.
| 126 | 1.98.0, nightly-2026-07-18 (old) | compiles | 3.3–3.6 s |
| 126 | nightly-2026-10-06 (new, default) | compiles | 1.9 s |
| 126 | nightly-2026-08-03 / -08-04, `-Znext-solver=globally` | compiles, one overflow warning | 2.4 s / 2.5 s, 425 MB |
| 200 | 1.98.0, nightly-2026-07-18 (old) | E0275 "overflow evaluating the requirement `Map<…>: Iterator`", 1 error | 2.2 s, ~120 MB |
| 200 | nightly-2026-08-03, `-Znext-solver=globally` | rejected, 1 error, 2 overflow warnings | 4.9 s, 507 MB |
| 200 | nightly-2026-08-04, `-Znext-solver=globally` | rejected, **74 × E0320** ("overflow while adding drop-check rules for `Map<…>`") and 219 overflow warnings | **40.4 s, 2.37 GB** |
| 200 | nightly-2026-10-06 (new, default) | the same as 08-04 | ~53 s, 2.0 GB |

So since nightly-2026-08-04, under the new solver (nightly's default), rejecting the program
costs about 8× the CPU time and 4.7× the memory, and reports 75 error lines and 219 warnings
where older compilers report one error. The overflow warnings are the future-compatibility lint
"this was previously accepted by the compiler but is being phased out".

## Bisection

Expand Down Expand Up @@ -64,6 +65,7 @@ fulfillment iteration, which would fit the growth with N, was not checked.

## Scope

Any method chain long enough that the solver overflows on the receiver's trait goal while its
innermost type is still being inferred. 200 is long for hand-written code; generated code and
builder- or iterator-heavy macros reach it.
Programs that are rejected anyway: a method chain over the recursion limit (here from 128
calls). The cost is a slow, noisy error (tens of seconds and gigabytes, 75 errors instead of
one) rather than wrong acceptance or a slower valid build. Generated code and macros that build
long chains are where a user would meet it.
Loading
Loading