Leaguepedia | League of Legends Esports Wiki

Edit the documentation or categories for this module. LuaClassSystem (LCS) is a Lua library for emulating some features of object-oriented programming (classes, inheritance) on top of Lua. The version used on Leaguepedia is derived from the original version on GitHub that hasn't been maintained since mid-2014.

User documentation[]

This section of the documentation is intended for those who wish to use LCS in their modules.

System requirements[]

Compatibility with the following environments is intended:

  • MediaWiki Scribunto (both standalone Lua and LuaSandbox).
  • Non-MediaWiki Lua 5.1 and all newer versions, including other standalone implementations based on these versions (for an incomplete list, see the one on the LuaUsers wiki). Please note that none of these environments have yet been tested with the modified version, though they are expected to work because at least some of them work with the original version.


LuaClassSystem should be placed into a separate file (or, when using MediaWiki, a module) and imported with a require() call. The value returned by the call is a table with the following functionality:

  • _VERSION – a string designating the current version of LCS. At the moment it's equal to 2.1.1.
  • class (details) – a table with the functionality to create new classes:
    • field abstract – a class constructor that creates an abstract class. Attempting to instantiate an abstract class is an error.
    • field final – a class constructor that creates a final class. Attempting to inherit from a final class is an error.
    • The table class itself is also callable as a class constructor that creates a regular class, i. e. one that is neither abstract nor final.
  • instanceOf (details) – a function that checks whether an object belongs to a given class. See the documentation for instanceOf for details.
  • xtype (details) – a function that behaves like the builtin type, but returns "object" or "class" for LCS values. See the documentation for xtype for details.
  • isObject (details) – a function that returns true/false depending on whether it is provided an LCS object.
  • isClass (details) – a function that returns true/false depending on whether it is provided an LCS class.
  • debug – features intended to be used by people developing LCS itself. They are not intended for non-developers.

In code examples provided in the documentation, the table as listed above is referenced by the variable LCS. This can be achieved in Scribunto like: local LCS = require("Module:LuaClassSystem").

Creating classes[]

LuaClassSystem can create base classes by means of any of the three class constructors:

  • LCS.class, which produces a regular class.
  • LCS.class.abstract, which produces an abstract class that can't be instantiated.
  • LCS.class.final, which produces a final class that can't be extended.

All of the three class constructors are callable values with the same syntax. Class constructors may optionally accept a member table that will be used as the base for the class itself and all of its instances. The member table for the class is a deep copy of the one provided to the class constructor, and instances are created as deep copies of the class. Deep copies created in this manner copy references to LCS objects or classes instead of cloning the objects or classes. The member table may not contain functions directly.

The following code creates a class:

local MyClass = LCS.class()

Instance constructors[]

To create an instance of a class, use the :new() method of the class or call the class directly.

local MyClass = LCS.class()

-- These are functionally identical, and the former internally falls back to the latter.
local object1 = MyClass()
local object2 = MyClass:new()

You can have an instance constructor for a class that does more than merely creates an empty instance. For example, such a constructor could initialize some fields of the class. To do that, declare an :init() method for the class.

The :init() method is called as part of the :new() method, receives all values passed to :new(), and should not return any values. (Any values returned by :init() implementations will be ignored.) Do not override the :new() method itself. See Reserved names for more information.

The following code creates a class, declares a custom constructor for a class, and uses it to initialize a field.

local MyClass = LCS.class()

function MyClass:init(length)
    local length = tonumber(length) or 0
    self.array = {}
    for i = 1, length do
        self.array[i] = math.sqrt(i)

local object1 = MyClass:new(4)
print(object1.array[2]) -- prints sqrt(2), approximately 1.414
print(object1.array[4]) -- prints 2

Declaring methods[]

Methods are declared on classes in the same manner as seen above with init.

local MyClass = LCS.class()

function MyClass:init()
    self.value = 10

function MyClass:runCountdownTick()
    if self.value > 0 then
        self.value = self.value - 1
    return self.value

function MyClass:isReady()
    return self.value == 0

function MyClass:reset()
    self.value = 10

local object = MyClass:new()
for i = 1, 8 do
-- value is now 2

