language icon indicating copy to clipboard operation
language copied to clipboard

Sealed parts for parts-with-imports

Open lrhn opened this issue 1 year ago • 2 comments

This is not a fully fledged idea. It likely has problems, but maybe it can be an inspiration for a better solution.

With part files being [merged with augmentation libraries][parts-with-imports], and being the unit of code-generation for macros, we may want a way to introduce names in a part file [that will not conflict][part-private-declarations] with names parts of the library. There are also wishes for part files that do not inherit their parent file’s imports.

This is an attempt for a proposal for parts that do not expose privately named declarations to the parent/library, an idea suggested by @leafp. It allows a part file to seal itself off from its parent, and a parent to seal off a part file, both of which makes the privately named declarations of the part file not flow into the rest of the library. If the part file does the sealing, it can also prevent inheriting the parent file’s imports. There is still a way for parent file to explicitly access the private names of its sealed-off part file, but it’s only available between the parent and part files.

Proposal

The grammar of a part-of directive becomes:

<part-file-header> ::= `sealed'? `part' `of` <uri> `;'

That is, a part file can add sealed in front of its part of directive. The effect of a sealed part file is that:

  • The part file does not inherit the imports of its parent file.
  • The part file does not expose the privately-named declarations of its declaration scope to its parent file’s declaration scope.

Further, the part directive is changed as:

<part-directive> ::= `part` <uri> | `sealed' `part' <uri> (`as' <identifier>)? `;'

That is, a parent file can prefix a part directive with sealed. if it does so, it can postfix it with an as id. The effect of a sealed part directive is:

  • The part file does not expose the privately-named declarations of its declaration scope to its parent file’s declaration scope.
  • If there is an as prefix, that introduces a sealed import prefix in the parent file which contains the private names of the part file’s declaration scope.
    • It’s a compile-time error if the name of a sealed import prefix is also used as a prefix by any import or other part directive.
    • Like a deferred import prefix, a sealed import prefix does not combine with inherited prefixes of the same name, and if inherited, it can only be shadowed, not combined with.
    • This prefix can be inherited by any (non-sealed) nested part files.

Resolution and scopes

This changes the declaration scopes of part files. Prior to this change, every part file in a library had the same entries in their declaration scope (which was all top-level declarations in every file of the library). With this change, the library private members may not be library-global, but non-private members are.

The top-level scope of a Dart file is defined (recursively, likely needing a fixed-point computation) as follows:

  • A Dart file has an inherited scope if and only if it is a non-sealed part file.

    • If so, the inherited scope is the combined import scope of its parent file.
  • The combined import scope of a Dart file is its import prefix scope, which has its import scope as parent scope, which again has the files inherited scope as parent scope, if there is an inherited scope.

  • The import scope of a Dart file is the traditional import scope containing the imported declarations from all imports directives of the file that does not have a prefix. It includes an implicit unprefixed import of dart:core if the file has no inherited scope, and no explicit import of dart:core.

  • The import prefix scope is a scope that contains an entry for each name that occurs as a prefix in an import or part declaration of the file. Each entry is bound to a scope rather than a declaration.

    • For an import prefix from one or more import directives, the corresponding scope contains the imported declarations from all import directives with that prefix. If deferred, include the usual modifications to add loadLibrary.
      • If the import prefix is not deferred, and the inherited scope has a non-deferred, non-sealed scope bound to the same import prefix, then that scope is the parent scope of the entry’s scope.
    • For an import prefix from a part directive, the scope contains all privately named declarations in the declaration scope of the corresponding part file.
  • The declaration scope of a Dart file contains:

    • All non-privately named top-level declarations from any file of the library.

    • Any privately named top-level declaration from any file of the library that is accessible to the current file.

      A privately named top-level declaration is available to a file if and only if:

      • It’s declared in the same file,
      • It’s available to an immediate part file of the same file, and neither the part directive nor the part file is sealed, or
      • It’s available to the parent file of the file, if any.

      (This sounds complicated and recursive, but should be doable without too much extra work while finding all the declarations to begin with, especially if the part files is traversed depth-first, since a sealed-part boundary always encapsulates an entire subtree.)

  • It’s a compile-time error if any declaration scope contains more than one (non-augmenting) declaration with the same name.

  • It’s a compile-time error if any file’s import prefix scope contains an entry with the same name as a declaration of the file’s declaration scope.

Alternatives, extensions and problems (aka. “discussion”)

There are other ways something like this can be designed.

  • The part or part of directives could allow show/hide modifiers to expose some, but not all, privately named declarations.
  • Sealing doesn’t have to combine not exposing privates with not inheriting imports. Those could be separate features.
  • This design conflates library privacy with part privacy. We could have another kind of privacy that allows declarations to not be exposed to the parent file. (But library private members is exactly something the library author already has complete control over, so it’s not completely random to use that.)
  • Especially for macros, which are the inspiration for not inheriting imports, it might be too restrictive to not be able to expose some private members. A @BuiltValue class _Foo {} may need to expose a class _FooBuilder {}.
    • If macros could introduce a further nested part file with its own helper values, it could access those only using a prefix, and then be certain that those names won’t conflict with anything else.

lrhn avatar May 15 '24 18:05 lrhn

  • Sealing doesn’t have to combine not exposing privates with not inheriting imports. Those could be separate features.

Macros in particular only want the not inheriting imports part, I see two issues with not exposing private symbols for macros:

  • We can no longer merge generated macro augmentations together (there is a semantic difference now between merging them and not merging them).
  • Some macros may want to expose private symbols to the original library.

jakemac53 avatar May 15 '24 18:05 jakemac53

Yeah, I think the granularity isn't optimal here.

Macros may still want to be able to introduce names that won't conflict with the rest of the library.

Needing to generate a fresh variable name for that means that anyone looking at the generated code will see things like _cache$A78E1, or however the macro framework generates fresh names. (If it can minify after step 1, it might recognize when it doesn't need to mangle the name, but it cannot know before all macros have run.)

This, or part-private declarations, is an attempt to introduce a way to have locally declared, globally non-conflicting names.

lrhn avatar May 15 '24 19:05 lrhn

Moved to the "breaking changes" milestone with the expectation that we probably close as "won't fix" as part of finishing up the augmentations design.

davidmorgan avatar Jun 28 '24 08:06 davidmorgan

Agree. Not part of current design, not planned. It's probably not worth its own complication, unless compelling use-cases show up.

lrhn avatar Jun 28 '24 11:06 lrhn

SGTM, let's close it now then :) thanks.

davidmorgan avatar Jun 28 '24 13:06 davidmorgan