Skip to Content
Cloth
DocumentationReferenceLanguageInterfaces, Traits, and Enums

Interfaces, Traits, and Enums

In addition to classes and structs, Cloth has three further declaration kinds: interfaces for method contracts, traits for attributes attached to declarations, and enums for fixed sets of named singleton values with optional associated data.

Interfaces

An interface declares a method contract. It lists a set of members that any implementing type must provide. Interfaces have no fields, no constructors, no destructors, and no method bodies — only signatures.

[visibility] interface Name { ...member signatures... }

A class or struct declares that it implements one or more interfaces using the implements list:

public class (): Object -> Serializable, Nullable { ... }

Each interface in the list contributes its required members. The class must supply a definition for every required member, with matching name, parameters, and return type.

A single class may implement any number of interfaces. A single interface may be implemented by any number of unrelated classes.

Interface inheritance

An interface may extend one or more parent interfaces using a : clause, mirroring the class-extends syntax. Multiple parents are separated by commas:

public interface Greeter { public func greet(): string; } public interface Polite : Greeter { public func farewell(): string; }

A child interface inherits all of its parents’ required methods. A class implementing the child must satisfy every transitively-required member — methods declared on the child and on every ancestor:

public class () -> Polite { public func greet(): string { return "hello"; } // from Greeter (transitive) public func farewell(): string { return "goodbye"; } // from Polite (direct) }

A class that implements Polite is also assignable to a Greeter slot — the upcast follows the interface chain. The same applies to interface-to-interface assignment: a Polite value can be bound to a Greeter variable.

Multiple inheritance is allowed because interfaces are pure contracts with no data; diamond shapes (the same parent reachable through two children) are deduplicated by (name, parameter types, return type). Cycles in the chain — direct or transitive — are rejected with a compile-time error.

A parent listed in the : clause must itself be an interface. Naming a class or trait there is a compile-time error.

Traits

A trait is an attribute-style annotation applied to a declaration. Traits are not contracts in the way interfaces are; they are tags that the compiler and tools recognize, sometimes attaching behavior, sometimes merely informational.

A trait declaration introduces a new annotation that can then be applied:

[visibility] trait Name { ... }

A trait is applied by writing @TraitName immediately above the declaration it annotates. Three trait annotations are built in:

