Declaring entities

Table of contents

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.
  • A required, owning definition.
    • For example, class MyClass { ... }.
    • The definition might be the only declaration.
  • An optional, owning declaration in a match_first block.
    • This only applies to impl declarations.
    • This must be in the same file as the first owning declaration.
  • 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 (without library).
      • For example, extern class MyClass;.

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 library declaration, declarations differ if they don’t match semantically.
  • 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.

  • extend in extend impl is 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 (though abstract functions won’t have definitions).
  • If any owning declaration has the extern modifier, all owning declarations must have it.
  • Access modifiers (private and protected) must match across all declarations and definitions, including between an extern library "<owning_library>" declaration and the owning extern declaration.

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 of F.
  • class N.C(T:! type).D(U:! type); would be a valid redeclaration of D, because:
    • D(U:! type) does not differ from the declaration of D.
    • class N.C(T:! type); would be a valid redeclaration of C, because:
      • C(T:! type) does not differ from the declaration of C.
      • namespace N; would be a valid redeclaration of N.

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 impl is 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 the impl is declared with knowledge of the Self type 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, extern limits access to the definition.
    • The entity must be directly imported in order to use the definition; otherwise it is incomplete.
    • An extern library declaration is optional.
    • Like all owning declarations, owning extern declarations use syntactic matching for redeclarations.
  • 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.
    • extern library declarations 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

References