Resolving cyclic dependency at module level?

Hi! I wonder whether Cargo can resolve dependencies on module level? It's finer-grained than crate-level dependency resolution in use.

The cyclic dependency problem I met arises from one of my project. My project is in a mono-repo, in which entities crate is generated by sea-orm, binary crate for main entrance, types for common types. In types::ids module, I define ID newtypes. This module only depends on uuid crate. In types::data module, I defined common data types, so the module depends on entities.

I want to use my ID newtypes in entities to enforce stronger type checking, then entities depends on types, thus a cyclic dependency error....... The error is understandable, but I think dependency resolution can be better if it can check dependencies at module level instead of at crate level.

The "dependency graph" at module level can be seen as

types::data -> entities
entities -> types::ids
types::ids -> uuid 

This in theory is compilable, since there is not a theoretically cyclic dependency on types (I mean types, not the crate types).

Is there existing effort to make module-level dependency resolution a reality? Or, is it just not as simple as I think and too hard to implement?

Something is not right. You are talking about module-level dependencies first, but then you claim that you have separate crates. Cyclic dependencies between crates are not supported, but they are supported between modules of the same crate. This compiles.

It fundamentally contradicts the definition of a crate (an independent unit of compilation).

1 Like

Cargo resolves dependencies on a package level (if not inheriting them from the workspace), not on a crate level. All crates in your package share the same dependencies.

1 Like

Assuming types and entities are crates, you need to break the cycle.

types::data -> entities

Can you define data without referring to specific entities? Could types define a trait, and then have entities -> types that implement the trait?

Alternatively, split types in two. types_with_entities -> types & entities -> types.

Yeah, I meant resolving dependencies between crates at a module level. I know "Cyclic dependencies between crates are not supported, but they are supported between modules of the same crate". To make what I meant more clearly, I drew the "dependency graph" at module level between crates.

It fundamentally contradicts the definition of a crate (an independent unit of compilation).

Yes, I know this is a fact. But I read something about parallel compilation in Rust, as far as I understand, a crate can be decomposed into smaller unit of compilation, which is a module. So, I wonder maybe this can also be done earlier when resolving dependencies.

Yeah, practically I just copy and paste types::ids as another crate ids in my project's workspace. But I think this is a bit awkward.

The dependency resolver can be more intelligent. To be clear, I mean nothing that the current resolver is not intelligent, and it is in fact much better than others in other languages, but it can always be better.

Lack of cycles is a useful property.

Rust-analyzer's author strongly dislikes the module resolver, so there's an argument that it's already too complicated.

1 Like

OK.... do you have a pointer to relevant discussion about the module resolver you mention?

pinging @matklad

To be more specific lack of cycles is a useful property. But cycles are also useful. And in Rust you have both:

  • If you want to have cyclic dependencies, you put the entire cycle in a single crate
  • If you dont need cyclic dependencies, than you can put your code into different crates and enjoy the benefits of separate compilation.

I think that's pretty much the pefect setup, and the big picture of Rust module/crate system is the second-best feature of Rust (after memory safety sans gc).

I do have some qualms with small small picture implementation:

  • I don't think it's a good idea to support unconstrained re-export. In current Rust, you can have infinite paths like foo::bar::foo::bar::foo::bar etc if you set up re-exports right. I think it woudl be possible to constrain the system in such a way that the tree of things defined in modules is finite, which would allow incremental compiler to be faster, without really loosing any expressiveness (in particular, you could still have cyclic dependencies between the modules).
  • The interaction between macro expansion ordering and module resolution are peculiar. I can't say how exactly I'd constrain this, but this (and not something like traits or lifetimes) is why Rust needs to go all the way to salsa, and why a simple map-reduce IDE doesn't work
  • :: is needless syntax, we should have been fine just using . :rofl:
3 Likes