  • @Override — Marks a method as overriding a member of a base class. The compiler verifies that the member actually overrides something.
  • @Implementation — Marks a member as the implementation of an interface requirement.
  • @Deprecated — Marks a declaration as deprecated. The compiler may emit warnings at use sites.

Example:

@Override public func toString() : string { return "Error: " + getMessage(); }

User-defined traits are declared with the trait keyword and can be applied the same way.

Interfaces vs. traits

The distinction matters: interfaces describe what a type can do (a set of methods that callers may invoke), while traits describe how a declaration should be treated (a tag the compiler reads when processing the declaration).

ConstructAdds members?Adds behavior at use site?Applied with
InterfaceYes (signatures)Yes (callable methods)-> Iface in implements list
TraitNoNo, but may change compilation@TraitName above the declaration

Enums

An enum declares a fixed, compile-time-known set of named singletons. Every variant of the same enum shares one struct shape; user code references variants by name, never constructs them, and uses each variant value as a pointer to the corresponding singleton.

Two declaration forms

Bare list — variants only, no constructor signature:

public enum { RED, GREEN, BLUE }

Each variant carries only its built-in metadata (ordinal + name). Use this form for plain named-value sets where no associated data is needed.

With constructor parameters — each variant binds values to a declared parameter signature:

public enum (f64 average, i32 age, string name) { MARK = (175.5, 25, "Mark"), JAMES = (160.0, 26, "James"), MARY = (144.0, 31, "Mary") }

The constructor signature appears in parentheses immediately after enum. Every variant must supply exactly that many arguments with matching (or losslessly-promotable) types. Each argument expression must be a compile-time constant — literals, unary -, and binary arithmetic over literals are accepted; identifier references, function calls, and other runtime expressions are rejected.

Variants are singletons

Each variant lowers to one constant global. Two references to the same variant compare equal as pointers (MARK == MARK is true); two distinct variants of the same enum compare unequal regardless of their field values. Equality uses == / !=. Variants are immutable — their fields cannot be reassigned.

Built-in methods

Every variant carries two compiler-supplied accessors, regardless of declaration form:

MethodReturnsDescription
getOrdinal()i32Zero-based position in the declaration. The first case is 0.
name()stringThe variant’s identifier as written in source (e.g. "MARK").

For the constructor-parameter form, an additional get<Capitalized>() getter is generated per declared parameter — matching the accessor-block naming convention (private int i = 0 { getter; }getI()):

let mark = People.MARK; mark.getAverage(); // 175.5 mark.getAge(); // 25 mark.getName(); // "Mark" mark.getOrdinal(); // 0 mark.name(); // "MARK"

Static valueOf(string)

Every enum also gets a static lookup-by-name method:

let person = People.valueOf("JAMES") ?? People.MARK; person.getName(); // "James" let none = People.valueOf("NOBODY"); // none is null (People?) let fb = none ?? People.MARK; // null-coalesce to a fallback

valueOf(name) returns EnumType? (nullable). It returns a non-null variant pointer when name exactly matches a declared case identifier and null otherwise. Use ?? to provide a fallback or pattern an if (x == null) check against the result.

Methods declared in the enum body

After the case list (optionally separated by ;), an enum body may declare regular methods. They behave like instance methods on a class: this is the variant pointer, the method has access to every field via the auto-generated getters or by name, and lives at module scope per enum (one body shared across all variants — there are no per-variant overrides).

public enum (f64 average, i32 age, string name) { MARK = (175.5, 25, "Mark"), JAMES = (160.0, 26, "James"), MARY = (144.0, 31, "Mary"); public func describe(): string { return name + " is " + age + " years old"; } }

Static values(): EnumType[]

Every enum also gets a static accessor returning all variants in declaration order:

for (People person in People.values()) { println(person.name()); } let n = People.values()::LENGTH; // 3 let first = People.values()[0]; // MARK singleton

The returned slice is heap-allocated (one allocation per call) and owned by the caller. Like any owned array, release it with delete if you keep it past the current scope; iterating it inline via for-in and discarding is fine — the slice falls out of scope normally. See Arrays for the full ownership rules.

Switching on an enum

switch patterns match enum variants by their case-ref:

switch (person) { case People.MARK: println("hi Mark"); case People.JAMES: println("hi James"); case People.MARY: println("hi Mary"); }

Cases compare the discriminant against each EnumName.CASE pattern in order; the first match runs that case’s body and exits the switch. There is no C-style fall-through — bodies don’t drop into the next case automatically.

A default: arm runs when no case pattern matches:

switch (person) { case People.MARK: println("matched mark"); default: println("everyone else"); }

The analyzer enforces exhaustiveness when the discriminant’s type is an enum and no default: arm is present. Every declared case must appear as a pattern, or the compiler rejects the switch with S02F NonExhaustiveSwitch, listing the missing cases.

Runtime representation

Each variant is a static struct constant with the layout { i32 ordinal, ptr name, <param-types…> }. Built-in fields at offsets 0 and 1 power getOrdinal() / name(); user parameters follow in declaration order. The enum-typed value carried in user code is a pointer to one of these constants — comparisons are pointer-equality, field access is a struct load through the pointer, method calls dispatch directly to the synthesized or user-written body.

The data is in static memory (one constant global per case), shared by every reference to a given variant. Variant access is allocation-free.

Diagnostics

CodeTrigger
S02C EnumCaseArgMismatchCase’s argument count differs from the constructor signature’s parameter count.
S02D EnumCaseNonConstA case argument isn’t a compile-time constant (literal, unary -, or arithmetic on those).
S02E DuplicateEnumCaseTwo cases in the same enum share a name.
S02F NonExhaustiveSwitchA switch over an enum-typed value omits one or more cases and has no default: arm.

Each fires from a global pre-pass so the structural error is reported from the enum’s own file regardless of how the code that uses the enum is ordered alphabetically across the project.

Not yet supported

  • Variants with per-case payload shapes (sum types like Some(T) vs. None). The singleton-pointer layout requires every variant to share the same struct, so the user’s design choice here was deliberate: data attached to enum cases is uniform across all variants. Per-variant shapes would need a separate language construct (e.g. a union or case class keyword), which is on the backlog but not in this release.
  • Ordinal-based comparison operators (<, <=, >, >=) — currently only == and != are wired. Comparing ordinals manually via getOrdinal() works in the meantime.
  • Variant-specific method overrides (Java’s anonymous-class-per-variant). Methods on an enum live once at the type level; per-variant behavior should branch inside the method or be expressed via a separate type.