What is a circular type hierarchy and why does EK9 ban it?

← Type Hierarchy Constraints · Ref: Q602

A circular type hierarchy occurs when a class directly or indirectly extends itself, creating an impossible loop. EK9 detects this at compile time and raises E05020.

DIRECT CIRCULAR REFERENCE

The simplest case is a class extending itself:

  A extends A  //ERROR: E05020 - A depends on itself

This is logically impossible because A cannot inherit from something that hasn't been defined yet.

INDIRECT CIRCULAR CHAIN

More subtle cycles involve multiple classes:

  A extends B
  B extends A  //ERROR: E05020 - cycle A -> B -> A

The compiler traverses the full hierarchy chain and detects cycles at any depth.

WHY BANNED

Circular inheritance is logically impossible: if A inherits from B and B inherits from A, neither type can be fully defined. The method resolution order becomes undefined, field initialization creates infinite loops, and the type system becomes unsound.

WHAT TO DO INSTEAD

Use composition over inheritance. If two types need each other's features, extract the shared functionality into a common trait or base class.

See Q603 for multi-level chain detection. See Q604 for circular hierarchy checks on traits, records, and components. See Q610 for refactoring circular dependencies using composition. See Q101 for closed-by-default types.
See Q677 for abstract implementation chain. See Q679 for sealed allow-only. See Q697 for inheritance depth limit.
See Q729 for inheritance depth boundary example.

Example

defines module qa.typehierarchy.circular

  defines class

    //CORRECT: Linear hierarchy with no cycles
    Vehicle as open
      kind()
        <- rtn as String: "vehicle"
      default operator ?

    Car extends Vehicle as open
      override kind()
        <- rtn as String: "car"
      default operator ?

    ElectricCar extends Car
      override kind()
        <- rtn as String: "electric car"
      default operator ?

  defines function

    testLinearHierarchy()
      vehicle <- Vehicle()
      car <- Car()
      electric <- ElectricCar()

      require vehicle?
      require car?
      require electric?

      //Each type in a clean linear chain: ElectricCar -> Car -> Vehicle
      require vehicle.kind() == "vehicle"
      require car.kind() == "car"
      require electric.kind() == "electric car"

Common mistakes

E05030 — If Car extended ElectricCar which extends Car, a circular inheritance cycle would be created. The compiler traverses the full hierarchy chain and detects cycles at any depth. See ek9 -h E05030 for details.

Incorrect:

Car extends ElectricCar as open

Correct:

Car extends Vehicle as open

E05120 — When a child class has a method matching a parent method, the override keyword is required. Omitting it causes method shadowing detection. See ek9 -h E05120 for details.

Incorrect:

kind()

Correct:

override kind()
Other ways to ask this
  • Why can't a class extend itself in EK9?
  • What happens with circular inheritance in EK9?
  • How does EK9 prevent circular class hierarchies?

Coming from another language?

Java: circular inheritance detected at compile time (same error). Python: MRO computation fails for circular hierarchies. C++: compilation error for circular inheritance. Rust: no class inheritance, traits cannot self-reference. Kotlin: compile error for circular inheritance. EK9: E05020 detects cycles at any depth in the hierarchy chain.

Keywords: chain, inheritance, extends, cycle, composition, E05020, hierarchy, type, circular, self-reference