The Great Module Adventure Continues

Yes, this is interesting. I also agree that massive and noisy deprecation, while technically backwards compatible, may not feel great. In the past we have sometimes thought about doing a kind of "silent deprecation", in which e.g. tools like rustfmt just rewrite your code to use the now preferred form, but you never get warnings or errors about it. Those might come only later, if we actually decide (e.g., in the next epoch) to remove that deprecated form entirely in order to make room for something new.

1 Like

Man, I had forgotten how many comments were on the original RFC. If I get some time, I think I will try to go and copy over some of the more persuasive arguments on these various questions. Seems like it’d be useful to avoid retreading ground.

A lot of the discussion there however focused on a slightly different alternative, one in which we introduce extern paths but (conceptually deprecate extern crate), leading to use foo::bar for the local crate and use extern::regex::Regex for others.

I personally find that less appealing than variant 1, but because of the increased asymmetry (you can think of extern::regex as a kind of “unit”) and more importantly because we lose the distinction between absolute vs relative paths.

3 Likes

Hm, I think in this regard ::std/::crate proposal is isomorphic to variant 1? Like, in V1 we deprecate use std::foo and require to write use extern::std::foo. In ::std/::crate we as well deprecate use std::foo and require to write use ::std::foo. The :: plays exactly the same role as extern::, but is strictly shorter :slight_smile:

Am I correct that we don't want to lean on epochs to make a breaking change here, because "to upgrade to a newer epoch, you must change every use statement" is unacceptable?

Perhaps you are discussing a Variant 3, and I am confused. Are you saying that I would write use ::std::foo and use ::crate::foo, or use std::foo and use crate::foo?

Yep, I am discussing variant 3, which is (if we forget about backwards compatability) a slight modification of variant 1, which makes syntax less noisy.

In variant 1, absolute paths always begin with extern::crate_name or with crate::module_name. In my proposal, absolute paths begin with ::crate_name or with ::crate::module_name.

So yeah, one would write

use ::std::foo; 
use ::crate::bar;

I am unsatisfied with variant 1, because it completely deprecates ::foo form of paths, which, at least to me, seems strictly better than extern::.

EDIT: “seems strictly better” on the syntactic level: I want to reuse ::foo syntax, but with a semantics of variant 1: it looks up foo crate (and not foo item in the root module), and the current crate gets the name crate.

3 Likes

It seems redundant as when you’re writing a program you already know that utils is your module and serde_json is an external dependency so this extra piece of information isn’t really adding to the comprehension of a module.

I think the whole point of this proposal is to be able to look at foreign code and get exactly this information.

5 Likes

Another goals clarification question:

Allow paths that appear in use to also be used in fn bodies

Do we want the inverse to be true? That is, that you can take path from fn body and place it into use as is?

I am thinking about the following case:

mod foo;

fn f() {
    foo::bar(); // works
}
mod foo;
use foo::bar; // fails, you need self:: :(

fn f() {
    bar();
}

I see, my bad. It's hard to keep it all straight. I already sort of regret using numbered variants, though I did try to give them names. Anyway, I agree that variant 3 achieves many objectives, and feels like a smaller delta on today, if we can resolve the backwards compatibility aspects of it.

It is an open question to me whether

use ::foo::bar;
use ::hir::tcx::map;

would be something I would eventually get used to. I expect so.

I am not sure. I too have wondered this. I think though that if we shoot for an unambiguous syntax (variants 1 or 3) then it can be reached, though only after a "deprecation epoch". i.e., we need some epoch transition where writing use foo::bar is a hard error (given the current meaning), so that then, later on, we can repurpose it.

FWIW, the smallest delta to hit all the goals except the first one is to just deprecate use foo::bar paths, without adding any new syntax :slight_smile:

I don't know, by empirically, this is a nice goal, because it seems to me that people are more likely to add use declarations to get rid of qualified paths, rather than go in the opposite direction.

6 Likes

The other place we use absolute paths (for better or worse) is pub(in foo). So we would also require pub(in ::foo) there. I think though that effectively all of these approaches are deprecating that notation, if not formally, then informally. The syntax is already long.

I've been trying to "just use crate" more and more. I still run into privacy errors -- which we were supposed to be putting an end to -- which makes it hard to evaluate.

Even so, I haven't decided how I feel about it. Definitely I find that it makes me draw broader boundaries than I really want. Often there is a more narrow boundary than what is written when I write crate. On the other hand, writing pub(in xxx) to specify the boundary very precisely feels long and tedious and makes the code kind of hard to read, especially when it's reproduced many times.

