Skip to main content

SR Lua Extensions

Operators

Alternate tokens

As they're more familiar to some users, the following tokens have alternatives:

andornot~=
&&||!!=

However do note that Lua not is not equivalent to usual implementations of ! and will consider 0 to be true.

Compound assignments .=

A much missed feature from other languages is the compound assignment which avoids repetitive code like x = x + 1. SR Lua supports +=, -=, *=, /=, ^=, ..=, but not -- or ++ since -- would clash with the comment style.

local x = 1
local tbl = { [2] = 3 }
tbl[x+1] += 1
print(tbl[x+1]) -- Prints "4"

Unary plus +x

Unary plus as a no-op operator is implemented in SR Lua, primarily for stylistic reasons.

local POINTS = {
vec3(-2, +3, +4),
vec3(-2, -3, +4),
vec3(-2, +3, -4),
}

Ternary operator c?x:y

Although standard Lua offers x and y or z as an alternative to a real ternary operator, after a long development period we've noticed that this caused a lot of bugs where the condition could be met, but y would evaluate to false, which would lead to the result being z.

As a result, SR Lua implements a ternary operator with the syntax x ? y : z, you can see the problems with the old syntax demonstrated below.

local t1 = { x =  true, xv = false }
local t2 = { xv = 4 }
print(t1.x ? t1.xv : t2.xv) -- False
caution

As this leads to ambigious lexing in cases like x?a:b():y, the : token has to be preceded by a non-identifier character such as whitespace, solving this problem in the form of x ? a:b() : y.

Optional chaining operator ?.

As they make working with multiple nested table? types much more pleasant, SR Lua also implements the optional chaining operator.

local x = { a = { b = 5 } }
print(x?.a?.b) -- 5
print(x?.b?.b) -- nil
print(y?.a?.b) -- nil

Constructs

Initializing conditionals

Lua has three kinds of statements expecting a conditional:

  • repeat <> until [cond]
  • while [cond] do <> end
  • if [cond] then <> end

We extend these statements to additionally allow local assignments in one of the following forms:

  • x = y, where x gets assigned y and also evaluated as the conditional.
  • x = y; z, where x gets assigned y and z gets evaluated as the conditional.

This leads to less repetitive code and better performance due to less table lookups.

local friend = { name = "Hello" }
if name = friend.name then
print(name)
end
if name = friend.name; name == "Hello" then
print(name)
end

Switch-case

Switch case statements help tremendously with long lists of comparsion and are fully supported by SR Lua. A minor caveat is that the default block must be at the end.

switch x do
case 1 then
print("a")
case 5 then
print("b")
case 3*10 then
print("c")
break
default
print("d")
end

Loop constructs

As an alternative to while true do, we implement a shorthand construct called loop.

local function printIota(x)
loop
print(x)
x += 1
end
end

Continue keyword

Continue statements allow fine control in loops and they are fully implemented in our framework.

local c = 0
for i=0,10 do
if i % 2 != 1 then continue end
c += 1
if c == 1 then continue end
print(i) -- Prints "3,5,7,9"
end

Short lambda syntax

The function(...) return ... end syntax gets repetitive real quick especially when writing functional code, as a solution SR Lua implements a new syntax for declaring lambdas.

  • |<A>| <B> will be parsed equivalently to function(<A>) return <B> end
  • |<A>| do <B> end will be parsed equivalently to function(<A>) <B> end
local function iota(max, fn)
return |max,i| do
while i < max do
i += 1
if fn(i) then
return i
end
end
end, max, 0
end

for i in iota(10, |i| i % 2 == 1) do
print(i) -- Prints "1,3,5,7,9"
end

Unit bound numerals

Throughout the engine we consistently use three units, radians for angles, centimeters for distance and milliseconds for time. This sometimes causes ugly conversions following a magic number, or even worse calls to math.rad and potentially degrading performance.

In order to solve this problem SR Lua allows numeric constants to be suffixed with units that convert to the basic units we use, which leads to easy to read and even easier to write code.

  • cm, m and km convert to cm.
  • ms, s and min convert to ms.
  • rad and deg convert to rad.

You can also suffix it with 2 to indicate squared and 3 for cubed.

print(1rad, 1deg)          -- 1       0.017453292519943
print(150cm, 1.5m, 1.5km) -- 150 150 150000
print(10ms, .01s, 1.2min) -- 10 10 72000
print(1m) -- 100
print(1m2) -- 10000

Function decorators

SR Lua implements a special syntax for function and lambda statements that calls into another function with the function as its only argument. This construct allows us to implement special syntax such as async function.

