SR Lua Extensions
Operators
Alternate tokens
As they're more familiar to some users, the following tokens have alternatives:
and | or | not | ~= |
---|---|---|---|
&& | || | ! | != |
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.
- SR Lua
- Lua
local x = 1
local tbl = { [2] = 3 }
tbl[x+1] += 1
print(tbl[x+1]) -- Prints "4"
local x = 1
local tbl = { [2] = 3 }
local tmp = x+1
tbl[tmp] = tbl[tmp] + 1
print(tbl[tmp]) -- Prints "4"
Unary plus +x
Unary plus as a no-op operator is implemented in SR Lua, primarily for stylistic reasons.
- SR Lua
- Lua
local POINTS = {
vec3(-2, +3, +4),
vec3(-2, -3, +4),
vec3(-2, +3, -4),
}
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.
- SR Lua
- Lua
local t1 = { x = true, xv = false }
local t2 = { xv = 4 }
print(t1.x ? t1.xv : t2.xv) -- False
local t1 = { x = true, xv = false }
local t2 = { xv = 4 }
print(t1.x and t1.xv or t2.xv) -- 4, bug!
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.
- SR Lua
- Lua
local x = { a = { b = 5 } }
print(x?.a?.b) -- 5
print(x?.b?.b) -- nil
print(y?.a?.b) -- nil
local x = { a = { b = 5 } }
local t1 = x and x.a
print(t1 and t2.b) -- 5
local t2 = x and x.b
print(t2 and t2.b) -- nil
local t3 = y and y.a
print(t3 and t3.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.
- SR Lua
- Lua
local friend = { name = "Hello" }
if name = friend.name then
print(name)
end
if name = friend.name; name == "Hello" then
print(name)
end
local friend = { name = "Hello" }
if friend.name then
print(friend.name)
end
if friend.name == "Hello" then
print(friend.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.
- SR Lua
- Lua
switch x do
case 1 then
print("a")
case 5 then
print("b")
case 3*10 then
print("c")
break
default
print("d")
end
if x == 1 then
print("a")
print("b")
print("c")
elseif x == 5 then
print("b")
print("c")
elseif x == 3*10 then
print("c")
else
print("d")
end
Loop constructs
As an alternative to while true do
, we implement a shorthand construct called loop
.
- SR Lua
- Lua
local function printIota(x)
loop
print(x)
x += 1
end
end
local function printIota(x)
while true do
print(x)
x = x + 1
end
end
Continue keyword
Continue statements allow fine control in loops and they are fully implemented in our framework.
- SR Lua
- Lua
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
local c = 0
for i=0,10 do
if i % 2 == 1 then
c = c + 1
if c ~= 1 then
print(i) -- Prints "3,5,7,9"
end
end
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 tofunction(<A>) return <B> end
|<A>| do <B> end
will be parsed equivalently tofunction(<A>) <B> end
- SR Lua
- Lua
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
local function iota(max, fn)
return function(max,i)
while i < max do
i = i + 1
if fn(i) then
return i
end
end
end, max, 0
end
for i in iota(10, function(i) return i % 2 == 1 end) 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
andkm
convert tocm
.ms
,s
andmin
convert toms
.rad
anddeg
convert torad
.
You can also suffix it with 2
to indicate squared and 3
for cubed.
- SR Lua
- Lua
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
print(1, math.rad(1)) -- 1 0.017453292519943
print(150, 1.5*100, 1.5*100*1000) -- 150 150 150000
print(10, .01*1000, 1.2*1000*60) -- 10 10 72000
print(100) -- 100
print(100*100) -- 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 tof = d(function() end)
local d function f() end
will be parsed equivalently tolocal f = d(function() end)
An alternate use of this syntax is calling into functions that only take a callback with no parentheses.
- SR Lua
- Lua
-- 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"
-- Define a decorator which always doubles the return value.
--
local function double(f) return function(...) return 2*f(...) end end
local test = { double = double }
-- Closure syntax.
--
local a = test.double(function(y)
return y * 0.5
end)
b = test.double(function(y) return y * 0.5 end)
-- Can't use declaration syntax.
--
local c = double(function(y)
return y * 0.5
end)
d = double(function(y)
return y * 0.5
end)
print(a(1), b(2), c(3), d(4)) -- Prints "1 2 3 4"
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.
- SR Lua
- Lua
try
throw "Oopps"
catch(ex)
print("failed:", ex) -- Prints 'failed: [string ""]: Oopps'
end
xpcall(function()
error "Ooops"
end, function(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
- SR Lua
- TypeScript
-- 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
// Exported class.
//
export class TSClass<T> {
y: T;
constructor(y: T) {
print(`Created TSClass(${y})`);
this.y = y;
}
getY(): T {
return this.y;
}
}
Importing a class
- SR Lua
- TypeScript
--[[
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
declare class LuaClass<T> {
x: T;
constructor(x: T);
getX(): T;
}
import { LuaClass } from "@LuaExport";
/*
Prints:
Created TSTest()
Created LuaClass(512)
512 512 true
*/
class TSTest extends LuaClass<number> {
constructor() {
print("Created TSTest()");
super(512);
}
}
const test = new TSTest();
print(test.getX(), test.x, test instanceof LuaClass);
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.
All
Promise.allAllSettled
Promise.allSettledAny
Promise.anyRace
Promise.race
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.
-- 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)