Classes, Traits and Inheritance
Status: Stable (the OOP syntax freeze, 0.2.0a2) · Evidence: aura/parser/to_ast.py (parse_class_decl, parse_trait_decl), aura/transpiler/transformers/statements.py (transform_ClassDecl, transform_TraitDecl), aura/transpiler/rules.py (E307/E308/E309/E314/E315/ E316/E317/E321).
Aura has four kinds of type declaration: class, trait, abstract class (a class with abstract), and enum. There is no record and no interface. Instances are created by calling the type: Point(1, 2), never new Point(1, 2) (new is rejected).
1. Classes
class Point {
public let x: int = 0
public let y: int = 0
public def new(x: int, y: int) {
self.x = x
self.y = y
}
public def distance() -> float {
return (self.x ** 2 + self.y ** 2) ** 0.5
}
}
let p = Point(1, 2)
print(p.distance())
1.1 Header fields (the constructor)
Fields may be declared in the class header. The header generates the constructor and the accessors:
class User(private name: str, mut age: int = 0, public id: int = 0) {
public def greet() -> str { return "hi " + self.get_name() }
}
| Header field | Generated |
|---|---|
name: str | constructor parameter name; get_name(); stored as self.name |
private name: str | same, but the field is private (owner-mangled) |
mut age: int = 0 | get_age() and set_age(v); defaults to 0 |
public id: int = 0 | get_id()/set_id() and direct access u.id |
A required header field (name: str with no default) may not follow an optional one (age: int = 0) — the constructor would have no unambiguous position for it.
1.2 One constructor style per class
A class has exactly one constructor style:
- header fields (
class U(name: str) { ... }), or - body fields with a manual
def new(...) { ... }.
Mixing a header with a manual new is a syntax error: the header already generates a constructor and a second one would silently leave the header fields unassigned.
// OK — header style
class A(name: str) { }
// OK — body style
class B {
public let name: str = ""
public def new(name: str) { self.name = name }
}
// ERROR — header and manual `new` mix
class C(name: str) {
public def new(name: str) { self.name = name }
}
def new(...) compiles to __init__. Instantiate with C(args), never C.new(args).
1.3 Built-in protocols (dunder methods)
Aura maps a readable, unprefixed method name to its Python dunder when the member is declared. The mapping is fixed (SPECIAL_METHOD_NAMES in aura/transpiler/ast.py); a representative subset:
| Aura method | Python dunder |
|---|---|
new | __init__ |
destroy | __del__ |
str / repr / format | __str__ / __repr__ / __format__ |
bool / int / float / hash / len | __bool__ / __int__ / __float__ / __hash__ / __len__ |
eq / ne / lt / le / gt / ge | __eq__ / __ne__ / __lt__ / __le__ / __gt__ / __ge__ |
getitem / setitem / delitem / contains | __getitem__ / __setitem__ / __delitem__ / __contains__ |
iter / reversed | __iter__ / __reversed__ |
call | __call__ |
add / sub / mul / truediv / mod / pow / neg / invert | __add__ / __sub__ / __mul__ / __truediv__ / __mod__ / __pow__ / __neg__ / __invert__ |
enter / exit | __enter__ / __exit__ |
Names not in the map (next, aenter, aexit, await, …) are written as the literal dunder (def __next__(self) { ... }), which always works. A readable alias is used only where the map defines one. Writing the literal dunder for a mapped name is also accepted, but the readable form is canonical.
self (and cls) may be written as the explicit receiver parameter — def str(self) { ... } — even though self is a reserved word elsewhere.
2. Visibility
Every class member must carry an explicit visibility modifier; omitting it is E307 (MISSING_VISIBILITY).
| Modifier | Meaning |
|---|---|
public | reachable from anywhere; a public field is also read/written directly |
private | reachable only inside the declaring class (owner-mangled at runtime) |
protected | reachable inside the declaring class and its subclasses |
Accessing a non-public member from outside is E308 (INACCESSIBLE_MEMBER). The check is owner-aware: a subclass may reach a parent’s protected member but not its private one.
class Account {
private let balance: int = 0
public def deposit(amount: int) { self.balance = self.balance + amount }
public def get_balance() -> int { return self.balance }
}
self.x = ... (member assignment) is always allowed inside the class, even for an immutable let field — the field’s immutability governs the binding, not in-place mutation of the object.
3. Inheritance
Inheritance uses extends only (implements is rejected). A class may list several traits/classes: class C extends A, B.
class Animal { public def speak() -> str { return "..." } }
class Dog extends Animal {
public def speak() -> str { return "woof" }
}
- A subclass that declares a method with the same name overrides it. There is no
overridekeyword; writingoverride defis a parse error. - The subclass header (
class Admin extends User(email: str)) declares only its own fields; inherited fields arrive from the base constructor. - A base must resolve to a class/trait or a built-in exception root; otherwise
E314(UNKNOWN_BASE_CLASS). A duplicate base or an inheritance cycle isE315(INVALID_INHERITANCE). super(args)calls the parent constructor;super.method()calls the parent method. Callingsuper.m()wheremis an abstract (body-less) method with no implementation isE321(ABSTRACT_SUPER_CALL).
4. Traits
A trait is a pure contract: methods, no state, no constructor. A method with no body is already abstract — abstract is not used inside a trait (trait T { abstract def f() } is a parse error).
trait Drawable {
public def draw() -> void
public def bounds() -> float
}
class Circle extends Drawable {
public let radius: float = 1.0
public def draw() -> void { print("circle") }
public def bounds() -> float { return 3.14159 * self.radius * self.radius }
}
A trait transpiles to a base class. A concrete class must implement every body-less method it inherits, or it is E309 (UNIMPLEMENTED_ABSTRACT).
5. Abstract classes
An abstract class is a real class — fields, concrete methods and a constructor — that cannot be instantiated and may defer methods with abstract def.
abstract class Shape {
public let name: str = "shape"
public def describe() -> str { return "a " + self.name }
public abstract def area() -> float
}
class Square extends Shape {
public let side: float = 2.0
public def area() -> float { return self.side * self.side }
}
print(Square().area()) // 4.0
print(Square().describe()) // a shape
// let s = Shape() // E316: abstract, cannot be instantiated
Rules:
abstractis a modifier beforeclassor beforedef:abstract class C { public abstract def f() -> int }. Anabstract defis body-less;abstract def f() { ... }andabstract def f() = exprare syntax errors. Anabstract defmay appear only in anabstract class.- An
abstract classmay itself defer:abstract class Mid extends Shapecompiles; the obligation passes transitively to the concrete subclass. - Instantiating an
abstract classisE316at compile time; it also compiles to a Python ABC with@abstractmethod, so the rule holds at runtime. - A concrete class that does not implement every inherited abstract method (from a trait or an abstract class) is
E309, naming the class, the method and its declarer. - Overriding is implicit;
overrideis not a keyword.
Use a trait when the base needs no state; use an abstract class when it needs fields or shared concrete behaviour.
6. Static and class methods, properties
class MathUtil {
public @staticmethod def max(a: int, b: int) -> int {
if a > b { return a }
return b
}
public @classmethod def create() -> MathUtil {
return MathUtil()
}
}
class Rect {
public let w: int = 0
public def new(w: int) { self.w = w }
public @property def area() -> int { return self.w * 2 }
}
print(MathUtil.max(3, 9)) // 9
print(Rect(4).area) // 8 — accessed without parentheses
@staticmethod— no receiver; usingself/clsinside isE317(SELF_IN_STATIC).@classmethod— first parameter iscls.@property— the method is read as a field (r.area, notr.area()).
7. Nested classes and generics
Classes may nest (class Outer { public class Inner { } }), and may be generic with bracket type parameters:
class Box[T] {
public let value: T = none
public def new(value: T) { self.value = value }
public def get() -> T { return self.value }
}
let b = Box(42)
print(b.get()) // 42
Generic parameters use brackets only: Box[T], never Box<T>.
8. Enums
enum Color { Red, Green, Blue }
enum Status { Ok = 200, NotFound = 404 }
Members are separated by ,, ;, or a newline. enum transpiles to a Python enum: Color.Red prints as Color.Red, is a distinct member value, and never equals the bare string "Red".
9. What is NOT part of class syntax
| Spelling | Why | Use instead |
|---|---|---|
new C() | new is rejected | C() |
class C(...) as a record | no records in Aura | a class with header fields |
class C implements I | implements is not Aura | class C extends I |
override def f() | overriding is implicit | def f() |
def init() | one constructor name | def new() |
abstract def in a trait | trait methods are already abstract | def f() |
let private x in a body | modifiers prefix the declaration | private let x |
header fields and def new | one constructor style | pick one |
| metaclasses | not exposed | — |