rust-lang/rust · critical
projecting to field of non-ADT {ty}
Error message
projecting to field of non-ADT {ty} What it means
`Place::project_to_field` only accepts places whose type is `ty::Adt`; for any other kind (primitive, array, tuple, closure, etc.) it panics. rustc_middle enforces this precondition because field indices in `PlaceElem::Field` are indexed against an ADT's `non_enum_variant().fields`, which has no meaning for non-ADT types.
Source
Thrown at compiler/rustc_middle/src/mir/statement.rs:449
pub fn project_deeper(self, more_projections: &[PlaceElem<'tcx>], tcx: TyCtxt<'tcx>) -> Self {
if more_projections.is_empty() {
return self;
}
self.as_ref().project_deeper(more_projections, tcx)
}
/// Return a place that projects to a field of the current place.
///
/// The type of the current place must be an ADT.
pub fn project_to_field(
self,
idx: FieldIdx,
local_decls: &impl HasLocalDecls<'tcx>,
tcx: TyCtxt<'tcx>,
) -> Self {
let ty = self.ty(local_decls, tcx).ty;
let ty::Adt(adt, args) = ty.kind() else { panic!("projecting to field of non-ADT {ty}") };
let field = &adt.non_enum_variant().fields[idx];
let field_ty = field.ty(tcx, args).skip_norm_wip();
self.project_deeper(&[ProjectionElem::Field(idx, field_ty)], tcx)
}
pub fn ty_from<D>(
local: Local,
projection: &[PlaceElem<'tcx>],
local_decls: &D,
tcx: TyCtxt<'tcx>,
) -> PlaceTy<'tcx>
where
D: ?Sized + HasLocalDecls<'tcx>,
{
// If there's a field projection element in `projection`, we *could* skip everything
// before that, but on 2026-01-31 a perf experiment showed no benefit from doing so.
PlaceTy::from_ty(local_decls.local_decls()[local].ty).multi_projection_ty(tcx, projection)
}View on GitHub (pinned to 22057b88b0)
Solutions
- Before calling `project_to_field`, dispatch on `place.ty(...).ty.kind()` and only call it for `ty::Adt(..)`; route tuples, closures, and arrays through their dedicated projection logic.
- If you meant tuple element access, use `ProjectionElem::Field` with the tuple's per-element field type via `PlaceTy::projection_ty`, not `Place::project_to_field`.
- For closure/coroutine captures, ensure the place is the synthesized closure-state ADT (or use the capture-path API) before treating it as ADT.
- Reproduce with `-Zmir-opt-level=0`; if stock rustc hits this in user code, file an ICE with the backtrace and minimal repro.
Example fix
// before
let fp = place.project_to_field(idx, local_decls, tcx);
// panic: projecting to field of non-ADT [T; 3]
// after
match place.ty(local_decls, tcx).ty.kind() {
ty::Adt(..) => place.project_to_field(idx, local_decls, tcx),
_ => /* handle tuple/array/closure case explicitly */
} Defensive patterns
Strategy: type-guard
Validate before calling
// project_to_field() asserts the base type is an ADT (ty::Adt(..)).
// Verify the type kind BEFORE projecting.
use rustc_middle::ty::{self, Ty};
fn is_field_projectable(ty: Ty<'_>) -> bool {
matches!(ty.kind(), ty::Adt(..)) // structs, enums, unions
}
// Usage:
// let base_ty = place.ty(local_decls, tcx).ty;
// if is_field_projectable(base_ty) {
// place.project_to_field(idx, local_decls, tcx);
// } else {
// // tuples, closures, arrays, slices, primitives -> use the correct projection
// } Type guard
// Strict narrowing: only proceed when the type is exactly an ADT.
use rustc_middle::ty::{self, Ty};
fn require_adt<'tcx>(ty: Ty<'tcx>) -> Option<(&'tcx ty::AdtDef<'tcx>, &'tcx ty::GenericArgsRef<'tcx>)> {
match ty.kind() {
ty::Adt(def, args) => Some((def, args)),
_ => None,
}
}
// Pattern:
// match require_adt(base_ty) {
// Some((def, args)) => place.project_to_field(idx, local_decls, tcx),
// None => /* pick the right projection for tuples/closures/arrays */
// } Prevention
- `project_to_field` is for ADTs only; tuples, closures, coroutine state, arrays, and slices each have their own field/tuple-index projection — choose the one matching the base type kind.
- Always derive the projection element from `ty.kind()` rather than assuming a place is an ADT because of how it was written in source.
- When lowering field accesses in a macro/lint, branch on `ty.kind()` first (Adt vs Tuple vs Closure vs ...) and emit the matching PlaceElem.
- If you refactor a type from struct to tuple/struct-to-array, audit every call site that projects a field — a previously-valid ADT projection becomes this panic.
When it happens
Trigger: Triggered by code that calls `place.project_to_field(idx, local_decls, tcx)` on a place whose `.ty(...).ty` is not an ADT — for instance a custom MIR pass or analysis that walks `PlaceElem::Field` without first checking the base type, or hand-built MIR that uses `Field` projection on a tuple/array/closure place.
Common situations: Out-of-tree MIR tooling assuming all field projections target structs; mir-opt regression that mis-types a downcast place; closure/coroutine state-access where the place type is `Closure`/`Coroutine` rather than the synthesized ADT; user-reported ICE on code that hand-rolls field access via macros that lower to MIR field projection.
Related errors
- invalid terminator state
- destructed mir constant of adt without variant idx
- expected subslice projection on fixed-size array
- range should be nonempty
- there must be provenance somewhere here
AI-assisted analysis of rust-lang/rust@22057b88b0 (2026-08-03).
Data as JSON: /data/errors/20bf8511390be256.json.
Report an issue: GitHub.