RFA-597 · Case file with fixtures · Case 569 of 694 · Compiler evidence
Rust main Cannot Have a where Clause
The process entry has a fixed language contract. Move generic constraints into ordinary functions and keep main responsible for concrete composition.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- executable targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- A reusable generic constraint was attached to the fixed process entry contract instead of an ordinary helper whose call selects concrete types.
- First discriminating check
- Move parameterised logic and bounds into a run function, leaving main to compose concrete dependencies and manage process concerns.
main is a special process entrypoint called by Rust's startup machinery. There is no ordinary Rust caller choosing generic arguments or proving arbitrary bounds. The failing fixture attaches a where i32: Copy clause and receives E0646.
A true bound belongs to a parameterised item
The example bound is always true, but truth is not the issue. A where clause constrains generic parameters or other type relationships for an item's callers. The entrypoint has a fixed accepted form and is not designed as a generic abstraction.
The official E0646 page states the restriction. The Reference documents accepted main function signatures and termination behaviour.
I inspect how a bound arrived there. A macro may append a common where clause to every generated function, or a developer may try to share generic startup logic directly in main.
The repair separates entry from generic engine
The repaired fixture moves the generic bound to run<T> and calls run::<i32>() from ordinary main. Now a normal Rust call site chooses the concrete type and satisfies its contract.
Real applications benefit from this split. main selects concrete adapters—database client, clock, transport, configuration—and passes them to testable generic or trait-based application logic. Tests can substitute deterministic implementations without simulating process startup for every case.
The entrypoint remains responsible for arguments, environment, tracing, final errors, and exit status. Generic business logic should not know how the operating system launched it.
Concrete composition is a useful architecture boundary
Applications eventually need concrete choices. A generic server can abstract storage, but the binary must choose Postgres or an in-memory implementation. Keeping that selection near main makes deployment wiring searchable.
I avoid making the entire application generic through many layers if dynamic dispatch or simple concrete types are easier. Monomorphisation can increase build time and binary size; trait objects can simplify composition at some runtime cost. The correct choice depends on measured needs.
E0646 usefully prevents the special entrypoint from becoming an invisible generic root whose instantiation is unclear.
Macro-generated entrypoints need a separate form
Async runtimes often provide an attribute macro on main. The macro transforms an async-looking entry into runtime construction plus a supported synchronous entrypoint. Its accepted signature and attributes are versioned API.
If a shared macro generates both normal functions and main, I give entrypoints their own template instead of conditionally deleting compiler errors after expansion. Expanded code should contain one valid crate-level main and put bounds on helpers.
Embedded and custom runtime programs may use no_main and exported symbols rather than the standard entrypoint. That is a different target contract with ABI, linker, and startup-state obligations. It is not justification for a where clause on ordinary main.
Error handling should remain concrete too
main can return supported termination types, or it can call a fallible run and map errors deliberately. I prefer structured errors internally and concise safe output at the process boundary. Configuration secrets should not appear in debug chains by accident.
Smoke tests launch the packaged binary and verify arguments, startup, and exit status. Unit tests call run with test dependencies. This combination proves both the fixed entry contract and the generic engine.
Several binaries can reuse the same bounded core
A Cargo package may provide a server, migration utility, and maintenance command. I keep each main concrete and small, then let all three call library functions parameterised over the capabilities they genuinely share. This prevents command-specific parsing and exit policy from leaking into the reusable engine.
It also makes feature combinations easier to reason about. Each binary target selects available adapters under explicit configuration, while the library tests generic contracts once. If a target is intentionally unavailable under one feature set, the manifest or cfg should express that target policy rather than leaving behind a malformed main. Clear composition scales better than trying to make one magical entrypoint adapt to every environment.
My E0646 checklist
- Is the where clause attached directly to the special crate entrypoint?
- Which generic relationship was the code trying to express?
- Can that relationship move to an ordinary
runor builder function? - Which concrete implementations should the binary compose?
- Did an attribute or code-generation macro add the clause uniformly?
- Does the supported main signature handle errors and termination correctly?
- Is a custom no-main runtime genuinely part of the target contract?
- Do both engine tests and packaged-binary smoke tests cover the split?
The core principle is that abstraction and process entry are different layers. Generic helpers describe reusable families of behaviour. main makes concrete choices and hands control to one of them under the runtime's fixed contract.