docs / specification / Specification
Chapters — 8. Classes
- 1. Introduction
- 2. Grammar & Lexical Structure
- 3. Types, Values & Variables
- 4. Allocation & Storage
- 5. Ownership & the Borrow Checker
- 6. Conversions & Contexts
- 7. Names, Scopes & Packages
- 8. Classes
- 9. Interfaces
- 10. Annotations & Aspects
- 11. Templates & Wildcards
- 12. Arrays, Views, Slices & Records
- 13. Statements & Patterns
- 14. Expressions
- 15. Errors & Stack Traces
- 16. Concurrency
- 17. Accelerated Compute (XPU)
- 18. Script Units
- 19. Notebook Kernel & Jupyter Compatibility
- 20. Execution & Program Lifecycle
- 21. Complete Grammar
8 — Classes
This chapter defines class declarations and their members — fields, methods, constructors, destructors, and operator declarations — together with Cajeta’s inheritance model: a class may extend multiple classes, each contributing methods and state, with virtual dispatch across all of them.
8.1 Class Declarations
classDeclaration
: CLASS identifier typeParameters?
(EXTENDS typeList)?
(IMPLEMENTS typeList)?
(PERMITS typeList)?
classBody
;
Modifiers (public, final, abstract, …) precede the declaration. A class declares an optional type-parameter list (Templates §11), an optional list of extended classes (§8.4), an optional list of implemented interfaces (Interfaces §9), and a body of members.
Discussion.
finalon a class declares it closed to extension. As of 0.27.0 extending afinalclass is not yet diagnosed. The restriction is bound here and enforcement follows.sealed/permits/non-sealedare in the grammar, and their semantics are not yet specified here.
8.2 Members
A class body declares fields, methods, constructors, at most one destructor (§8.8), and operator declarations (§8.7). Members are in scope throughout the class body regardless of order. Instance members are reached through a receiver (this.field, obj.method()). Static members belong to the class and are reached through the class name.
Methods may be overloaded: two methods of the same name with different parameter types are distinct. Transfer mode is not part of a signature — declaring overloads that differ only in # is a compile-time error (Ownership §5.5.1).
Example 8.2-1. Static state and methods.
public final class Counter {
public static int32 count;
public static int32 bump() { Counter.count = Counter.count + 1; return Counter.count; }
}
Counter.bump();
System.stdout.println(Counter.bump()); // 2
8.3 Constructors
A constructor is declared with the class’s own name and no return type. Construction runs the parents’ constructors first — implicitly, one per extended class, in the declared order of the extends list — then the class’s own constructor body. An explicit super(args) invocation reaches the first declared parent.
Example 8.3-1. Implicit parent construction in declared order.
public class A { public A() { System.stdout.println("A ctor"); } }
public class B { public B() { System.stdout.println("B ctor"); } }
public class Both extends A, B { public Both() { System.stdout.println("Both ctor"); } }
Both b = heap Both(); // prints: A ctor, B ctor, Both ctor
Discussion. As of 0.27.0 a parent that lacks a no-argument constructor is silently skipped by implicit construction rather than diagnosed, and a diagnostic is intended. Reaching a non-first parent’s constructor explicitly is not yet specified.
8.4 Inheritance
class C extends A, B inherits the members of every listed parent: methods and fields. Each parent contributes a sub-object to the instance layout, so parents with state compose without interference.
Example 8.4-1. Two parents, each contributing state and behavior.
public class Timestamped { public int64 created; public int64 age(int64 now) { return now - this.created; } }
public class Labeled { public String label; public String describe() { return "<" + this.label + ">"; } }
public class Event extends Timestamped, Labeled {
public Event() { this.created = 100; this.label = "ev"; }
}
Event e = heap Event();
System.stdout.println(e.age(150) + " " + e.describe()); // 50 <ev>
Overriding. A method in C with the same name and parameter types as an inherited method overrides it — on every path: a call through a receiver typed as any ancestor dispatches to the most-derived override.
Super and base selection. super.method() resolves to the first declared parent. The qualified selectors super<Base>.method() and this<Base>.field name a specific base’s member when more than one parent is in play.
Discussion. A bare collision — two parents declaring the same member name, uninvolved in any override — is not yet diagnosed: as of 0.27.0 the resolution silently picks one parent’s member. The intended rule is strict-by-default (a collision is an error until the program disambiguates with the qualified selectors), and it is bound here when the diagnostic lands.
8.5 Abstract Classes and Methods
An abstract method is declared with the abstract modifier and no body. It contributes a signature and a dispatch slot, and it obligates every concrete descendant to supply an implementation. An abstract class is a class declared abstract. It is a type that bindings and parameters may name, and it is not instantiable.
A class that inherits an abstract method and does not override it is a compile-time error, CAJETA_ERROR_ABSTRACT_NOT_IMPLEMENTED. The obligation is inherited through every parent, so a class must satisfy the abstract methods of each of its bases (§8.4).
Example 8.5-1. An abstract base, a concrete subclass, and dispatch through a base-typed binding.
public abstract class Shape {
public Shape() { return; }
public abstract int32 area();
public int32 twice() { return this.area() * 2; }
}
public class Square extends Shape {
int32 side;
public Square(int32 s) { this.side = s; }
public int32 area() { return this.side * this.side; }
}
public final class C {
public static int32 run() {
Shape sh = heap Square(3);
return sh.twice(); // 18 — twice() calls the derived area()
}
}
Example 8.5-2. A rejected program. Blob is concrete and inherits area() without implementing it.
public abstract class Shape {
public Shape() { return; }
public abstract int32 area();
}
public class Blob extends Shape {
public Blob() { return; } // CAJETA_ERROR_ABSTRACT_NOT_IMPLEMENTED
}
Satisfaction across parents. Under multiple inheritance, an abstract method inherited from one parent is satisfied by a concrete method of the same signature inherited from another. The obligation belongs to the class, not to the branch that declared it, and dispatch through the abstract declaration lands on the concrete implementation.
Example 8.5-3. The obligation from A is discharged by the implementation in B, and a call through the A-typed binding reaches it.
public abstract class A {
public A() { return; }
public abstract int32 step();
}
public class B {
public B() { return; }
public int32 step() { return 42; }
}
public class Both extends A, B {
public Both() { return; }
}
public final class C {
public static int32 run() {
A a = heap Both();
return a.step(); // 42
}
}
Discussion. Three checks around
abstractare unenforced as of 0.27.0, recorded as disabled pinning tests intest/type/AbstractClassTests.cpp. Allocating a class that has an unimplemented abstract method compiles and faults at run time when the empty slot is called, rather than being rejected at the allocation site. An abstract method declared with a body is accepted, and the body is ignored. A class that declares an abstract method is not required to carry theabstractmodifier. The rules above are the intended behavior and bind when the diagnostics land.
8.6 Method Dispatch
Instance method calls dispatch virtually: the runtime selects the most-derived override for the receiver’s dynamic type, regardless of the receiver expression’s static type. Assigning a derived instance to a base-typed binding adjusts the reference to the base’s sub-object, and calls through it still reach the derived overrides.
8.7 Operator Declarations
Two shapes, by mutation:
- Binary operators are
public static, both operands explicit, no implicitthis. The operator returns a fresh value and mutates neither operand. Shipped:+ - * / % & | ^ << >>and the comparisons== != < > <= >=. - Indexed access is an instance member —
[]and[]=— because it targets the receiver. The call site must hold a mutable borrow for[]=.
Example 8.7-1. A static binary operator.
public final class Vec2 {
public float32 x; public float32 y;
public Vec2(float32 x, float32 y) { this.x = x; this.y = y; }
public static Vec2 operator+ (Vec2 a, Vec2 b) { return stack Vec2(a.x + b.x, a.y + b.y); }
}
Vec2 v = stack Vec2(1.0f, 2.0f) + stack Vec2(3.0f, 4.0f);
System.stdout.println("" + v.x + "," + v.y); // 4,6
Discussion. Deferred operator forms — unary
+/-, mutating++/--, compound assignment, andoperator!/operator~— have grammar coverage in part but are not lowered. They are specified in the internal operator document and bound here as they ship.
8.8 Destructors
A class may declare one destructor, ~ClassName():
- The identifier must match the class, and a parameter list or return type is a parse error.
- It is not user-callable. Only the drop chain invokes it (Allocation §4).
- Inside the body,
thisis live: fields, methods, and intrinsics all work. The instance’s memory is reclaimed after the body returns. - It runs exactly once per instance, at whichever drop entry ends up owning the instance (Ownership §5.9).
Destructor chaining is automatic and non-suppressible: the drop runs the class’s own destructor body and field auto-drops, then every transitive ancestor’s, each ancestor exactly once even in a diamond. Dispatch on drop is virtual for heap instances — Base b = heap Derived() fires ~Derived() — while stack instances use static dispatch, since the allocation site fixes the dynamic type.