local one = object:runCountdownTick()
local zero = object:runCountdownTick()

object:reset() -- back to 10

Reserved names[]

Some built-in class fields rely on internal functionality and/or implement such functionality. If users set these fields to other values, such functionality is likely to break, causing runtime errors. Classes should not have elements with the following names:

  • new: used for instance creation.
  • extends, abstractExtends, finalExtends: used for extending classes (see below).
  • super: used when calling original versions of methods from superclasses.
  • getClass, getSubClasses: built-in functionality.
  • __index, __call: reserved metamethods, used for class system functionality.

Extending classes[]

Subclassing (inheritance) allows having a single source of shared functionality between multiple classes where the subclasses are "subtypes" of the base type. For example, this is a common approach in GUI libraries where "Component" or such is the base class that implements functionality common to all GUI components, and every subclass ("Button", "Image", "ScrollPane"...) implements its own functionality in addition to the common features.

Lua does not implement classes and inheritance natively (why libraries like LCS are needed), but it does implement features useful in construction of class systems (the :method() syntactic sugar, __index metamethods, and maybe others).

To create a subclass, call the :extends() method on the base class. The method takes an optional "member table" with functionality identical to that of class constructors.

local BaseClass = LCS.class()

local DerivedClass = BaseClass:extends()

Derived classes (subclasses) do not clone class fields. Methods and fields not specified in the subclass are accessible from the superclass via an __index fallback. Modifications performed to table fields through a subclass are replicated in superclasses:

local BaseClass = LCS.class()
BaseClass.values = {1, 2, 3}

function BaseClass:sum()
    local sum = 0
    for _, v in ipairs(self.values) do
        sum = sum + v
    return sum

print(BaseClass:new():sum()) -- prints 6

local DerivedClass = BaseClass:extends()
table.insert(DerivedClass.values, 4)

print(DerivedClass:new():sum()) -- prints 10. Note that :sum() is not defined in the derived class
print(BaseClass:new():sum()) -- ...also prints 10 now

Interacting with superclasses[]

Subclasses often override methods defined in their superclasses, and also need defer to the original implementation. This is what the :super() method is for.

Unlike some other Lua class system implementations, LCS does not require to retain a reference to the superclass to call an original version of a method from that superclass. Syntax like SuperClass.super(self:method(args)) isn't needed and is replaced with self:super('method', args). While using the function name (as a key to the superclass) as a string might look less aesthetically pleasing, it should be noted that in Lua, .field is syntactic sugar for ['field'].

local Counter = LCS.class()

function Counter:init()
    self.value = 0

local DoubleCounter = Counter:extends()

function DoubleCounter:init()
    self.value2 = 0

Passing a method name that isn't overridden in the inheritance chain for the calling class is invalid. This is likely to produce an error.

local Counter = LCS.class()

function Counter:init()
    self.value = 0

local DoubleCounter = Counter:extends()

function DoubleCounter:init()
    self:super('meow') -- no cats involved (sadly), so Lua just crashes.
    self.value2 = 0

If the original method isn't overridden, there is no need to use :super(), and the method can be called directly.

Abstract and final classes[]

A feature inspired by some class system implementations such as the one in Java, abstract and final classes restrict some inheritance-related behavior.

Abstract classes do not allow creation of instances. To create an abstract class, use LCS.class.abstract(). To make a derived abstract class, use BaseClass:abstractExtends(). Note that the base class does not need to be abstract in the latter case.

Final classes do not allow creation of subclasses. To create a final class, use LCS.class.final(). To make a derived final class, use BaseClass:finalExtends().

For hopefully obvious reasons, a class cannot be both abstract and final. It should not be possible to make such a class, but if one is made, the program is considered to be in an invalid state.

Checking types[]

LCS exports some functions for checking types of objects in an LCS-aware manner.

LCS.instanceOf allows to check whether a given object is an instance of a given class or its direct or indirect superclass.

local Animal = LCS.class()
local Feline = Animal:extends()
local Cat = Feline:extends()
local Tiger = Feline:extends()

