Skip to Content
Cloth

Nested Classes

A class may declare another class inside its body. The inner declaration is a nested class. By default a nested class is a separate type that happens to live in the outer’s namespace; with the inner modifier it becomes a capturing class — every instance carries a reference to the outer instance that produced it.

This page covers both kinds, the visibility rules that apply to them, and how to construct and reference them from other code.

Declaring a nested class

A nested class is declared inside the outer class’s body, with the same class keyword used at the top level:

public class Container () { private Inner child; public Container { this.child = new Inner(); } public class Inner () { public Inner {} public func read(): i32 { return 7; } } }

Two rules differ from top-level declarations:

  • The identifier is required. A top-level class can omit its name and inherit it from the file (public class () { ... } in Container.co declares the class Container). A nested class has no filename to fall back on; the name appears immediately after the class keyword.
  • The primary-parameter parentheses are required. A top-level class may write public class { ... }; a nested class must write public class Inner () { ... } even when the parameter list is empty. The mandatory () keeps the syntactic shape uniform with top-level declarations and signals that primary parameters are part of the type’s surface.

The same shape applies when nesting a struct, enum, interface, or trait inside a class.

Visibility

Nested classes use the same three visibility modifiers as any other declaration: public, internal, private.

ModifierMeaning
publicVisible everywhere. Other modules may import the outer and reference the nested class through it.
internalVisible inside the declaring module. The default if no modifier is written.
privateVisible only to the outer class and other classes nested inside the same outer. Treat the outer’s body as a single privacy scope.

private is genuinely useful for nested types — it expresses this type is an implementation detail of the outer class. Top-level private is rejected by the parser; on a nested type, private is the most restrictive form available.

Non-capturing nested classes

A nested class declared without the inner modifier is independent of any outer instance. Its instances do not carry a hidden reference to an outer object, and they are constructed exactly like any other class:

let c = new Container(); let n = new Container.Inner(); // independent of `c`

A non-capturing nested class is a way to organize related types: the outer’s namespace gives the type a stable home, and the visibility modifier controls who can see it. Beyond that, the nested type is no different from a top-level class.

Inner classes — capturing the outer instance

Prefixing a nested class declaration with inner opts into Java-style outer-instance capture. Every instance of an inner class carries a reference to the outer instance it was constructed against, and the body of the inner class can read fields and call methods on that outer instance directly:

public class Container () { private i32 outerField = 42; private Inner child; public Container { this.child = new Inner(); // captures `this` } public inner class Inner () { private i32 v = 7; public Inner {} public func read(): i32 { return v + outerField; // outerField belongs to Container } } }

Reading outerField from inside Inner.read() is automatic — when an unqualified name doesn’t refer to a local, parameter, or member of Inner itself, Cloth looks for it on the captured outer instance. The same lookup applies to method calls.

inner is a soft keyword. It is recognized in the modifier position before a class keyword, but stays available as an ordinary identifier elsewhere — a local named inner or a field named inner is still legal.

Constructing an inner class

How an inner class is constructed depends on where the construction happens.

From inside the outer class’s instance methods or constructor, write new Inner(args) directly. The compiler supplies the current this as the captured outer:

public Container { this.child = new Inner(); // outer is `this`, supplied automatically }

From outside the outer class’s body, the outer instance is named explicitly using a qualified-new expression:

let c = new Container(); let n = c.new Container.Inner();

The receiver expression to the left of .new is the outer instance. Any expression that evaluates to a Container works; a local, a field, or a method call all qualify.

Constructing an inner class without an outer in scope is a compile-time error (S015 NoOuterContext). The compiler tells you which outer is missing and how to supply it.

The qualified-new form is only legal for inner classes — using it on a non-inner class is rejected.

Accessing the outer through Outer.this

When a name on the inner class shadows a name on the outer, the outer’s member is reached through a Outer.this qualifier:

public class Container () { private i32 v = 100; public inner class Inner () { private i32 v = 7; public func sum(): i32 { return v + Container.this.v; // 7 + 100 } } }

<TypeName>.this resolves to the nearest enclosing instance whose class’s name matches TypeName. Whichever level of nesting TypeName refers to, the expression evaluates to that level’s this.

If no enclosing class in the inner-class chain matches the name, the compiler reports S016 OuterChainNotFound.

Multi-level nesting

Inner classes can themselves declare inner classes. Each level of inner adds a captured-outer link, and the chain of Outer.this qualifiers walks back through them:

public class Tower () { private i32 a = 100; public inner class Mid () { private i32 b = 20; public inner class Leaf () { private i32 c = 3; public func sum(): i32 { return c + Mid.this.b + Tower.this.a; // 123 } } } }

A Leaf instance is constructed against a Mid, which was constructed against a Tower. From a Mid instance m, write m.new Tower.Mid.Leaf() to construct a Leaf against it.

Each <TypeName>.this walks as many levels as needed to reach the named class. The implicit-fallback lookup for unqualified names walks the same chain — so unambiguous outer-field references can be written without any .this qualifier.

Referring to a nested class from outside

The full name of a nested class is Outer.Inner — the outer’s name followed by a dot and the nested name. From outside the outer’s body, this dotted form is accepted directly in any type position:

private Container.Inner child; let n = new Container.Inner(); let m: Container.Mid = ...;

The same shape applies at any depth: Tower.Mid.Leaf. If the outermost class is from another module, an import of the outer is enough — the dotted suffix is resolved against the imported name.

Lifetimes and captured outers

The captured-outer reference held by an inner instance is a borrow, not an ownership transfer. The inner instance does not extend the outer’s lifetime, and the outer is not freed when the inner is destroyed.

The corollary is the constraint: an outer instance must outlive every inner instance that captured it. Calling a method on an inner whose outer has already been destroyed is a use-after-free; the compiler’s existing borrow-tracking treats the captured reference like any other, so the same diagnostics apply.

In practice this means freeing instances in the right order:

let t = new Tower(); let m = t.new Tower.Mid(); let leaf = m.new Tower.Mid.Leaf(); delete leaf; // destroy the deepest first delete m; delete t;

Storing an inner instance in a field of its outer is the common case where this happens automatically — the field is destroyed during the outer’s destructor, before the outer’s storage is released. Hold inner instances elsewhere only when you can guarantee the outer outlives them.

What can be nested

Inside a class body, you can declare any kind of type:

  • class — both capturing (inner class) and non-capturing.
  • struct
  • enum
  • interface
  • trait

Only class honors the inner modifier. The other kinds are always non-capturing.

The reverse — nesting a class inside a struct, interface, or trait — is not currently supported. Outer containers must be classes.