Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolution rules for nested parts with imports #3726

Open
lrhn opened this issue Apr 25, 2024 · 3 comments
Open

Resolution rules for nested parts with imports #3726

lrhn opened this issue Apr 25, 2024 · 3 comments
Labels
augmentation-libraries feature Proposed language feature that solves one or more problems

Comments

@lrhn
Copy link
Member

lrhn commented Apr 25, 2024

This is an attempt to specify how nested part files with imports, and parent file import inheritance, should work.
This assumes that part files can contain import, export and part directives.

That could be the result of unifying part files and library augmentation files.

Recursive part files with imports

Goals:

  • Part files with imports (and exports, and parts) should be an extension of the current syntax and semantics.
  • A part file which has no imports (existing code) keeps getting all its parent file's imports, and import prefixes.
  • A part file which imports all its own dependencies is able to completely ignore imports from its parent file.
  • A part file which has some imports is able to use those without risk of conflict, and still use any other imports from the parent file.

The third item allows macros to create self-contained code that doesn't depend on inheriting imports (as long as the macro author cooperates, if they write raw strings into the generated code, anything can happen).

Assumptions:

  • It's one library. The author is in charge of every choice of name, and of avoiding name conflicts.
  • Part files are not abstractions and not encapsulation. You can break things by adding declarations in other parts of the library, because the declaration scope is library-wide. See first item.

Import scopes

A library introduces a declaration scope and an export scope, as usual.

A file with imports introduces two scopes:

  • The (top level) import scope
  • The declared import prefix scope

A file without imports can be said to introduce those scopes as well, they're just empty. Per the goals, that should give the same effect.

Each import without a prefix introduces names and associated declarations into the file's import scope. As usual, those names can be conflicted if multiple imports introduce the same name.

Each prefix which there is an import for in the file, introduces a name into the declared prefix scope, bound to a scope/namespace for that prefix. (One entry and scope per different name, even if multiple imports use the same prefix name.)

Each import with a prefix introduces names and associated declarations in the the corresponding prefix-named scope. Again conflicted names are remembered as such.

Declarations in a file introduce declarations into the declaration scope of the library. Augmentations introduce augmentations to a declaration. (The declaration scope maps names to one declaration and a sequence of zero or more augmentations. The order of augmentation application isn't relevant here.)

With all this, we can define the scope conflict errors and lexical scopes for each file:

  • It's a compile-time error if any file introduces an import prefix with the same base name as declaration in the declaration scope of the containing library.
  • The lexical scope of the declarations of a file is the following scope chain (from bottom to top):
    • The declaration scope of the library.
    • The combined import scope of the file. This means that the declaration scope of the library does not have a single unique parent scope, it has a different parent scope in each file. We can spin this as copying the declaration scope, or as defining scope chains separately from the scopes, instead of a scope having a single unique parent scope.
  • The combined import scope of the file is:
    • The transitive import prefix scope of the file, defined below.
    • The import scope of the library. (Its own imports.)
    • The combined import scope of its parent file. (Inherited imports and prefixes.)
  • The transitive import prefix scope of a file is the scope defined as follows:
    • For each prefix name in the declared import prefix scope of this file:
      • Iterate parent files until either:
      • Running out of parent files, in which case
        • use the declared import prefix scope of this library as binding for the prefix name.
      • or finding a parent file whose declared import prefix scope contains the same name, in which case
        • map the prefix name to the scope chain extending the that parent file's transitive import prefix scope binding for the name with the declared import prefix scope of this file. (So extend any parent file's import scope of the same name.)

That is:

  • The combined import scope of a file are all the imports that are available to that file, including both normal imports and prefix names, and both inherited and declared ones.
  • Prefix names are not in the declaration scope. But …
  • Still cannot declare a library member with the same name as an import prefix. Since declaration scope is library-wide, and takes precedence over import scopes, the prefix would be inaccessible. This prevents the same prefix names everywhere. (Which means a macro generating a public name can conflict with an import prefix with the same name. But it can also conflict with any existing real declaration, so just don't use public names that macros will generate. Those names should not come as any surprise.)
  • Part files inherit the combined import scope of their parent, but can override/shadow as:
    • Directly imported names shadow anything from the parent file's imports.
    • Prefix names hides imports with the same name. Declaring a prefix in a file means direct imports, inherited or in the same file, with that name are ignored and inaccessible.
    • Prefix named scopes combine with the same prefix name in the nearest (transitive) parent file which declares one, if any.
      • Even if the prefix name is not otherwise accessible because a normal import has shadowed it.
      • If the author doesn't want to combine with a parent file's import prefix, don't use the same name. Current file's imports take precedence, so only using what you have yourself imported means you can still ignore the parent scope.
  • There can be multiple distinct prefix import scopes with the same name, either completely separate and declared in separate subtrees, or extending each other down the same part path.

Exports

An export in a part file is added to the export scope of the library, just as if it had occurred in the main library file. Any conflict is a compile-time error.

@lrhn lrhn added feature Proposed language feature that solves one or more problems augmentation-libraries labels Apr 25, 2024
@natebosch
Copy link
Member

Can you elaborate on how

  • A part file which imports all its own dependencies is able to completely ignore imports from its parent file.

Is solved by your proposal? Does it required the part file to use import prefixes in some particular way to be safe from the parent library imports causing problems?

I think we should have a mechanism where a part file can have a clean slate and not inherit any imports or import prefixes. (I think that should be the default, but it doesn't need to be the default for the mechanism to be worthwhile.)

Can we use some syntax in the part of to choose whether imports are inherited? As a bad proposal to build from, how about part of 'foo.dart' hide default;

@lrhn
Copy link
Member Author

lrhn commented May 16, 2024

The full specification is in this PR: #3800

The part file imports always take precedence over inherited imports from the parent file. So do any name declared by the library itself, as always.
If a part file imports a declaration itself, or introduces a prefix import, that's what it get for that name. It doesn't need import prefixes, it just needs to import the name it wants to use.
The only way an import from a parent file can get used, is if the part file doesn't introduce that name in the lexical scope at all. (That does mean that a part file cannot completely ignore the parent file's imports, an unqualified foo that matches a parent file import will resolve to that, not default to this.foo, which it would if the parent file imports were not there.)

Having the part file inherit the parent file's imports is necessary for backwards compatibility.
We need to do something different from part "part.dart"; and part of "parent.dart"; to have different behavior than today.

Adding a way to have a part file not include the parent file's imports is definitely possible.
It's only really interesting for macros and their generated part files. Any other use, I think the library author can figure it out.
For macros, I'm more worried about introducing a top-level name conflict, even for a private name, than about inheriting imports.

Still, something like sealed part of "parent.dart";, part of "parent.dart" show ... hide ...; or part of "parent.dart" hide; are all possible. I just want to make sure which problems it's intended to solve.

@jakemac53
Copy link
Contributor

I do think that from a technical perspective, macros wouldn't actually need the ability to hide the parent imports, if they are also allowed their own prefixed imports.

This would make some of the more string-based apis work (for example, @SomeAnnotation("x + y") could work, assuming x and y are in scope wherever that string is output, and refer to the "correct" x and y). I am not sure that is a desirable outcome, but we don't have a good alternate proposal yet, and it would sort of solve some issues, just not in a very elegant or safe way.

But, I don't believe this is the behavior most people actually want for parts generally. So, I would not make it the default if it was only up to me. I would only make it an option, just to be less breaking for existing codegen. Which, isn't a super compelling case for the option either.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
augmentation-libraries feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests

3 participants