For lambdas the decorator is allowed to be any statement (e.g. util.decorator) whereas with named declarations using local function / function, the decorator has to be a single name.

  • <D> |_| do end will be parsed equivalently to <D>(|_| do end)
  • <D> function() end will be parsed equivalently to <D>(function() end)
  • d function f() end will be parsed equivalently to f = d(function() end)
  • local d function f() end will be parsed equivalently to local f = d(function() end)

An alternate use of this syntax is calling into functions that only take a callback with no parentheses.

-- Define a decorator which always doubles the return value.
--
local double = |f| |...| 2*f(...)
local test = { double = double }

-- Closure syntax.
--
local a = test.double function(y)
return y * 0.5
end
b = test.double |y| y * 0.5

-- Declaration syntax.
--
local double function c(y)
return y * 0.5
end
double function d(y)
return y * 0.5
end

print(a(1), b(2), c(3), d(4)) -- Prints "1 2 3 4"
tip

Since it gives the appearance of a keyword, we generally prefer lowercase names with no special characters for decorator names. async, generator, etc.

Try/Catch blocks

SR Lua implements a familiar try/catch statement instead of bothering the user with the Lua library calls of pcall/xpcall.

The statement starts with a try ..., followed by either catch ... end or catch(<exception var>) ... end. We also offer throw as an alias for error. Although do note that despite the first-class support for exceptions, they remain slow and should be avoided where possible.

try
throw "Oopps"
catch(ex)
print("failed:", ex) -- Prints 'failed: [string ""]: Oopps'
end

Classes

For object oriented code, we recommend usage of the Class utilities in Lua code, which is cross-compatible with TypeScript code.

Prototype  Class.new(Prototype? superClass, string? name)
bool Class.InstanceOf(any object, Prototype class)
any Prototype.new(...)

Constructors can be assigned by setting the ctor function in the unique Prototype object returned and metatable functions can be assigned similarly.

local MyClass = Class.new(nil, "MyClass")
function MyClass:ctor(x)
self.x = x
print(("new MyClass(%s)"):format(x))
end
function MyClass:__call(y)
print(self.x * y)
end

local instance = MyClass.new(6) -- new MyClass(6)
instance(9) -- 54

Defining a class

LuaExport/index.lua
-- Exported class.
--
LuaClass = Class.new(nil, "LuaClass")
function LuaClass:ctor(x)
self.x = x
print(("Created LuaClass(%s)"):format(x))
end

function LuaClass:getX()
return self.x
end

Importing a class

LuaTest/index.lua
--[[
Prints:
Created LuaTest()
Created TSClass(hey)
hey hey true
]]
Event.OnInit |_| do
local LuaTest = Class.new(@TSExport.TSClass, "LuaTest")
function LuaTest:ctor()
print("Created LuaTest()")
LuaTest.super(self, "hey")
end

local test = LuaTest.new()
print(@TSExport.getY(test), test.y, Class.InstanceOf(test, @TSExport.TSClass))
end

Async

SR Lua has first-class promise and asynchronous code support, fully interoperable with its TypeScript counterparts.

Promise Type

The promise object is very similar to it's JavaScript equivalent, which can be constructed using Promise.new. The inner function takes two callbacks, one to resolve the promise and another to reject it. If the function given throws, the error will propagate to the rejection callback. If the result is a promise itself, then the result is propagated.

For utility reasons, there's additionally Promise.Resolve and Promise.Reject which create a promise and immediately resolve/reject it.

Promise<T> Promise.new(function<function<T res> onResolve, function<any err> onReject>)
Promise<T> Promise.resolve(T res)
Promise Promise.reject(any err)

Multiple promises can be merged using All, Any, Race, AllSettled functions, equivalent to it's JavaScript counterparts. For more information you can read Mozilla documentations for each function.

Promise    Promise.All(Iterable<Promise>)
Promise Promise.AllSettled(Iterable<Promise>)
Promise Promise.Any(Iterable<Promise>)
Promise Promise.Race(Iterable<Promise>)

Callbacks for each stage of the promise can be registered using Promise.Then, Promise.Catch and Promise.Finally. Finally does not create a new promise however the rest will change the return type to be equivalent to the return of each of the callbacks.

Promise    Promise<T>:Then(function<T res>? onResolve, function<any err>? onReject)
Promise Promise<T>:Catch(function<any err> onReject)
Promise<T> Promise<T>:Finally(function<> onSettle)

Async Functions

Just like in JavaScript, you can use the async decorator to make a function asynchronous.

