Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-515 · Case file with fixtures · Case 487 of 694 · Compiler evidence

The Rust Main Function Cannot Have Generic Parameters

The runtime enters one concrete main function with a prescribed signature. Put generic work in ordinary functions and let main select concrete types from configuration or program structure.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all Rust targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
Runtime startup invokes one concrete entry symbol rather than a monomorphised family, leaving no Rust call site that could select T.
First discriminating check
Keep main concrete as the composition root and delegate reusable generic work to a run function after selecting concrete adapters and configuration.

I declared fn main<T>(). Rust emitted E0131 because the executable entry point cannot ask an ordinary Rust caller to choose T.

The failing fixture places genericity at the boundary where the operating environment starts the program. There is one entry symbol and no turbofish call site.

Main is selected by the executable runtime contract

Rust recognises one crate-level main function with an allowed concrete signature. Startup code invokes it according to the platform and language runtime conventions.

A generic function is a family of possible monomorphised functions. Something must select its type arguments. The operating system does not know Rust types or choose one member of that family.

The official E0131 page therefore rejects generic parameters on main.

The repaired fixture delegates to a generic helper

The repaired fixture keeps main concrete and calls run<T>() with T inferred as u8 from the binding.

This preserves reusable generic logic without making startup ambiguous. In real programs, main parses arguments and configuration, chooses concrete adapters, connects dependencies, and hands them to generic or trait-based application code.

I keep the entry point thin and explicit.

Runtime choice is different from generic choice

Suppose a command-line flag selects PostgreSQL or an in-memory store. That choice happens while the program runs. Rust generics are normally resolved into concrete compiled code before runtime dispatch.

I can branch and call two concrete generic instantiations, use an enum, or use a trait object when open runtime selection is needed. Adding <T> to main does not connect a CLI value to a type parameter.

The dispatch design should match when the decision becomes known.

Dependency injection belongs below main

I often write fn run<R: Repository>(repository: R) -> Result<()>. Tests call it with a fake repository, while main constructs the production implementation.

This gives generic testability and keeps environment access at the outer edge. It also avoids hiding startup failure inside global state.

The concrete composition root is a feature, not a limitation.

Async runtimes still generate a concrete entry point

Attributes such as #[tokio::main] transform an async-looking function into runtime setup and a concrete synchronous entry. Macro restrictions may produce additional diagnostics, but generic startup remains unresolved for the same reason.

I put type parameters on the async worker called by the annotated main. When debugging, expanded code can show which signature the macro generated and what return types it accepts.

The entry remains one chosen program.

Main return types have their own contract

Rust permits certain return types that implement the termination contract, commonly Result forms. This flexibility concerns how one concrete outcome becomes a process exit status; it does not permit arbitrary generic parameters.

I choose a return form that preserves error reporting, then keep domain logic in ordinary functions where types and errors can be richer.

The Reference on main functions is the authoritative signature guide.

Feature flags can choose concrete composition at build time

When one backend is selected by Cargo features, conditional modules can expose the same concrete application interface and main can call the selected implementation. I validate mutually exclusive or required feature combinations so the entry point never depends on an unspecified generic.

This differs from runtime selection. Feature-based code produces a particular binary composition, while trait objects or enums let one binary choose after startup. I document which model the program uses because deployment teams need to know whether changing a backend requires a rebuild.

Tests should call application logic without launching a process

A small concrete main lets unit and integration tests invoke run directly with controlled dependencies. Separate process-level tests then verify arguments, environment parsing, exit codes, and signals. This division makes generic logic reusable while keeping startup behaviour observable.

I avoid placing business logic only inside main, because its special status and environment coupling make focused tests harder.

My E0131 checklist

  • Is the generic parameter declared directly on crate-level main?
  • Which caller would supposedly choose that type?
  • Can generic work move into a run helper?
  • Which concrete implementation should startup construct?
  • Is the choice compile-time, feature-based, or runtime configuration?
  • Would an enum or trait object model runtime selection?
  • Does an async-main macro impose another concrete signature?
  • Is error-to-exit-status conversion kept at the outer boundary?

The core principle is that an executable begins at one concrete function. Generic families remain valuable below that point, after main explicitly composes the program that will run.