Declaring entities
Table of contents
- Overview
- Matching redeclarations of an entity
externandextern library- Alternatives considered
- References
Overview
Entities may have up to four declarations:
- An optional, owning forward declaration.
- For example,
class MyClass;. - This must come before the definition. The API file is considered to be before the implementation file.
- For example,
- A required, owning definition.
- For example,
class MyClass { ... }. - The definition might be the only declaration.
- For example,
- An optional, owning declaration in a
match_firstblock.- This only applies to
impldeclarations. - This must be in the same file as the first owning declaration.
- This only applies to
- An optional, non-owning
extern library "<owning_library>"declaration.- For example,
extern library "OtherLibrary" class MyClass;. - It must be in a separate library from the definition.
- The owning library’s API file must import the
externdeclaration, and must also contain a declaration. - The owning library’s declarations must have the
externmodifier (withoutlibrary).- For example,
extern class MyClass;.
- For example,
- For example,
For example, a library can have a forward declaration of an entity in the API file, and use the implementation file for the entity’s definition. Putting the definition in an implementation file this way can reduce the dependencies for API file evaluation, improving compile time. This is commonly done with functions. For example:
library "MyLibrary";
fn DoSomething();
impl library "MyLibrary";
fn DoSomething() {
...
}
An impl may appear in at most one match_first block, and only once within it. A declaration in a match_first block may be the forward declaration or the definition. It may also be a fourth declaration that is neither, and which is only allowed to exist within such a block.
Matching redeclarations of an entity
In order to determine whether two redeclarations refer to the same entity, we apply the rules:
- Two named declarations declare the same entity if they have the same scope and the same name. This includes imported declarations.
- When two named declarations declare the same entity, the second is said to be a redeclaration.
- Two owned declarations differ if they don’t syntactically match.
- Otherwise, if one is a non-owned
extern librarydeclaration, declarations differ if they don’t match semantically.
- Otherwise, if one is a non-owned
- The program is invalid if it contains two declarations of the same entity that differ.
class A {
// This function will be redeclared in order to provide a definition.
fn F(n: i32);
}
// ✅ Valid: The declaration matches syntactically.
fn A.F(n: i32) {}
// ❌ Invalid: The parameter name differs.
fn A.F(m: i32) {}
// ❌ Invalid: The parameter type differs syntactically.
fn A.F(n: (i32)) {}
Details
Modifier keywords
As a rule of thumb, modifier keywords are required when, if prior optional declarations were removed, the lack of the modifier keyword would change behavior.
extendinextend implis only on the declaration in the class body (whether that is a forward declaration or definition).- Class and interface modifiers other than
extend(abstract,base,final) exist only on the definition, not on the forward declaration. - Function modifiers (
override,virtual,default,abstract,final) must match between forward declaration and definition (thoughabstractfunctions won’t have definitions). - If any owning declaration has the
externmodifier, all owning declarations must have it. - Access modifiers (
privateandprotected) must match across all declarations and definitions, including between anextern library "<owning_library>"declaration and the owningexterndeclaration.
References:
Syntactic matching and scopes
Two owned declarations syntactically match if the sequence of tokens in the declaration following the introducer keyword and the optional scope, up to the semicolon or open brace, is identical, except for unused modifiers on parameters.
An entity may be redeclaration in a different scope using a qualified declaration:
- Take the portion of the declaration from the introducer up to the end of the scope.
- Replace the introducer keyword with the introducer keyword of the scope.
- Replace the trailing
.with a;. - The result must be a valid declaration of the scope, ignoring restrictions on how often the scope can be redeclared.
Put another way: each portion of the qualified name must not differ from the declaration of the corresponding entity.
For example:
namespace N;
class N.C(T:! type) {
class D(U:! type) {
fn F(a: T, b: U);
}
}
fn N.C(T:! type).D(U:! type).F(a: T, b: U) {}
In this function definition:
F(a: T, b: U)does not differ from the declaration ofF.class N.C(T:! type).D(U:! type);would be a valid redeclaration ofD, because:D(U:! type)does not differ from the declaration ofD.class N.C(T:! type);would be a valid redeclaration ofC, because:C(T:! type)does not differ from the declaration ofC.namespace N;would be a valid redeclaration ofN.
So this is a valid definition of F.
Note that this means that, for example, all members of a class must use the same name for each generic parameter of that class. It cannot be T in one out-of-line member definition and ElementType in another, or the scope in the out-of-line definition would not match.
To redeclare an impl after the end of the class scope it was declared in, that scope may be re-entered as part of the impl redeclaration, in the same way, except with parentheses around the name of the impl, as in impl X.(as Y) { ... }. See “Declaring implementations” in the “Generics: details” design document.
The members of an impl are not required to syntactically match the corresponding members of the interface they are implementing since:
- We don’t want to syntactically couple declarations that could be in different libraries or packages. Such coupling would make refactorings that change the way that code is expressed but not its meaning either difficult or impossible.
- The associated function in an
implis expected to have different syntax than that in the interface in some cases. The two declarations are in different scopes, so will refer to the same types in different ways. And the declaration in theimplis declared with knowledge of theSelftype and associated constants for the interface, which we allow to be used directly in the declaration of the function.
See “impl members vs interface members” in the “Generics: details” design document.
For let and var declarations with a single name binding (let Scope.A: Type = Value;), the end of the declaration is at the = or ; rather than at the } or ;. Note though it is an open question whether this form permits redeclarations. An arbitrary pattern that is not a single binding (let (A: Type1, B: Type2) = Value;) does not permit redeclarations.
Any unqualified names used in syntactic matching will resolve to the same entity in redeclarations due to the poisoning of failed unqualified lookups. See “Unqualified name lookup” in the “Name lookup” design document.
References:
extern and extern library
There are two forms of the extern modifier:
- On an owning declaration,
externlimits access to the definition.- The entity must be directly imported in order to use the definition; otherwise it is incomplete.
- An
extern librarydeclaration is optional. - Like all owning declarations, owning
externdeclarations use syntactic matching for redeclarations.
- On a non-owning declaration,
extern libraryallows references to an entity without depending on the owning library.- The library name indicates where the entity is defined.
- This can be used to improve build performance, such as by splitting out a declaration in order to reduce a library’s dependencies.
extern librarydeclarations only use semantic matching for redeclarations, not syntactic matching.
For example, a use of both might look like:
library "owner";
// This `import` is required due to the `extern library`, but we also make use
// of `MyClassFactory` below. This is a circular use of `MyClass` that we
// couldn't split between libraries without `extern`.
import library "factory";
extern class MyClass {
fn Make() -> MyClass* {
return MyClassFactory();
}
var val: i32 = 0;
}
library "factory";
// Declares `MyClass` so that `MyClassFactory` can return it.
extern library "owner" class MyClass;
fn MyClassFactory(val: i32) -> MyClass*;
impl library "factory";
// Imports the definition of `MyClass`.
import library "owner";
extern fn MyClassFactory(val: i32) -> MyClass* {
var c: MyClass* = new MyClass();
c->val = val;
return c;
}
References:
Valid scopes for extern
The extern modifier is only valid on namespace-scoped entities, including in the file scope. In other words, class C { extern fn F(); } is invalid.
Effect on indirect imports
Indirect imports won’t see the definition of an extern entity. We expect this to primarily affect return types of functions. If an incomplete type is encountered this way, it can be resolved by directly importing the definition. For example:
library "type";
// Because this is `extern`, the definition must be directly imported.
extern class MyType { var x: i32 }
library "make_type";
import library "type";
// Here we have a function which returns the type.
fn MakeMyType() -> MyType*;
library "invalid_use";
import library "make_type";
fn InvalidUse() -> i32 {
// ❌ Invalid: `MyType` is incomplete because it's `extern` and not directly
// imported. `x` cannot be accessed.
return MakeMyType()->x;
}
library "valid_use";
import library "make_type";
// ✅ Valid: By directly importing the definition, we can now access `x`.
import library "type";
fn ValidUse() -> i32 {
return MakeMyType()->x;
}
Indirect imports of non-extern types
Non-extern entities are complete if their definition is imported, even if that import is indirect, as in:
library "a";
class C { fn F(); }
library "b";
import library "a";
fn G() -> C;
library "c";
import library "b";
// Valid: `C` is complete here, even though it's not in name lookup.
G().F();
References:
Using imported declarations
Since extern library "a" class C; must be imported by the owning library, we allow uses of the imported name prior to its declaration within the same file. This means the following works:
library "extern";
extern library "use_extern" class MyType;
library "use_extern";
import library "extern";
// Uses the `extern library` declaration.
fn Foo(val: MyType*);
extern class MyType {
fn Bar[ref self: Self]() { Foo(&self); }
}
References:
Validation for non-owning extern library declarations
We offer some validation that the library in extern library is correct, in the sense of being the single non-owning library declaring that entity and naming the single owning library. When the owning library is incorrect, it’s very likely to be detected in two cases:
- A compile-time error when the owning library imports the non-owning library, when the owning declaration is evaluated.
- A link-time error as a fallback.
Other cases, such as when both libraries are independently imported, may or may not be caught, dependent upon the cost of validation.
References:
Alternatives considered
- Other modifier keyword merging approaches
- No
externkeyword - Looser restrictions on declarations
externnaming- Default
externto private - Opaque types
- Require a library provide its own
externdeclarations - Allow cross-package
externdeclarations - Use a partially or fully semantic rule
- Use package-wide name poisoning
- Allow shadowing in implementation file after use in API file
- Allow multiple non-owning declarations, remove the import requirement, or both
- Total number of allowed declarations (owning and non-owning)
- Don’t require a modifier on the owning declarations
- Only require
externon the first owning declaration - Separate require-direct-import from non-owning declarations
- Other
externsyntaxes - Have types with
externmembers re-export them - Require syntactic matching for
extern librarydeclarations - Use semantic match for the scope
References
- Proposal #3762: Merging forward declarations
- Proposal #3763: Matching redeclarations
- Proposal #3980: Singular
externdeclarations - Proposal #5337: Interface extension and
final implupdate - Proposal #5366: The name of an
implinclassscope - Proposal #7493: Disallow impl in match_first twice