-- Decorates an async function, returns a promise and can await promise-like objects.
--
function async(function fn)

Inside asynchronous functions, you are allowed to use $await which will await the result of a given promise-like type (if not, returns as is), and throw on rejection.

-- Yields the function, resumes execution after the specified promise is finalized, throws if it was rejected. If a promise is not given, returns the value as is.
--
T $await<T>(PromiseLike<T> promiseLike)

An important distinction to make here is that await works with any promise-like type and not just the Promise, which allows you to create your own awaitable types. the object is reqired to have a then function that ignores the first argument for the sake of TypeScript compatibility and behaves like Then for the rest.

any PromiseLike:then(any _, function? onResolve, function? onReject)

This allows you to create constructs such as a transactions which can be very useful, for instance, all Event types are awaitable.

Example
-- Declare a function that resolves a promise once Engine.Delay completes.
--
local function sleepFor(time)
return Promise.new(|res, rej| Engine.Delay(|_| res(4), time))
end

async function asyncTest(q)
local x = $await(sleepFor(1s))
print("1s passed", x)
local tick = $await(Event.OnTick)
print("asyncTest ->", q, tick)
return 6
end

local promise = asyncTest(9)
promise:Then(|x| print("Result:", x, Class.InstanceOf(promise, Promise)))

--[[
Prints:
1s passed 4
asyncTest -> 9 18
Result: 6 true
]]

Miscellaneous

Extended identifier set @/$

SR Lua allows @ and $ to be used in identifiers such as variable or function names, although this is mainly implemented for the sake of clarifying the special @ tables which will be explained in the Environment section.

local @x = 3
local $x = 4
print(@x, $x) -- "3, 4"

Extended type query xtype

You can use xtype(value) instead of type(value) when you want to know the exact type of userdata where possible. The function will fallback to the result of type if not relevant, however do note that this is much slower and should not be the go-to in all cases.

Global caching

All global engine namespaces and standard lua interafces like math are cached by default, so you don't need redundant headers in your code in the form of local vec3 = vec3.

Library extensions

-- Returns the argument if non-nil or a read-only empty table avoiding allocation.
--
table optional(table? x)

-- Returns true if the string containts the substring.
--
bool string.contains(string str, string substr)

-- Returns true if the string starts with the substring.
--
bool string.startsWith(string str, string substr)

-- Returns true if the string ends with the substring.
--
bool string.endsWith(string str, string substr)

-- Adds properties so that `table.a | table.a = 3` tries calling `table:get_a() | table:set_a(3)` if it exists.
-- Note that this function should be called only once and after all properties are declared, and it will strip the property functions.
--
void table.addproperties(table)

-- Returns true if the given value is an array.
--
bool array.is(any? x)

-- Collects a stateful iterator into an array.
--
T[] array.collect<T>(Iterable<T> it)

-- Clears a table.
--
void table.clear(table)

-- Clones the given table, if deep is not specified default to shallow.
--
table table.clone(table?, bool? deep)
table array.clone(table?, bool? deep)

-- Maps each value according to the given function and returns the result.
--
table array.map(table, function<any v> cb)
table table.map(table, function<any k, any v> cb)

-- Filters entries in an table according to the given function.
--
table array.filter(table, function<any v> filter)
table table.filter(table, function<any k,any v> filter)

-- Finds the minimum or maximum of the array, if callback is not given compares elements using __lt.
--
any? array.max(table?, function<any a,any b>? lessThen)
any? array.min(table?, function<any a,any b>? lessThen)

-- Find an element in the array where the callback returns true.
--
any? array.find(table|nil t, function<any v> filter)

-- Enumerates the table.
--
void table.each(table|nil t, function<any k,any v> cb)
void array.each(table|nil t, function<any v> cb)

-- Reverses the array.
--
table array.reverse(table)

-- Reduces the array.
--
any array.reduce(table, function<any v> cb, any? init)

-- Converts the table to string.
--
table table.tostring(table?, function<any x>? tostring)
table array.tostring(table?, function<any x>? tostring)

-- Pretty prints a long table.
--
void table.prettyprint(table, function<any x>? tostring)

-- Removes an element in an unordered way by swapping with the last.
--
void array.removeu(table, int index)

-- Erases every element where the enumerator returns true, returns the number of elements erased.
--
int table.eraseif(table, function<any k, any v> filter)
int array.eraseif(table, function<any v> filter)
int array.eraseifu(table, function<any v> filter)

-- Merges table contents shallowly from src to dst, returns dst.
--
table table.merge(table dst, table src)