Why are most crates > 1000 LOC?

The orphan rules make some otherwise desirable refactorings impossible.

It's quite common to have code which just needs to reference types/traits as part of type signatures, but never directly calls any of the code. It would often be nice to have an "interface" crate with just type or trait definitions, and another "impl" crate which has the actual implementations.

But the orphan rules prevent the implementations of a trait for a type being anywhere other than either in the trait or type defining crate, which in some cases can mean bringing a substantial amount of code. Even worse, it means that all downstream users also get that code's transitive dependency graph.

I know there's been some discussion of "friend" crates which are allowed to locally break the orphan rule (because all the crates are controlled by / originate from the same entity). No idea if there's a concrete plan for this though.

6 Likes

One particularly annoying e

One particularly annoying example of this is: consider something like pub struct Mat4f(pub [f32; 16]).

There are parts of my code where all I need to do is read the slice and feed it to OpenGL. However, to pull in Mat4f, I need to pull in the crate that contains all the matrix ops, and this also includes pulling in Vec4f and related functions.

2 Likes

For that particular situation, consider mint — a deliberately minimal crate which provides vector and matrix types, but not any operations.

I don't think we are discussing the same problem here. My goal is:

I want to pull in the "layout" of a struct, without pulling in all the functions of the struct.

In particular, if X is defined in crateA, and crateB depends on the layout of X:

Right now, crateB has to wait for all of crateA to parse (and even type check?); whereas I'd like to separate this into

crateA_header , crateA, crateB

crateB depends only on crateA_header

crateA_header contains "struct X { ... }"
crateA contanis "impl X { ... }"

1 Like

Maybe the dependency should put the functions into optional features. I guess it would be a good habit to provide a very fine granularity with lots of features, but it makes testing more complex (the all-features or default-features cases may work but some untested combination of features that should work may fail).

This doesn't help with compile pipelining, though. If anything, it can make it worse!

In the workspace, the feature is going to be used. The difference is that there's a diamond dependency involved here; you have dependency edges e.g. bin -> utils, bin -> math-impls, math-impls -> math-types, and utils -> math-types. In this scenario, math-impls and utils can build in parallel, but if math-impls is part of math-types (because it wants to define inherent and trait impls), it needs to be metadata-ready before starting work on utils, despite none of that actually being used.

Cargo builds can pipeline further, but the benefits of starting parsing before meta is available is typically extremely minimal. Starting downstream before upstream type checks might be worth it in more cases, but it's typically more payoff to improve compiler performance directly than the large effort required to allow rustc to start working before the dependency metadata is available.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.