Implementing `impl Trait` in gccrs: APIT, RPIT, and Desugaring
Desugaring but just not where we should?
In this post, I'll walk through how `impl Trait` both in argument and return positions is being implemented in gccrs, the GCC front-end for Rust. This feature is widely used in Rust and has subtle but important differences depending on whether it appears in argument position (APIT) or return position (RPIT).
`impl Trait` in Rust: APIT vs RPIT
Argument-position `impl Trait` (APIT) works like syntactic sugar for a named generic parameter. It allows users to write more concise function signatures without explicitly naming the type parameter. However, this does not mean the function behaves exactly like one declared with a named generic , the type is inferred at the call site, and the generic parameter is implicit and inaccessible to the caller. Internally, the compiler still treats this as a generic function, and type checking proceeds accordingly.
This section explains how `impl Trait` behaves depending on its position in function signatures, but some limitations apply when using it in Rust today. We'll move these into a dedicated limitations section below.
Rust allows you to write functions like:
These are two different forms of `impl Trait`:
Argument-Position `impl Trait` (APIT)** is syntactic sugar for a generic parameter:
Return-Position `impl Trait` (RPIT)** is *not* a generic , it creates a unique opaque type known only inside the function.
While `impl Trait` in return position is opaque *at the language level*, it is *not* opaque to the compiler. Internally, the compiler always knows the exact type being returned , the opaque type is backed by a concrete type inferred from the function body. This is essential for things like method resolution, trait selection, and code generation. If the rustc compiler didn’t know the actual type, it wouldn’t be able to emit valid MIR or LLVM IR, nor determine which methods are valid on the returned value.
The takeaway here is that APIT behaves like generics inferred at call site, while RPIT introduces an opaque return type chosen by the callee.
`impl Trait` Limitations
Although `impl Trait` provides ergonomic abstraction in function signatures, there are some current limitations in stable Rust:
You cannot explicitly specify generic arguments for APIT-style functions. For example:
This will produce a diagnostic like:
You cannot use `impl Trait` in a `let` binding directly:
This will trigger a diagnostic like:
These restrictions are in place to preserve the distinction between existential types and named generics and may be relaxed in the future via `type_alias_impl_trait` or other RFCs.
My Opinion on `impl Trait`
Personally, I'm not a fan of `impl Trait`, especially in argument position. While it appears to simplify function signatures, **APIT** (`impl Trait` in argument position) is *just syntactic sugar* for a generic parameter, and that sugar often hides complexity instead of removing it. You can't name the inferred type, you can't specify it explicitly, and yet the compiler is doing all the work of generics anyway.
Return-position `impl Trait` (**RPIT**) is more interesting, and nearly expressible without special syntax if Rust allowed you to define a return type with an unnamed inference variable and constrain it via `where`. since rust already supports inferring generic parameters anyway so seems like so it almost feels like this could be a generic too.
My broader concern is that Rust is getting increasingly complex, often by layering new syntactic forms on top of existing semantics. This makes the language harder to teach, harder to implement, and harder to reason about at the compiler level. In gccrs, we see this complexity first-hand when trying to make each of these features interact cleanly with name resolution, substitution, and monomorphization.
In short, while I understand the ergonomics `impl Trait` brings, I worry that it's pushing Rust's surface complexity further than it needs to go , especially when better foundational primitives already exist (e.g., opaque types).
There are also several open RFCs looking to extend the usage of `impl Trait`, including support for it in more positions (like let bindings and trait items), as well as tighter integration with `type_alias_impl_trait` RFC 2515. While these features may provide more power and convenience, they also increase the surface area and mental burden for both users and implementers.
Implementing Early Desugaring in gccrs
One downside of our current approach is that for each non-trivial desugar that occurs at the AST level , such as `for` loops, the `?` operator, and `impl Trait` , we walk and mutate the crate separately for each construct. This is less efficient and more error-prone than doing all transformations in a single HIR lowering phase. However, this trade off simplifies integration with our existing pipeline for now. We plan to consolidate this logic in the future as the HIR layer and desugaring infrastructure in `gccrs` matures.
In `rustc`, the desugaring of `impl Trait` is deferred until *after* name resolution and is handled during HIR lowering. This lets the compiler use resolved `Res` information when generating synthetic generics or opaque types.
In `gccrs`, however, we are currently doing desugaring **prior to HIR lowering**. Why?
While changing the pipeline is non-trivial and would be a distraction from getting things working right now, and performing desugaring before HIR lowering ensures consistency with our current pipeline and avoids needing to thread unresolved constructs through name resolution.
We can produce correct semantics earlier by synthetically generating the generics ourselves just slightly more work than if we could do it at the hir level.
It unblocks work on trait resolution and monomorphization while keeping the pipeline moving.
A More Complex Desugaring: Nested `impl Trait`
Here's a function that uses `impl Trait` inside an associated type – this is more involved than simple APIT or RPIT, because it requires introducing multiple generic parameters and adding trait bounds in a `where` clause.
At first glance this may look straightforward, but `impl Foo` inside the associated type is itself an RPIT-like position – we don't want to leak the underlying type. Instead, we must synthesize a second generic parameter `U` for the inner `impl Foo` and add the necessary constraints on both `T` and `U`. This desugars to:
You might ask: why not always add bounds like this in the parameter list? The answer is: Rust requires more complex constraints (especially those involving associated types or higher-ranked lifetimes) to be written in `where` clauses for clarity and disambiguation. gccrs follows suit by placing these generated bounds in a `where` clause to preserve idiomatic structure and avoid ambiguity in types with nested `impl Trait`s. that uses `impl Trait` inside an associated type:
What’s Next
This desugaring strategy works for now, but long-term, the plan is to shift this logic into HIR lowering like `rustc`, where we’ll have proper resolution context and a richer IR to work with.
Argument position impl traits support is almost ready to be merged so whats left is to:
Supporting RPIT with `-> impl Trait` as an opaque type tied to the function.
This is more work but the guts of the opaque type are already merged just needs finished off
Error diagnostic when passing generic arguments to impl traits
Error diagnostic for using them outside of function parameters and return types.
This work lets us move forward with generics, monomorphization, and trait-solving in a way that aligns with user expectations but fits our current implementation maturity in gccrs.
Stay tuned!
---
Thanks to Open Source Security and Embecosm for their continued support of the gccrs project.
Stay up to date with progress: