Proposal: Speed up `cargo build` by constraining language to disallow mutual usage between files

Rust allows recursive usage between .rs files.

What if cargo/rust compiler could restrict Rust to subset of language that allows using mutual dependencies only inside of same file.

Would it give speedup? Is there such option already? Or alternative rust compiler?

I do not think that would give much of a speedup, mainly because a file is not really the unit of compilation in Rust.

In C and C++, each .c or .cpp file is a separate translation unit, so file boundaries really do affect how much the compiler can parallelize. Rust works differently. The compilation unit is the whole crate. rustc parses the crate and then does name resolution, type inference, borrow checking, trait resolution, and monomorphization with whole-crate visibility.

Modules and files are mostly just organization inside that crate. References between items in different files are completely normal, and the compiler is designed around that. So forbidding those references would not unlock per-file parallel compilation, because Rust was not compiling those files independently in the first place.

The good news is that the idea you are reaching for already exists, just at the crate level instead of the file level.

Crates are the real compilation boundary. Cargo can compile independent crates in parallel and cache them separately. So if one large crate rebuilds slowly, the Rust-shaped version of this idea is to split it into smaller crates with a clean dependency graph.

There are also already some knobs inside a crate. codegen-units lets the backend split work into parallel chunks, incremental compilation avoids redoing unchanged work, and the parallel frontend work on nightly is aimed at speeding up parts of the analysis phase.

One practical trick: if a heavy dependency dominates your rebuild time, put it behind a thin facade crate. Then it tends to compile once and stay cached while you iterate on the rest of your code.

So I think the dial you want is real. In Rust it is usually spelled “split the crate boundary thoughtfully,” rather than “restrict references between files.”

If you mean making files the compilation unit like in C/C++, then the answer is no, never was, never will. This will make up an entirely different (and IMO, much worse) programming language that is incompatible with pretty much 100% of all Rust code ever written, and the chances of that happening are just absolute zero.

If you mean limiting some interaction, then like @robertnio said this is unlikely to have a significant benefit. Furthermore, if you require declaring that statically you make it very inconvenient and no existing Rust code will benefit from it, and if you infer it dynamically then it already exists: that's pretty much rustc's incremental compilation system.

Yeah, I think the incremental-compilation point is the cleanest way to frame it.

The dynamic version of this idea basically already exists, and it is actually finer-grained than files. rustc incremental compilation is query-based, so it tracks dependencies closer to the item level rather than treating a whole source file as the unit of work.

So if you change one function, the compiler can recompute the queries that depended on that change instead of blindly rebuilding the entire file it happened to live in.

That is why “infer it dynamically” is not only possible, it is already a better fit for Rust than using file boundaries. The proposal is reaching for a real build-speed concern, but file-level granularity would be a step backward compared with what Rust is already designed to do.

While true, it should be noted that having this information statically will allow for much faster compilation, even compared to the current incremental system.

But also, like I said, that's definitely not an option for Rust, and IMO a bad idea for languages in general.

No, not at all. The parsing step is essentially trivial and the hard parts of name resolution have nothing to do with file splits. Plus usually the slowest part of compilation is codegen, which is already parallelized without caring about what's in what files, and wouldn't be improved by this either.

(And when you include things like traits, this is completely impractical anyway.)

There are some things that could help the IDE experience, like [Edition vNext] Consider deprecating weird nesting of items · Issue #65516 · rust-lang/rust · GitHub, but those have nothing to do with file splits.

There is a way in which code being in the same file does have a cost which looks a bit like “rebuild the entire file”. If you change a function’s source code so that it has a different number of lines, then all the lines below it are renumbered, which means that every explicit or implicit panic!() must have different line and column data recorded, so those functions get somewhat recompiled. Proc macros also have to rerun since they can see line numbers.

This doesn’t mean that everything is recomputed, but if you care a lot about about incremental compilation performance, you can improve it by using more separate files.

Note that this is not given. rust-analyzer uses an incremental system very similar to rustc's, yet is way more incremental due to being designed from the ground up around that. Of course the work is different (we don't have to track line number for panics), and latency is more important than throughput, but even when requirements are similar we often do things differently. For instance, in r-a proc macros won't be rerun unless they actually observe the line numbers (and most don't).

Note that at least for rust-analyzer, we just never supported the patterns that are problematic for us, so forbidding them won't make r-a faster. Of course it's still important, e.g. non_local_definitions was a very important step.

Do note that assuming proc-macros are idempotent is approximately fine, but incorrect, as far as the language goes. I do want us to have an annotation for proc-macros to explicitly claim that they are idempotent (if the token stream being fed into it, the output can never change, which means they can be safely cached/not re-run), and potentially change the default over an edition boundary.

Changing the behavior of the language here would be a breaking change. IDEs (and diagnostic recovery) can afford to use approximations and heuristics, but codegen can't.

The letter of the law has never allowed them to do that (unless the letter of the law is rustc's implementation, of course, which... maybe?), this is cursed and I think the people designing them has never imagined that.

However, unfortunately some very popular crates (I'm looking at you sqlx) do that, so that is settled.

Something like cargo build script rerun-if lines would be nice if we're making a shopping list...