Skip to content

Advanced Features & Interop

This section covers Kotlin's null-safety, operator overloading, infix functions, scope functions, collections, and specific interop features for environments like Dota 2.

Null Safety

One of the most powerful features of Kotlin is its null-safety. ktox-lua transpiles Kotlin's null-safety operators (?., ?:, !!) into robust, multi-step Lua checks.

Safe Call Operator (?.)

Each step in a safe call chain is transpiled into a do...end block with temporary variables to ensure the receiver is evaluated only once.

Kotlin:

val result = calc?.add(3, 7)

Lua:

local result
do
    local __t0 = calc
    local __t1 = (__t0 ~= nil) and __t0:add(3, 7) or nil
    result = __t1
end

Example of a longer chain: Kotlin:

val result = aaa?.bbb?.replace("a", "b") ?: "nothing!"

Lua:

local result
do
    local __t0 = aaa
    local __t1 = (__t0 ~= nil) and __t0.bbb or nil
    local __t2 = (__t1 ~= nil) and ktox_replace(__t1, "a", "b") or nil
    if __t2 ~= nil then
        result = __t2
    else
        result = "nothing!"
    end
end

Elvis Operator (?:)

The Elvis operator provides a default value if the preceding expression evaluated to nil.

Kotlin:

val result = calc?.add(3, 7) ?: -1

Lua:

local result
do
    local __t0 = calc
    local __t1 = (__t0 ~= nil) and __t0:add(3, 7) or nil
    if __t1 ~= nil then
        result = __t1
    else
        result = -1
    end
end

Not-Null Assertion (!!)

Kotlin's !! operator is supported by direct access.

Kotlin:

val result = calc!!.add(3, 7)

Lua:

local result = calc:add(3, 7)

Smart Casts and is checks

ktox-lua supports type checks using the is and !is operators.

Kotlin:

if (animal is Dog) {
    animal.bark() // Smart cast
}

if (name is String) {
    println(name.length)
}

Lua:

if ktox_isinstance(animal, Dog) then
    animal:bark()
end