local flash = Cat()
assert(LCS.instanceOf(flash, Animal))
assert(LCS.instanceOf(flash, Feline))
assert(LCS.instanceOf(flash, Cat))
assert(not LCS.instanceOf(flash, Tiger))

LCS.xtype stands for extended type and works like type(), but returns "object" or "class" instead of "table" for known LCS objects and classes.

LCS.isObject and LCS.isClass return boolean values for whether the given value is a known LCS object or class respectively.

Advanced usage[]

There are some nontrivial possibilities allowed by LCS. Here are some of them.

Types as parameters[]

An LCS class can accept another LCS class as a parameter and expect to be able to perform some operations with instances of that class. While not enforced in any way by LCS itself, this offers the possibility for informal "interface"/"trait" specifications. Alternatively, the parameter class may be expected to be a subclass of some superclass, the functionality of which is then used. Compare it to T implements Interface and T extends BaseClass in Java terminology.

Additional considerations[]


General-purpose cloning functions attempt to reconstruct LCS objects in a manner that does not make LCS aware of these new objects. Such cloned objects trigger state validity checks if used with some LCS internal methods. For example, calling :new() from a cloned class or :super() for a cloned object will cause an error because the class / object is not known to LCS.

mw.clone() is known to cause such errors.

LCS exports LCS.deepCopy, which is an LCS-aware deep copying / cloning function. References to stored objects or classes are preserved. In addition, this function disregards metatables on any non-LCS values in the original table. This makes it incidentally work on mw.loadData() tables (mw.clone() breaks on them because of the __index/__newindex-based write protection), but if any contained tables rely on metatables being preserved for correct operation, LCS.deepCopy is unsuitable.

Two registries[]

It is invalid for two LCS instances (each with its own registry) to exist simultaneously. This can happen, for example, if LCS is imported twice as two different modules. In such a case, a lot can go wrong, up to the point of the program crashing due to an attempt to deep-copy a recursive object.

Developer documentation[]

Debug features[]

debug.isKnownObject, debug.isKnownClass: same as LCS.isObject and LCS.isClass (but the debug versions came before). The debug versions also give errors if given something other than tables.

Object registry data[]

  • __superClass (class): the class the object is an instance of

Class registry data[]

  • __superClass (class|false): the superclass of this class, or false if this is a root class
  • __subClass (map: class → true): stores weak references to direct subclasses as indexes so that indexing the table is enough to say whether some other class is a direct subclass of the current class.
  • __abstract (boolean): true if this class is abstract
  • __final (boolean): true if this class is final


If any of the following is broken, the LCS state is invalid, which incurs undefined behavior.

  • No value may be registered as both a class and an object.
  • No registry data value may be of an inappropriate type.
  • There may be no inheritance loops.
  • No class may be both abstract and final.
  • Only one registry may exist in a single Lua state at any given time.
  • All LCS classes and objects must be referenced in the LCS registry with valid data.

Dev stories[]

Good First Bug[]

...except there wasn't anything good about it.

The first bug discovered with LuaClassSystem was rather damaging because it made some pages give errors that looked like the user was doing something wrong. Yet the code was just about exactly the same in two different pages, one of which worked fine, and the other gave the error. Running the code in the debug console also didn't show any errors.

I was randomly poking around the module for maybe an hour or two and eventually edited out the registry's __mode settings, hoping that they weren't the issue. They turned out to be the issue. I wasn't happy.

