3.1 Classes
3.1.1 Syntax
[decorators][export] [abstract] class Name[<TypeParams>] [extends Base[<TypeArgs>]] [implements I1, I2, …] member*endClasses may only be declared at the top level of a file (E0115). Members:
class Animal -- fields name: string protected sound: string = "..." readonly id: number nickname?: string -- optional field (string?) static count: number = 0 -- constructor constructor(name: string, id: number) self.name = name self.id = id Animal.count += 1 end -- methods (self is implicit) function speak(): string return `{self.name} says {self.sound}` end static function create(name: string): Animal return Animal.new(name, Animal.count + 1) end -- accessors (§3.1.10) get label(): string return `{self.name} #{self.id}` end -- metamethods function __tostring(): string return `Animal({self.name})` endend| Modifier | Applies to | Meaning |
|---|---|---|
public |
fields, methods, accessors | Default. Accessible everywhere. |
protected |
fields, methods, accessors | Accessible in the class and its subclasses. |
private |
fields, methods, accessors | Accessible only inside the class body. |
static |
fields, methods | Belongs to the class table, not to instances. |
readonly |
fields | Assignable only in its initializer or the constructor. |
abstract |
methods (abstract classes only) | No body. Subclasses must implement it. |
override |
methods, accessors | Required when overriding a superclass member (implementing an abstract method included), and an error when nothing is overridden (E3005). |
overload |
method and constructor signatures | Declares an overload signature without a body (§3.1.11). |
Access modifiers and readonly are compile-time only. They produce no
runtime checks.
Visibility details.
- As in TypeScript,
privatemembers are accessible on any instance of the same class from inside the class body, not only onself. protectedmembers are accessible from inside the declaring class and its subclasses, on instances of the accessing class or its subclasses.- A field and a method (or accessor) with the same name in one class, or across a class and its superclass, is error E3042.
3.1.2 Instances and construction
Class.new(args) creates an instance. Class(args) is equivalent (the class
table has a __call metamethod). The checker treats both as constructor
calls. .new avoids the metamethod and is what the compiler emits for
internal construction.
Construction order:
- a new table is created with the class as metatable,
- field initializers run in declaration order (for a derived class: after
the
super(...)call, see below), - the constructor body runs.
A class without a constructor gets an implicit one. For a base class it takes no parameters. For a derived class it forwards all arguments to the superclass constructor and has the superclass constructor's parameter types.
Varargs. A constructor may be variadic (constructor(...: string)). Field
initializers run inside the generated __init but are not part of the
constructor's body, so ... cannot be used in field initializers or static
initializers (E3041).
Definite assignment. In strict mode, every non-optional instance field
without an initializer must be assigned on every path through the constructor
before the constructor returns or calls a method of a wowlua class on self
(E2052). Calls of native methods (self:SetSize(…) in a widget class) do not
end the check, because they cannot read the fields.
3.1.3 Methods and self
Methods declared with function name() take an implicit first parameter
self typed as the class instance (or Self in generic contexts). They are
called with :: dog:speak(). Calling a method with . and no explicit
receiver is an error (E2008). Static methods have no self and are called with
.: Animal.create("x").
Inside methods, self.field accesses fields. A field or method that is not
declared is an error on class instances (E2003) in both modes. Classes are
sealed: all members are declared in the class body. Defining a method
outside the body (function Animal:extra() … end at top level) is E2003, with
a quick fix that moves it into the class (decision L29).
3.1.4 Inheritance
export interface Pet name: string function play(withToy: string?): booleanend class Dog extends Animal implements Pet private static lastId: number = 100 tricks: string[] = {} static function nextId(): number Dog.lastId += 1 return Dog.lastId end constructor(name: string) super(name, Dog.nextId()) -- must come before any use of self self.sound = "woof" end override function speak(): string return super:speak() .. "!" end function play(withToy: string?): boolean return withToy ~= nil endend- Single inheritance. A class extends at most one class (E3008 for non-class values).
- In a derived class's constructor,
super(args)calls the superclass constructor. It must be called exactly once, as a top-level statement of the constructor body, before any access toself(E3001). The derived class's field initializers run immediately after it. super:method(args)/super.method(self, args)calls the superclass implementation. Lowered to a direct call on the superclass table:Animal.speak(self). In static methods,super.memberrefers to the superclass table (super.count→Animal.count).- Static members are inherited:
Dog.countreadsAnimal.countuntilDogassigns its own. - Metamethods are inherited. The runtime copies them into subclasses at class finalization, because Lua looks up metamethods with a raw access.
3.1.5 Abstract classes
abstract class Shape abstract function area(): number function describe(): string return `area {self:area()}` endendAn abstract class cannot be instantiated (E3002), whether directly or through a
value of type ClassOf<T> where T is abstract. Every concrete subclass must
implement all abstract methods (E3003). In debug builds, abstract methods are
emitted as stubs that raise an error if ever called.
3.1.6 Generic classes
class Stack<T> private items: T[] = {} function push(item: T) self.items[#self.items + 1] = item end function pop(): T? local n = #self.items; local v = self.items[n]; self.items[n] = nil; return v endend local s: Stack<string> = Stack.new()Type arguments are inferred from constructor arguments or the expected type. They are erased at runtime. Variance follows Types §2.2.6.
3.1.7 Reserved member names
new, __index, __newindex, __name, __super, __init, __ctor,
__initFields, __class, __meta, __getters and __setters are reserved
(E3004). The metamethods allowed as methods are __tostring, __eq, __lt,
__le, __add, __sub, __mul, __div, __mod, __pow, __unm,
__concat and __call.
3.1.8 Typing
- Classes are nominal. A value has type
Dogonly if it was created byDogor a subclass. A record with the same fields is not aDog. - Class instances may be assigned to interfaces and record types they structurally satisfy through their public members (Types §2.6).
- The class value itself has type
ClassOf<Dog>. It exposes static members,newand the call signature. implements Iis checked at the class declaration (E3006 for missing or incompatible members). It also addsIas a declared supertype for documentation and reflection.
3.1.9 Definition order and lowering
A class declaration executes in this order (decisions L24, L26):
- Hoisting. Every top-level exported function, class and enum is pre-declared as a local at the top of the module, so declarations can refer to each other regardless of source order.
- The class table is created (
__wl.class). - Methods, accessors and the constructor are defined, in member order.
- Member decorators run, in member order (Decorators §4.5.2).
__wl.finalizecopies inherited metamethods and installs debug stubs.- Static field initializers run once, in declaration order. They can call static methods and see the finalized class.
- Class decorators run.
- Registering decorators (
@global) and the export assignment receive the final class value.
export class Dog extends Animal tricks: string[] = {} static created: number = 0 constructor(name: string) super(name, 1) self.sound = "woof" end override function speak(): string return super:speak() .. "!" endend-- module top: local Dog (hoisted)Dog = __wl.class("Dog", Animal)function Dog.__init(self, name) Animal.__init(self, name, 1); self.tricks = {} self.sound = "woof"endfunction Dog.speak(self) return Animal.speak(self) .. "!"end__wl.finalize(Dog); Dog.created = 0; __wl_exports.Dog = Dog__wl.class(name, super) creates the class table (with __index pointing to
itself, a metatable chaining to super and providing __call), and defines
new. Accessors are registered with __wl.accessor(cls, kind, name, fn)
(kind is get, set, sget or sset), and abstract methods of debug builds
with __wl.abstract(cls, name):
cls.new = function(...) local self = setmetatable({}, cls) cls.__init(self, ...) return selfendField initializers of a base class without a constructor are emitted into a
generated __init. A class with no fields and no constructor inherits
__init (the root __init is a no-op).
3.1.10 Accessors (getters and setters)
class Temperature private celsius: number = 0 get fahrenheit(): number return self.celsius * 9 / 5 + 32 end set fahrenheit(value: number) self.celsius = (value - 32) * 5 / 9 endend local t = Temperature.new()t.fahrenheit = 212print(t.fahrenheit) -- 212get name(): T … endandset name(value: T) … enddeclare a property. A getter without a setter is read-only, and assignment is error E2010.- Accessors may be
static,private/protected, andoverridea superclass accessor. - Lowering. Only classes that declare accessors, and their subclasses, get
function-based
__indexand__newindexmetamethods that dispatch property names to the getter and setter tables, and fall back to the class table for methods. Property values are never stored under the property name, so__newindexfires for every property assignment. All other classes keep the plain table__index, so field access stays fast. - Widget classes cannot declare accessors (E3043), because the frame's metatable belongs to WoW.
3.1.11 Overloads
A method or constructor may declare several overload signatures, followed by one implementation:
class Inventory overload function find(id: number): Item? overload function find(name: string): Item? function find(key: number | string): Item? if type(key) == "number" then return self.byId[key] end return self.byName[key] endend- Callers see only the overload signatures, which are resolved as in Types §2.7.
- The implementation signature must accept every overload (E3044) and is not callable directly.
- Overloads are compile-time only. One function is emitted.
3.2 Widget classes
A class that extends a native widget type, i.e. a declared class marked
@widget("…") such as Frame, Button or StatusBar, is a widget class.
WoW widgets are created by CreateFrame and already have a metatable, so
widget classes are lowered as mixins, following Blizzard's own
Mixin pattern.
export class CounterButton extends Button count: number = 0 constructor(parent: Frame) super(nil, parent, "UIPanelButtonTemplate") -- CreateFrame("Button", nil, parent, template) self:SetSize(120, 24) self:SetScript("OnClick", function() self:increment() end) self:refresh() end function increment() self.count += 1 self:refresh() end function refresh() self:SetText(`Clicked {self.count}x`) endend local b = CounterButton.new(UIParent)b:SetPoint("CENTER")Rules:
- For the first class in the chain that extends a native widget,
super(name?, parent?, template?, id?)creates the frame withCreateFrame(<widgetType>, name, parent, template, id). Before that call,selfdoes not exist (E3001). In that class,super:m(args)calls the native widget method (getmetatable(self).__index.m(self, args)). - Native widget types are the classes declared with
@widget("…"). Until the generatedwow-apipackages provide them (milestone M4), the compiler declares every frame typeCreateFramecreates (decision T77). - At finalization, all methods of the class and its wowlua superclasses are flattened into one mixin table. Construction applies the most-derived class's flattened mixin with a raw copy, so overrides work and virtual calls in constructors dispatch to the most-derived implementation.
- Widget classes may extend other widget classes (
class FancyButton extends CounterButton). Construction is threaded through a hidden__ctor(cls, …)so only the root creates the frame. - Metamethods and accessors are not allowed on widget classes (E3007, E3043), because the frame's metatable belongs to WoW.
- The instance has a
__classfield pointing to its class, used byreflect.classOfandreflect.isInstance. @global("MyButtonMixin")exposes the flattened mixin as a global so XML templates can use it withmixin="MyButtonMixin". A method namedOnLoadis then called by the XMLOnLoadscript as usual. Field initializers for XML-created frames run in a generated__initFields(self), which the generated mixin calls before the user'sOnLoad.
3.2.1 Script handlers
The core decorator @script(name, opts?) makes an instance method of a widget
class the handler of one of the frame's scripts:
export class InputBox extends EditBox onSubmit: (string) -> () constructor(parent: Frame, onSubmit: (string) -> ()) super(nil, parent, "InputBoxTemplate") self.onSubmit = onSubmit end @script("OnEnterPressed") function submit() self.onSubmit(self:GetText()) self:ClearFocus() end @script("OnShow", { hook = true }) -- keeps the template's OnShow function shown() … endend- The handler is set with
SetScript, replacing any other handler of that script; with{ hook = true }it is added withHookScript. - Handlers are installed after the most-derived constructor has finished,
so the constructor cannot trigger them early (a handler the constructor sets
for the same script is replaced). For frames created from XML, they are
installed by the generated mixin's
OnLoad, after the field initializers and before the class's ownOnLoad. selfis the frame; the script's arguments are the method's parameters (function clicked(button: string, down: boolean)).- Registrations are inherited: a subclass that overrides a decorated method
keeps the registration, and its override is called. A
SetScripthandler of a subclass for the same script replaces the superclass's; hooks add up. @scriptapplies only to instance methods of widget classes (E3021). The checker (milestone M4) also checks the script name against the widget type.
3.3 Interfaces
export interface Pet name: string owner?: string function play(withToy: string?): booleanend interface Comparable<T> function compare(other: T): numberend interface Saveable extends Pet, Comparable<Saveable> function serialize(): stringend- Interfaces are compile-time only and produce no code.
function m(...)members are method signatures with implicitself. Fields use the record field syntax and may bereadonly.- Interfaces may extend multiple interfaces.
- Any value with a compatible shape satisfies an interface: class instances,
records, plain Lua tables.
implementsis an explicit check, not a requirement. - Interfaces may be declared in any block and are scoped to it.
- Two interfaces with the same name in the same scope are error E1031. Only
ambient
declare interfacedeclarations merge (Types §2.4.7).
3.4 Enums
3.4.1 Syntax
enum Direction { Up, Down, Left, Right } -- 1, 2, 3, 4 enum Quality { Poor = 0, Common, Uncommon, Rare, Epic, Legendary } -- 0..5 enum Anchor { TopLeft = "TOPLEFT", Center = "CENTER",}- Members are separated by
,or;, and a trailing separator is allowed. - Members without a value are numbered: they continue from the previous numeric
value plus one, starting at
1(Lua convention). - Explicit values must be number or string literals or constant expressions.
- An enum is either fully numeric or fully string-valued (E3010).
- Duplicate values are an error (E3011).
- Enums may only be declared at the top level of a file (E0115).
3.4.2 Typing
Directionas a type is the union of its members.Direction.Uphas the member typeDirection.Up.- Enum members are assignable to their enum and to the underlying primitive
(
number/string). The reverse needs a cast (raw :: Direction) or a checked conversion (reflect.enumFrom(Direction, raw)), because enums are nominal. switch-like narrowing on enum comparisons works as for literal unions.- Assigning to an enum member (
Direction.Up = 5) is error E2010.
3.4.3 Lowering
Member accesses with a constant key are inlined: Direction.Up becomes 1.
The enum table is emitted only if the enum is used as a value (iterated, passed
around, used with reflect, or exported to plain Lua):
local Direction = { Up = 1, Down = 2, Left = 3, Right = 4 }An exported enum always gets its table until tree-shaking removes unused
ones (milestone M7, decision T81). The emitted table contains exactly the
members, in every profile. Debug builds
attach a metatable to that same table (no proxy) that raises an error when an
unknown member is read or a new key is assigned. pairs therefore behaves
identically in debug and release builds, and plain Lua code sees the same
table.
Iteration order. pairs over the enum table is unordered, as for any Lua
table. reflect.enumEntries(E) returns the members in declaration order, and
reflect.enumName(E, value) does reverse lookup, so the enum table never
contains anything but its members.