Tuesday, July 17, 2012

Draft spec changes to library and import

Posted by Gilad Bracha


Library Reform

This document provides a draft specification for changes to library & import changes.

As usual, specification changes are highlighted in yellow.


12. Libraries and Scripts


A library consists of (a possibly empty) set of imports, and a set of top level declarations. A top level declaration is either a class, a type declaration, a function or a variable declaration.

topLevelDefinition:

classDefinition

| functionTypeAlias

| functionSignature functionBody

| returnType? getOrSet identifier formalParameterList functionBody

| (final | const) type? staticFinalDeclarationList ';'
   | variableDeclaration ';'
   ;

getOrSet:
 get
| set
;

libraryDefinition:

libraryName? import* partDirective* resource* topLevelDefinition*
   ;

Issues: Library keyword is optional (not yet covered above), and yet library is not a reserved word or even a built-in identifier. There were loud complaints about making library a keyword of any kind. Need to revisit why that was such an issue. I think we can get away by saying that if the first identifier after any initial metadata in a file is library, we treat it as a library declaration, otherwise, it’s just an identifier.


libraryName:
 metadata library qualified “;”
 ;

A library may optionally begin with a script tag,
which can be used to identify the interpreter of the script to whatever computing environment the script is embedded in. The script tag must appear before any whitespace or comments.  A script  tag begins with the characters #! and ends at the end of the line. Any characters after #! are ignored by the Dart implementation.

Libraries may be named or anonymous. A named library begins with the  word library, followed by a qualified identifier that gives the name of the library.

The name of a library is used to tie it to separately compiled parts of the library (called parts) and can be used for printing and, more generally, reflection. The name may be relevant for further language evolution (such as first class libraries) as well. In the future it may also serve to define a default prefix when importing.  

Names of libraries intended for widespread use should follow the well known reverse internet domain name convention.

Libraries are units of privacy. A private declaration declared within a library L can only be accessed by code within L. Any attempt to access a private member declaration from outside L will cause a run-time error. Since top level privates are not imported, using them is a compile time error and not an issue here.

The public namespace of library L is the mapping that maps the simple name of each public top level member m of L to m.

The scope of a library L consists of the names introduced of all top level declarations declared in L, and the names added by L's imports.

Libraries may include extralinguistic resources (e.g., audio, video or graphics files)

resource:
 metadata resource “(” stringLiteral “)” “;”
;

It is a compile-time error if the string literal x given in a resource directive is not a compile-time constant, or if x involves string interpolation.

Exports


A library L exports a namespace, meaning that the declarations in the namespace are made available to other libraries if they choose to import L.  The namespace that L exports is known as its exported namespace.

We say that a name is exported by a library (or equivalently, that a library exports a name) if the name is in the library’s exported namespace. We say that a declaration is exported by a library (or equivalently, that a library exports a declaration) if the declaration is in the library’s exported namespace.

A library always exports all names and all declarations in its public namespace. In addition, a library may choose to re-export any of its imported libraries.



Imports


An import directive (often abbreviated as import) specifies how a namespace derived from one
library is to be used in the scope of another library.

import:
  metadata import  stringLiteral (as identifier)? combinator* (“&” export)?“;”
;




combinator:
 show identifierList
 | hide identifierList
;


identifierList:
  identifier (, identifier)*
;


Every library L has an import namespace I that maps names to declarations given in other libraries.  Any name N defined by I is in scope in L, unless either:
  • a declaration with the name N exists in L, OR
  • L contains an import of the form importas N.

This allows members to be added to libraries without breaking their importers.

An import provides a string literal x that specifies a URI where the declaration of an imported library is to be found. It is a compile-time error if x is not a compile-time constant, or if x involves string interpolation. It is a compile-time error if the compilation unit found at the specified URI is not a library declaration.

The current library is the library currently being compiled. The import modifies the import namespace of the current library in a manner that is determined by the imported library and by the optional clauses provided in the import.

Imports assume a global namespace of libraries (at least per isolate). They also assume the library is in control, rather than the other way around.

An import directive I may optionally include:

  • A prefix clause of the form as Id used to prefix names imported by I.
  • Namespace combinator clauses used to restrict the set of names imported by I. Currently, two namespace combinators are supported: hide and show.
  • An export clause of the form & export used to re-export the namespace being imported by I.


It is a compile-time error if a name N is referenced or re-exported by a library A and N  is  introduced into the import namespace of A by more than one import.

It is not an error if a N is introduced by two or more imports but never referred to and not re-exported.

The policy above makes libraries more robust in the face of additions made to their imports.  

A clear distinction needs to be made between this approach, and seemingly similar policies with respect to classes or interfaces.  The use of a class or interface, and of its members, is separate from its declaration. The usage and declaration may occur in widely separated places in the code, and may in fact be authored by different people or organizations.  It is important that errors are given at the offending declaration so that the party that receives the error can respond to it a meaningful way.

In contrast a library comprises both imports and their usage; the library is under the control of a single party and so any problem stemming from the import can be resolved even if it is reported at the use site.

On a related note, the provenance of the conflicting elements is not considered. An  element that is imported via distinct paths may conflict with itself. This avoids variants of the well known "diamond" problem.


Let I be an import directive that refers to a URI via the string s1. Evaluation of I  proceeds as follows:

First,

  • If the contents of the URI that is the value of s1 have not yet been compiled in the current isolate then they are compiled to yield a library B. Because libraries may have mutually recursive imports, care must be taken to avoid an infinite regress.
  • Otherwise, the contents of the URI denoted by s1 have been compiled into a library B within the current isolate.


Let NS0 = ExportedB,  where ExportedB id the exported namespace of B. Then, for each combinator clause Ci, 1 <= i <= n,  in I:
Then, let ImportNamespace be the the mapping of names to declarations defined by cn(...(c1(ExportedB, a1), a2) …), an) where ExportedB is the exported namespace of B, and each  ci, 1 <= i <= n,  is one of the following namespace combinators:
  • If Ci is of the form  show id1, …, idk , then let NSi = show([id1, …, idk], NSi-1) where show(l,n) takes a list of identifiers strings l and a namespace n, and produces a namespace that  maps each name string in l to the same element that n does, and is undefined otherwise.
  • If Ci is of the form  hide id1, …, idk , then let NSi = show([id1, …, idk], NSi-1) where hide(l, n) takes a list of identifiers  strings l and a namespace n, and produces a namespace that is identical to n except that it is undefined for each name string k in l.  



Let ImportNamespace =  NSn . If an import I includes an export clause, then for each entry mapping key k to declaration d  in ImportNamespace  an entry mapping k to d is added to the exported namespace of L unless a  top-level declaration with the name k exists in L.  We say that L re-exports library B, and also that L re-exports namespace ImportNamespace. When no confusion can arise, we may simply state that L re-exports B, or that L re-exports ImportNamespace. It is a compile-time error if a name in ImportNamespace has already been added to the exported namespace of L by another import directive.


The prefix combinator, prefix(s, n), takes a string s and a namespace n. If s is the empty string,  the result is n. Otherwise, the result is a namespace that has, for each entry mapping key k to declaration d in n,  an entry mapping s.k to d.

Next, if I includes a prefix clause of the form as p, let NS =  prefix(p, ImportNamespace) where prefix(id, n), takes an identifier id and produces a namespace that has, for each entry mapping key k to declaration d in n,  an entry mapping id.k to d. Otherwise, let NS = ImportNamespace.
It is a compile-time error if NS includes the key p.

Then, for each entry mapping key k to declaration d in NS prefix(s, ImportNamespace), d is made available in the top level scope of L unless either:

  • a declaration with the name k exists in L, OR
  • a prefix clause of the form  as k is used in L.

The allows members to be added to libraries without breaking their importers.

We say that the namespace NS prefix(s, ImportNamespace) has been imported into L.
An import directive that has no export argument specified is assumed to have the argument export: false.
An import directive that has no prefix: argument is assumed to have the argument prefix: “”.

It is a compile-time error if b is true and a name in ImportNamespace has already been added to the exported namespace of L by another import directive.

It is compile-time error to import two different libraries with the same name.

A widely disseminated library should be given a name using the common inverted
internet domain name convention, as in other popular languages.

It is a compile-time error to import into a library two or more namespaces that define the same name.  It is a compile-time error if any of the optional arguments ai , 1 <= i <= n+1,  is not a compile-time constant. It is a compile-time error if an actual argument to the prefix combinator is not a constant string that  denotes either a valid identifier or the empty string. It is compile time error if an  actual argument to the prefix combinator denotes a name that is declared by the importing library. It is a compile-time error if any of the elements of the first argument of a use of a show or hide combinator does not denote a valid identifier

Note that no errors or warnings are given if one hides or shows a name that is not in a namespace.  This prevents situations where removing a name from a library would cause breakage of a client library.




Parts


A library may be divided into parts, each of which can be stored in separate location. A library identifies its parts by listing them via a part directives.


Includes


An include part directive specifies a URI where a Dart compilation unit that should be incorporated into the current library may be found.

partDirective:
 metadata part  stringLiteral “;”
  ;

partHeader:

metadata part of qualified
   ;


partDeclaration:

partHeader  topLevelDefinition* EOF
   ;


A compilation unit part header begins with  part of  followed by the name of the library the part belongs to.  A part declaration consists of a part header followed by is a sequence of top level declarations.

The part header serves as a “backpointer” from the part back to its library. This makes it clear to readers (be they human or mechanical) what scope is in force in the part.

Compiling an include part directive of the form part s; causes the Dart system to attempt to compile the contents of the URI that is the value of s. The top level declarations at that URI are then compiled by the Dart compiler in the scope of the current library. It is a compile time error if the contents of the URI are not a valid part declaration compilation unit. It is a static warning if the referenced part declaration p names a library other than the current library as the library to which p belongs.

It is a compile-time error if s is not a compile-time constant, or if s involves string interpolation.

Scripts


A script is a library with a top level function main().

scriptDefinition:
 scriptTag? libraryDefinition
;

scriptTag:
 “#!” (~NEWLINE)* NEWLINE
;


A script may optionally begin with a script tag which can be used to identify the interpreter of the script to whatever computing environment the script is embedded in. The script tag must appear before any whitespace or comments.  A script  tag begins with the characters #! and ends at the end of the line. Any characters after #! are ignored by the Dart implementation.

A script S may be executed as follows:

First, S is compiled as a library as specified above. Then, the top level function main() that is in scope in S is invoked with no arguments. It is a run time error if S does not declare or import a top level function main().

The names of scripts are optional, in the interests of interactive, informal use. However, any script of long term value should be given a name as a matter of good practice.