if type(name) == "string" then
    print(#name)
end
(Note: ktox_isinstance is a helper function provided by ktox-lib.lua for user-defined classes, while primitive types use Lua's built-in type() function.)

Operators & Infix Functions

ktox-lua supports Kotlin's operator overloading and infix functions, mapping them to Lua's metamethods or helper functions.

Operator Overloading

Many Kotlin operators are transpiled directly to Lua's metamethods.

Kotlin Operator Lua Metamethod Method Name
a + b __add plus
a - b __sub minus
a * b __mul times
a / b __div div
-a __unm unaryMinus
a == b __eq equals
a < b, a <= b __lt, __le compareTo
a(args) __call invoke
toString() __tostring toString

Note: Operators without direct Lua metamethods (like inc, dec, get, set, contains) are transpiled into explicit method calls or ktox_ helper calls.

Example:

class Vec2(val x: Int, val y: Int) {
    operator fun plus(other: Vec2): Vec2 = Vec2(this.x + other.x, this.y + other.y)
    override fun toString(): String = "(${this.x}, ${this.y})"
}

val a = Vec2(3, 4)
val b = Vec2(1, 2)
val c = a + b
println(c)

Lua:

function Vec2:plus(other)
    return Vec2:new(self.x + other.x, self.y + other.y)
end
Vec2.__add = function(a, b) return a:plus(b) end

function Vec2:toString()
    return "(" .. tostring(self.x) .. ", " .. tostring(self.y) .. ")"
end
Vec2.__tostring = function(a) return a:toString() end

Extension Functions

Extension functions are transpiled to top-level functions (for primitive types) or class methods (for user-defined classes).

Kotlin (Top-level):

fun String.shout() = this.uppercase() + "!"

Lua:

function string_shout(self)
    return ktox_uppercase(self) .. "!"
end

Kotlin (Class extension):

class User(val name: String)
fun User.greet() = "Hello, $name"

Lua:

function User:greet()
    return "Hello, " .. tostring(self.name)
end

Compound Assignments

Compound assignments (+=, -=, etc.) for collections are transpiled to ktox_ helper calls to ensure correct mutations.

Kotlin:

list += "item"

Lua:

list = ktox_plusAssign(list, "item")

Collections & Iterables

ktox-lua provides a comprehensive suite of collection functions transpiled into efficient Lua helpers in ktox-lib.lua. Kotlin lists and maps are both represented as Lua tables.

Lists and Maps

Kotlin:

val numbers = listOf(1, 2, 3)
val capitals = mapOf("France" to "Paris")

Lua:

local numbers = {1, 2, 3}
local capitals = {France = "Paris"}

Common Operations

Operation Kotlin Example Lua Transpilation
Iteration numbers.forEach { ... } ktox_forEach(numbers, ...)
Transformation numbers.map { ... } ktox_map(numbers, ...)
list.flatten() ktox_flatten(list)
Filtering numbers.filter { ... } ktox_filter(numbers, ...)
Search numbers.find { ... } ktox_find(numbers, ...)
Predicates numbers.any { ... } ktox_any(numbers, ...)
Sorting numbers.sortedBy { it.age } ktox_sortedBy(numbers, ...)
Aggregation numbers.fold(0) { ... } ktox_fold(numbers, ...)
numbers.joinToString(", ") ktox_joinToString(numbers, ", ")
Properties list.size #list
string.length #string
map.keys ktox_keys(map)
pair.first pair[1]

Scope Functions

Kotlin's scope functions (let, run, with, apply, and also) are implemented as Lua helpers.

Function Receiver Reference Return Value Transpiled Helper
let it Lambda result ktox_let
run this Lambda result ktox_run
with this Lambda result ktox_with
apply this Receiver object ktox_apply
also it Receiver object ktox_also

Example (apply):

val user = User().apply {
    name = "Alice"
    age = 25
}

Lua:

local user = ktox_apply(User:new(), function(self)
    self.name = "Alice"
    self.age = 25
end)

Exceptions

Kotlin's exception handling uses throw (Lua error()) and try/catch/finally (Lua pcall()/xpcall()).

Kotlin:

try {
    throw RuntimeException("fail")
} catch (e: Exception) {
    println("Caught: $e")
}

Lua:

local __ok, __err = pcall(function()
    error(RuntimeException:new("fail"))
end)
if not __ok then
    local e = __err
    print("Caught: " .. tostring(e))
end

Annotations & Interop

ktox annotations map idiomatic Kotlin declarations onto an existing native API — typically declared once in a types library and consumed as a jar, so application code stays pure Kotlin.

@NativeName

Maps a declaration to its native name. Where the annotation sits decides the emitted SHAPE:

Function rename — calls emit the native name:

@NativeName("FindUnitsInRadius")
fun findUnitsInRadius(origin: Vector, radius: Float): List<BaseNPC> = externalSource()

Accessor property (@get: / @set:) — reads and writes become native METHOD CALLS:

class BaseNPC {
    @get:NativeName("GetHealth")
    @set:NativeName("SetHealth")
    var health: Int = 0
}
local hp = unit:GetHealth()
unit:SetHealth(hp + 50)
unit:SetHealth(unit:GetHealth() + 50)  -- compound assignment reads then writes

Bare property rename — a plain @NativeName on a property is a FIELD rename (reads and writes stay field accesses); a dotted name (@NativeName("style.clip")) renames onto a nested path. Mixing is fine — each side resolves independently (@get:NativeName("GetMana") + @NativeName("mana_field") reads via the call, writes the field).

Resolution is receiver-type-aware: the annotation is looked up on the RECEIVER's type (source or compiled jar, following supertypes), so two types can map the same Kotlin name to different native names, and a plain data class field never borrows an unrelated type's accessor. Enum classes and entries rename the same way.

External data classes

Constructing a data class that exists ONLY in a types jar (no transpiled output — e.g. an engine event payload) emits a plain table, because there is no Lua class to instantiate:

send(LinkPayload(link = "item_blink", nav = true, shop = 1))
send({ link = "item_blink", nav = true, shop = 1 })

Data classes that ARE part of the program (your own modules, shared modules) construct normally.

Value classes

A @JvmInline value class is an identity at runtime — construction and .value reads vanish:

@JvmInline value class PlayerID(val value: Int)

val id = PlayerID(3)
val raw = id.value
local id = 3
local raw = id

@ReplaceReferencesWithLiteral

Replaces every reference to a property/function with a literal string. Useful for event names or dictionary keys.

Source Maps

ktox-lua generates source maps for easier debugging. When an error occurs in Lua, the traceback is rewritten to point to the original Kotlin source file and line number.