Declaring entities

Table of contents

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.
  • A required, owning definition.
    • For example, class MyClass { ... }.
    • The definition might be the only 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() {
  ...
}

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

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

References