Declaring entities
Table of contents
- Overview
- Matching redeclarations of an entity
extern
andextern library
- Alternatives considered
- References
Overview
Entities may have up to three 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, 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
extern
declaration, and must also contain a declaration. - The owning library’s declarations must have the
extern
modifier (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() {
...
}
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 library
declaration, 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
TODO: Figure out what details to pull from #3762 and #3763.
extern
and extern library
There are two forms of the extern
modifier:
- On an owning declaration,
extern
limits access to the definition.- The entity must be directly imported in order to use of the definition.
- An
extern library
declaration is optional.
- On a non-owning declaration,
extern library
allows 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.
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;
}
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;
}
Alternatives considered
- Other modifier keyword merging approaches
- No
extern
keyword - Looser restrictions on declarations
extern
naming- Default
extern
to private - Opaque types
- Require a library provide its own
extern
declarations - Allow cross-package
extern
declarations - 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
extern
on the first owning declaration - Separate require-direct-import from non-owning declarations
- Other
extern
syntaxes - Have types with
extern
members re-export them - Require syntactic matching for
extern library
declarations
References
- Proposal #3762: Merging forward declarations
- Proposal #3763: Matching redeclarations
- Proposal #3980: Singular
extern
declarations