A bit of an explanation, because the feature I refer to is probably not in the spellbook of almost any Scribunto wizard. Lua uses a garbage collector to keep track of what values are created and which of them are still used. Unlike with manual memory management (like in C), this does not require the programmer to manually malloc/free in just the right ways to avoid very serious bugs (from memory leaks eventually crashing programs or player characters' heads turning into brown blobs... to anything like major security vulnerabilities). Garbage collection is not suitable for lower-level programming, but for high-level features such as Scribunto it's fine.

With garbage collectors, any object still referenced is considered used and is not collected. (Maybe also with some variations to prevent reference cycles from causing memory leaks.) As the LCS registry keeps track of all objects it creates by referencing them, it means Lua never stops "using" any LCS objects even if they are just tracked in the registry. In other terms, the registry causes a memory leak.

That's why weak references exist. Weak references allow to refer to an object in a manner that doesn't prevent the garbage collector from collecting it. In Lua, weak references are assigned as values of the __mode setting in the metatable. The value must be a string: if it contains k, the keys in the table are weak, and if it contains v, the values are weak. __mode may contain either or both letters.

The registry associates references to LCS objects / classes with tables storing internal data. As such, the keys should have been stored as weak references. But the original LCS assigned __mode as v, and since the internal tables weren't used elsewhere, any garbage collection (an event the programmer cannot predict or observe) wiped out every single entry in the registry, causing data corruption.

So this sequence of events caused an error:

  1. User code creates a class.
  2. Lua collects garbage. Suddenly the above class isn't a class any more! And the user has no idea.
  3. User code attempts to instantiate the class that's no longer a class. LCS has checks against instantiating from non-classes (likely to guard against Class.new() instead of Class:new()), which get triggered and crash Lua with an error.


This is Super Fun[]

Oh, that one wasn't really fun. That time I had to wait until I can mess with the staging environment so that I can safely break everything.

Spent five hours trying to understand why :super() wasn't working in some module. Turned out that many lines of code before, the table storing LCS objects got fed to a utility function called safe that did... I don't remember what it did, but part of what it did was cloning the table via mw.clone(). That turned out rather unsafe instead: the cloned objects weren't recognized as such by LCS, and while they worked normally for regular usage (modifying, calling methods) a :super() call down the line exposed the error.

The Bloodthirster, Blade of the Ruined King... and pineapples, kittens, kittens, kittens![]

That one was even worse. Didn't help that resolution was a priority due to active performance issues.

With UCP, Leaguepedia eventually got LuaSandbox to significantly improve Scribunto performance. Esports wikis had to delay LuaSandbox rollout due to an issue with a module producing incorrect output.

At first glance, __ipairs, a Lua 5.2 feature backported by Scribunto devs to the 5.1 baseline, stopped working. Yet after further investigation it became clear it stopped working only in some specific scenario.

I had the possibility of using a hacky workaround to maybe stop the issue, but:

  1. the issue wouldn't be gone,
  2. the workaround would have been tailored to one specific Leaguepedia module and might have caused other issues I wouldn't be able to easily detect.

So I tried poking around, like in the last case. I eventually hit upon that inheritance breaks __ipairs, and later that the __ipairs metamethod wasn't in the metatable itself, but with LCS, that metatable itself had a metatable, and the __index fallback for the first metatable had __ipairs. In other terms, originally with LuaStandalone the __ipairs backport used something like (getmetatable(t) or {}).__ipairs, while LuaSandbox used something like rawget(getmetatable(t) or {}, '__ipairs'). At the same time, LCS didn't propagate metavalues to subclasses, and so it quietly relied on non-rawget access to __ipairs to work.

I thought it was an error in LuaSandbox at first, but I consulted Lua docs, and found out metatable access must be raw. I admit I didn't know of that before, like I didn't know of a use for __mode before that first LCS issue.

By the way, this LCS-free snippet allows to check whether your interpreter has this problem:

function test()
    local base = {}
    base.__index = base
    base.__ipairs = function(self)
        local i = 0
        return function()
            i = i + 1
            local value = self.values[i]
            if value then return i, value; end

    local derived = {}
    derived.__index = base
    setmetatable(derived, base)

    local instance = { values = {100, 20, 3} }
    setmetatable(instance, derived)

    local sum = 0
    for i, v in ipairs(instance) do
        sum = sum + v

    return sum

print(test()) -- if using Scribunto, works only in the debug console

This might look like it should print 123, but it's wrong: because of the rawget requirement, the code should print 0. If it prints 123, watch out.

As an extra note, I found out this line: base.__index = base is needed to actually replicate LCS behavior and reproduce the issue. However, note that it makes base recursive. When debugging the issue, a mistake caused the creation of two simultaneous LuaClassSystem instances with two registries, something I predicted was the issue with the clone bug above (it wasn't; it can only happen if LCS is in two separate modules at the same time). This time it was, and I suspect due to some deep-cloning involved (and LCS' deepCopy not having any recursion protection, and relying on the registry being valid), it overflowed all available memory, crashing the interpreter.

-- Copyright (c) 2012-2014 Roland Yonaba
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.

Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:

    1. The origin of this software must not be misrepresented; you must not
    claim that you wrote the original software. If you use this software
    in a product, an acknowledgment in the product documentation would be
    appreciated but is not required.

    2. Altered source versions must be plainly marked as such, and must not be
    misrepresented as being the original software.

    3. This notice may not be removed or altered from any source

local pairs, ipairs = pairs, ipairs
local assert = assert
local setmetatable, getmetatable = setmetatable, getmetatable
local type = type
local insert = table.insert

-- Internal register
local _registry = {
    class  = setmetatable({}, {__mode = 'k'}),
    object = setmetatable({}, {__mode = 'k'})

-- Checks if thing is a kind or whether an 'object' or 'class'
local function isObject(thing)
    return _registry.object[thing] ~= nil

local function isClass(thing)
    return _registry.class[thing] ~= nil

-- Given an object and a class, checks whether the object is an instance of the
-- class or one of its superclasses.
local function instanceOf(thing, class)
    assert(isObject(thing), 'instanceof: `thing` must be an LCS object')
    assert(isClass(class), 'instanceof: `class` must be an LCS class')
    local thingClass = _registry.object[thing].__superClass
    if class == thingClass then
        return true
    local thingSuperClass = _registry.class[thingClass].__superClass
    while thingSuperClass do
        if class == thingSuperClass then
            return true
            thingSuperClass = _registry.class[thingSuperClass].__superClass
    -- loop terminated = reached a base class and still haven't found one `thing` is an instance of
    return false

-- tostring
local function __tostring(self, ...)

    -- support for tostring as a method is deprecated and only here for backwards compatibility
    -- it will probably be removed in a future release

    if self.tostring then 
        return self:tostring(...)
    if isClass(self) then
        return "LCS class: ?"
    elseif isObject(self) then
        return "LCS object: ?"
    return tostring(self)

-- Base metatable
local baseClassMt = {
    __call = function (self, ...) return self:new(...) end,
    __tostring = __tostring

local Class

-- Simple helper for building a raw copy of a table
-- Only pointers to classes or objects stored as instances are preserved
local function deepCopy(t)
    local r = {}
    for k, v in pairs(t) do
        if type(v) == 'table' then
            if (_registry.class[v] or _registry.object[v]) then 
                r[k] = v
                r[k] = deepCopy(v)
            r[k] = v
    return r

-- Checks for a method in a list of attributes
local function checkForMethod(list)
    for k, attr in pairs(list) do
        assert(type(attr) ~= 'function', 'Cannot assign functions as members')

-- Instantiation
local function instantiateFromClass(self, ...)
    assert(isClass(self), 'Class constructor must be called from a class')
    assert(not _registry.class[self].__abstract, 'Cannot instantiate from abstract class')
    local instance = deepCopy(self)
    _registry.object[instance] = {
        __superClass = self,
    local instance = setmetatable(instance, self)
    if self.init then
        self.init(instance, ...)
    return instance

-- Classes may not override these metavalues.
local restrictedMetavalues = { __index = true, __call = true }

-- Class derivation
local function extendsFromClass(self, extra_params)
    assert(isClass(self), 'Inheritance must be called from a class')
    assert(not _registry.class[self].__final, 'Cannot derive from a final class')
    local class = Class(extra_params)
    class.__index = class
    class.__tostring = __tostring
    _registry.class[class].__superClass = self
    _registry.class[self].__subClass[class] = true
    for k, v in pairs(self) do
    	if type(k) == 'string' and k:find("^__") and not restrictedMetavalues[k] then
    		class[k] = v
    return setmetatable(class, self)

-- Abstract class derivation
local function abstractExtendsFromClass(self, extra_params)
    local c = self:extends(extra_params)
    _registry.class[c].__abstract = true
    return c

-- Final class derivation
local function finalExtendsFromClass(self, extra_params)
    local c = self:extends(extra_params)
    _registry.class[c].__final = true
    return c

-- Super methods call
local function callFromSuperClass(self, f, ...)
    assert(isClass(self) or isObject(self), 'attempted to call :super from an unknown object/class')
    local superClass = getmetatable(self)
    if not superClass then return nil end
    local super
    if isClass(self) then
        super = superClass
    else -- must be an object due to the assert above
        assert(isClass(superClass), 'attempted to call :super with an object that has an unknown class')
        super = _registry.class[superClass].__superClass
    local s = self
    while s[f] == super[f] do
        s = super
        super = _registry.class[super].__superClass

    -- If the superclass also has a superclass, temporarily set :super to call THAT superclass' methods
    local supersSuper = _registry.class[super].__superClass
    if supersSuper then
        _registry.class[superClass].__superClass = supersSuper

    local method = super[f]
    local result = method(self, ...)

    -- And set the superclass back, if necessary
    if supersSuper then
        _registry.class[superClass].__superClass = super
    return result

-- Gets the superclass
local function getSuperClass(self)
    local super = getmetatable(self)
    return (super ~= baseClassMt and super or nil)

-- Gets the subclasses
local function getSubClasses(self)
    assert(isClass(self), 'getSubClasses() must be called from class')
    return _registry.class[self].__subClass or {}

-- Class creation
Class = function(members)
    if members then checkForMethod(members) end
    local newClass = members and deepCopy(members) or {}                              -- includes class variables
    newClass.__index = newClass                                                        -- prepares class for inheritance
    _registry.class[newClass] = {                                                      -- builds information for internal handling
        __abstract = false,
        __final = false,
        __superClass = false,
        -- Superclasses have no logical dependency on their subclasses.
        __subClass = setmetatable({}, {__mode = 'k'}),

    newClass.new = instantiateFromClass                                                -- class instanciation
    newClass.extends = extendsFromClass                                                -- class derivation
    newClass.abstractExtends = abstractExtendsFromClass                                -- abstract class deriviation
    newClass.finalExtends = finalExtendsFromClass                                      -- final class deriviation
    newClass.__call = baseClassMt.__call                                               -- shortcut for instantiation with class() call
    newClass.super = callFromSuperClass                                                -- super method calls handling
    newClass.getClass = getSuperClass                                                  -- gets the superclass
    newClass.getSubClasses = getSubClasses                                             -- gets the subclasses
    newClass.__tostring = __tostring                                                   -- tostring

    return setmetatable(newClass, baseClassMt)

-- Static classes
local function abstractClass(members)
    local class = Class(members)
    _registry.class[class].__abstract = true
    return class

-- Final classes
local function finalClass(members)
    local class = Class(members)
    _registry.class[class].__final = true
    return class

-- These functions are exported for debugging.
local debug = {}

-- Given a table `which`, returns `true`/`false` for whether `which` is a known LCS object.
-- If given something that is not a table, produces an error. As this is a debug function,
-- it checks for errors more extensively.
function debug.isKnownObject(which)
    assert(type(which) == 'table', 'debug.isKnownObject not given a table. This should never happen.')
    return isObject(which)

-- Given a table `which`, returns `true`/`false` for whether `which` is a known LCS class.
-- If given something that is not a table, produces an error. As this is a debug function,
-- it checks for errors more extensively.
function debug.isKnownClass(which)
    assert(type(which) == 'table', 'debug.isKnownClass not given a table. This should never happen.')
    return isClass(which)

-- Stands for "e*x*tended type". Like the built-in `type`, returns a string for
-- the type of the given object. If the value `which` is a known object or a
-- class, returns 'object' or 'class' respectively, otherwise defers to `type`.
local function xtype(which)
    if isObject(which) then
        return 'object'
    elseif isClass(which) then
        return 'class'
        return type(which)

-- Returns utilities packed in a table (in order to avoid polluting the global environment)
return {
    _VERSION = "2.1.1",
    class = setmetatable(
            abstract = abstractClass,
            final = finalClass
            __call = function(self, ...) return Class(...) end
    -- custom LCS patches come below
    deepCopy = deepCopy,
    instanceOf = instanceOf,
    xtype = xtype,
    isObject = isObject,
    isClass = isClass,
    debug = debug,