Cyclic imports

Two modules may import each other, because nothing runs when a module is imported and its exports are computed to a fixpoint, but the same cycle between top-level statements is an error.

Importing is not running. Two files that name each other's types are not a mistake, because a module contributes nothing but declarations, and a declaration does not care in which order its neighbors were read.

Example#

first.trb:

use Second from "./second"

public type First {
  next: Second
}

second.trb:

use First from "./first"

public type Second {
  previous: First?
}

Syntax#

// first.trb
use Second from "./second"
// second.trb
use First from "./first"

Rules#

  1. Imports between modules may be cyclic, exactly as imports between packages may. std/core names std/text because Show.show answers a String, and std/text names std/core because a String is Compare - cutting that apart would need one package, or a String that is not a type of the standard library.

  2. A cycle is harmless because nothing runs when a module is imported. An imported module consists of declarations only (see Top-level code), so there is no initialization order for a cycle to break, and the exports on both sides are computed to a fixpoint.

  3. A cycle between workspace members is still an error. Workspaces build their members in the order their dependencies require, and a member cannot depend on itself through another member - that ordering question only exists between packages, not between the files inside one.

  4. A cycle of top-level statements is not the same question, and is still rejected. Only a file nothing imports may hold top-level statements at all; two such files cannot import each other in the first place, because each one would then be imported.

What this is not#

A cyclic import is not a design smell to break up. Two types that reference each other are ordinary in a program with algebraic data types, and splitting the cycle across more files does not remove it, it only moves where the use lines point.

use Second from "./second"

public type First {
  next: Second
}

A cycle of top-level statements is a different question, and stays an error. The rule is not about the two files naming each other; it is that a file which is imported may not hold a top-level statement at all, cyclic or not - see Top-level code for the exact diagnostic.

use Second from "./second"

print "loading first"
  • Top-level code - why an imported module has no initialization order to protect.
  • use - the import statement that can form the cycle.
  • Workspaces - the one place a cycle is still rejected: between member packages.
Esc