language
language copied to clipboard
Simple parts with imports
I'm worried about the complexity of the scoping structure with the current proposal for enhanced parts (aka parts with imports).
Currently specified scoping structure
For example, assume the following Dart files (based on an example from @lrhn):
// --- Library 'main.dart'.
import 'l1.dart' show v1, v2, v3;
import 'q1.dart' as q show q1;
import 'p1a.dart' as p show p1, p2;
part 'part.dart';
void foo() {}
// --- Part file 'part.dart'.
part of 'main.dart';
import 'l2.dart' show v1;
import 'p1b.dart' as p show p1, p3;
import 'p1c.dart' as p show p4;
void bar() {}
const v3 = "a";
The imported libraries are not shown, but every name in their exported namespaces are shown using show
clauses, so this is enough to know which names imported where.
graph BT;
MainLibrary["'main.dart' top-level scope<br>foo, bar, v3"]
MainPrefix["'main.dart' prefix scope<br>p, q"]
MainImport["'main.dart' import scope<br>v1, v2, v3"]
MainLibrary --> MainPrefix
MainPrefix --> MainImport
PartLibrary["'part.dart' top-level scope<br>foo, bar, v3"]
PartPrefix["'part.dart' prefix scope<br>p"]
PartImport["'part.dart' import scope<br>v1"]
PartLibrary --> PartPrefix
PartPrefix --> PartImport
PartImport --> MainPrefix
MainPrefixQ["'main.dart' prefix scope 'q'<br>q1"]
MainPrefix -->|q| MainPrefixQ
MainPrefixP["'main.dart' prefix scope 'p'<br>p1, p2"]
MainPrefix -->|p| MainPrefixP
PartPrefixP["'part.dart' prefix scope 'p'<br>p1, p3, p4"]
PartPrefix -->|p| PartPrefixP
PartPrefixP --> MainPrefixP
style MainLibrary fill:lightgreen;
style PartLibrary fill:lightgreen;
style MainPrefixP fill:yellow;
style MainPrefixQ fill:yellow;
style PartPrefixP fill:yellow;
style MainImport fill:lightblue;
style PartImport fill:lightblue;
First, each file (library or part) gets all declarations in any of the files in the part tree that constitutes the library (here: just main and part). So the two files have the following declarations from the library itself: foo
, bar
, v3
.
Moreover, main has access to p
(going to the prefix scope) and it provides the names p.p1
and p.p2
in the "prefix scope p". Similarly for q.q1
. Finally, v1
, v2
, and v3
are available because they are imported into main.
For part, we have a prefix scope p as well, but in this case it contains p.p1
(imported from p1b), p.p2
("inherited" from the prefix scope p of main), p.p3
(p1b), and p.p4
(p1c).
A simpler version
I'd much prefer to use a simpler structure. I do not think it's reasonable to assume that developers will always and immediately have a full understanding of the graph structure of a given set of files that constitute a library, and even less the scoping structure which is considerably more complex than the file structure.
Perhaps we should aim to ensure that we follow the approach used with the top-level scope more consistently? The point is that "the same name has the same meaning everywhere in the library".
Then, we do not introduce the notion of a prefix scope, we maintain that prefixes are simply entries in the library scope. This means that it is an error to have a prefix whose name is the same as a top-level declaration like a function, but that's OK today so why wouldn't it be OK also with parts-with-imports?
This means that a library prefix will be in scope from all files of the library, no matter which imports in which files are using that name as its prefix. Also, the library prefixes will be populated identically everywhere.
Each prefix namespace would be populated by collecting the imported bindings from each of the import directives with that prefix that exist in the complete file tree. Any name clashes give rise to an ERROR entry in the name space (that is, there is no shadowing, we use the same approach to name clashes as we have always done in the pre-feature language).
For any given prefix p
, if at least one import with prefix p
is deferred, and two or more imports with that prefix exist in the library (any file) then an error occurs. Otherwise the deferred import has the usual pre-feature semantics (e.g., with loadLibrary
).
The resulting scoping structure would be as follows:
graph BT;
MainLibrary["'main.dart' top-level scope<br>foo, bar, v3, p, q"]
Import["import scope<br>v1, v2, v3"]
MainLibrary --> Import
PartLibrary["'part.dart' top-level scope<br>foo, bar, v3, p, q"]
PartLibrary --> Import
PrefixQ["prefix scope 'q'<br>q1"]
MainLibrary -->|q| PrefixQ
PrefixP["prefix scope 'p'<br>p1, p2"]
MainLibrary -->|p| PrefixP
PrefixP["prefix scope 'p'<br>p1, p3, p4"]
PartLibrary -->|p| PrefixP
PartLibrary -->|q| PrefixQ
style MainLibrary fill:lightgreen;
style PartLibrary fill:lightgreen;
style PrefixP fill:yellow;
style PrefixQ fill:yellow;
style Import fill:lightblue;
We could emit a warning whenever an imported name is used in a Dart file, and no import exists in the parent chain that provides the binding for this name. This would be applicable to both plain and prefixed imported names.
In other words, we would technically have a flat namespace of imported names for the entire library, prefixed or not, but we would use the warnings to ensure that the imports that we actually rely on are "visible".
If a part file, say, a macro generated part, should be protected against imports in user-written code then it should generate a part that imports every imported name which is used in the generated code. Each import would have a prefix which is a name that is fresh for the library as a whole, and each reference to an imported name would be prefixed with that fresh name. Should be possible. ;-)
I know this is very different from the approaches that we've considered for quite a while, but I do think there is a need to simplify the specified structures as of today.
@dart-lang/language-team, WDYT?