I strongly disagree that any part of our language design should be motivated by ease of “running an entire program as a single file in a web interface”. But your comments are mostly good nonetheless.
In particular, within a specific program I agree that we should think of mod statements in terms of “flattening”, and regardless of location on disk, we should wind up with a canonical tree of modules equivalent to one written in a single file using explicit mod {} blocks.
We should use relative paths to some “project root” which probably we will need to define as the location of some manifest file, similar to Rust and Cargo.toml. Then we can copy Rust in saying that mod mymod; imports the contents of mymod.rs or mymod/mod.rs or mymod/mymod.rs (except that let’s pick one and stick with it please) (and drop Rust’s stupid -/_ conversion rules). We can whitelist our character sets, and between that and relative paths, there will be no cross-system incompatibilites.
It’s easy to define an object’s canonical path by referring to the tree of mods. No re-exports or other shenanigans will affect these canonical paths.
Now, when we introduce external libraries, we need to go beyond the module system and define something analogous to Rust’s “crates”. Here is where we start to get into trouble, and here’s where I think your concern is. What Rust does is very ad-hoc and really not appropriate for a language where we care deeply about supply-chain security. Essentially:
- rustc itself has very little notion of “crates”; it only knows about rlibs and never treats two distinct rlibs as being the “same” for purpose of type compatibility
- cargo’s job, among other things, is to compile all the crates into rlibs and to decide which ones are “the same”, which it does using complicated semver-compatibility rules and even more complicated rules surrounding paths, registries, git urls,
[patch]overrides, etc. But once it does that, then rustc’s job becomes easy. - But the Rust standard library is a special case; this is bundled with rustc, is considered unique by fiat, and you cannot have multiple versions of the standard library in play (and since 1.0 they have made huge efforts, mostly successfully, to ensure that no breaking change ever occurs in stdlib so this can work by everybody just declaring their “minimum supported rust version” and requiring the maximum of all MSRVs in a dependency tree).
The problem of crates, I think, is separate and deserves a separate issue. But I think the broad paradigm of “the compiler just sees a pile of named rlibs, with all names distinct, and each one is a tree of mods with types identified by their path within the rlib, and no two rlibs’ types are equivalent” is the right one.
So we can push our problem out one level to “how do we produce the pile of named rlibs”?
What do we actually want to get out of nominal identity here, in the end?
Without nominal typing we might as well have no type system at all. Without nominal typing users have almost no way to specify or verify the invariants they require on their objects and they will be flying blind.