Overall, writing crate, combind with ripgrep and some comments, may be the best thing.

2 Likes

I think it’s important to actually implement the fallback and see how it goes and how often it causes resolution to stuck.

To avoid non-deterministic behavior the fallback has to work as a crate-global pass during import resolution, e.g. “1. Iterate import resolution until it saturates” -> “2. Simultaneously fallback all determined unresolved imports to extern crates” -> repeat “1” -> … See https://github.com/rust-lang/rfcs/pull/2126#issuecomment-327776317

(FWIW, after implementing the crate/extern stuff I’m not so vehemently against the RFC 2126 as written, i.e. Variant 2 with fallback, so I’m interested in how it would look in fully implemented backward-compatible form.)

4 Likes

How is variant 3 (::std/::crate) different from variant 2, other than requiring the leading ::? Leading :: already has a meaning in both use/pub(in) and expressions, so wouldn’t it have the same need for fallback? (Although perhaps less frequently because use tends to avoid leading ::…)

Either way, looking at the old thread, one thing we missed here is that variant 2 don’t actually need fallback anyway if we gate the change on an epoch: https://github.com/rust-lang/rfcs/pull/2126#issuecomment-328998289

Instead, in this epoch, we:

  • Introduce crate::
  • Deprecate unused Cargo.toml dependencies
  • Deprecate non-root use of extern crate
  • Introduce Cargo.toml crate aliases
  • Deprecate absolute paths that don’t begin with one of:
    • A potentially-aliased dependency name (which given the other deprecations, must now be a non-conflicting top-level extern crate item, if it is to avoid warnings)
    • crate (which is new, and so can avoid any ambiguities by simply picking the new resolution)
    • super or self (though these aren’t really absolute paths anymore)

(This last point basically deprecates absolute paths starting with top-level items.)

Then, given the “code with no warnings compiles with the same behavior on the next epoch” rule, the next epoch can simply switch to the new resolution rules and deprecate extern crate.

This, again, retains the advantage of less churn than variant 1. Most code will only need to add crate:: to local paths, rather than changing every absolute path.

In this version, absolute paths are not syntactically distinguished, but instead distinguished by where they appear.

In ::std/::crate variant, absolute paths are distinguished syntactically, and special casing of use/pub in paths is removed.

Note that crate::foo paths cannot be used in a fn body – rather you type ::crate::foo

In ::std/::crate variant there is just a single syntax for absolute paths that works everywhere.

3 Likes

That doesn’t resolve the ambiguity within paths that start with ::. Today ::some::path can already refer to a local top-level item from both expressions and use paths. If we suddenly make ::some always refer to a dependency, that is not backwards compatible.

Indeed, Variant 2 as I described it was meant to be gated on the epoch -- and that is how it is implemented (well, it's gated on the feature gate). The fallback is if we want to avoid that gating.

1 Like

This works for you the primary author, but doesn't work for me the contributor. It may not work for you the primary author a year down the line either.

This is interesting. I kind of agree, but at the same time you're already moving files around so the diff isn't going to be tiny. Find/replace isn't too different from the move and copy. If you must, pub(crate) use extern::formerly_module as formerly_module; at your main crate's root.

3 Likes

I was going to say I liked variant 3, but I mis-read it. Why not this, i.e. without the crate:: part for local names?

use ::std::time; 
use ::top_level_name;
use super::parent_item;
use child_mod::item; // or self::child_mod::item

Advantages:

  • this works today
  • the only required change is to deprecate usage of absolute paths without leading ::

Optionally also deprecate usage of relative paths in use and pub(in X), excepting super and self keywords.

This avoids the following problem too:

The only drawback is that it isn't always immediately obvious whether ::foo refers to another crate or an item at the top of this crate (this is tentatively an advantage, allowing an external dependency to be moved to an internal module and customised easily).

3 Likes

Yeah, I originally thought that the only major problem with current module system is that paths in use are just alien, and that just removing this special case would be enough :slight_smile:

By the way, if we remove speacial-casing of use, we could also remove self:: from paths, as it becomes unnecessary .

However, now I find the first goal compelling: it feels right that at the top level there are just a bunch of crates, and the one with name crate is yours.

5 Likes

To me, that’s a valid goal, but not especially important; hence less breakage is better (strictly my opinion of course).

1 Like