This document uses Lua's variation of the notation for Extended Backus-Naur Form (EBNF).
Lilia guarantees support for Lua 5.1, 5.2, and 5.3. That means you can use any feature from those languages in Lilia safely.
Lilia is a free-form language. It ignores whitespace (including newlines!) and comments between lexical elements (tokens), except as delimiters between names and keywords.
Names (also called identifiers) in Lilia can be any string of letters, digits, and underscores, not beginning with a digit and not being a reserved word.
The following keywords are reserved by Lilia and cannot be used as names:
and as* break do else elseif false for function goto if in let*
match* nil not or repeat return true until while with*
Keywords marked with an asterisk (*) are additional keywords by Lilia.
The following keywords that are not included in the above list are reserved by Lua and cannot be used as names.
end
local
then
Lilia is a case-sensitive language: and is a reserved word, but And and AND are
two different, valid names. As a convention, programs should avoid creating
names that start with an underscore followed by one or more uppercase letters
(such as _VERSION
).
The following strings denote other tokens:
+ - * / % ^ # & ~ | << >> // == ~= <= >= < > = ( ) { }
[ ] :: ; : , . .. ... |>
Literal strings are almost the same as Lua, with some modifcations.
Long strings use triple quotes """
instead of double square brackets
[[]]
.
let lorem = """
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
"""
Nested long strings has additional equals signs.
let nested = """===
""""
"""
woo
"""
""""
==="""
Strings can be interpolated. Lilia uses the ${...}
notation (similar to
JavaScript.) These expressions are expanded on compile time.
let world = "World"
let hello_world = "Hello ${world}"
local world = "World"
local hello_world = "Hello " .. tostring(world)
String interpolation is only supported on double-quoted strings, and requires
Lua's tostring
function.
Numeric constants are also the same as in Lua, with several additions.
Lilia supports octal (0o
) and binary (0b
) representations. These are
compiled to equivalent hex numericals.
let file_perms = 0o777
let one_o_one = 0b101010
local file_perms = 0x1FF
local one_o_one = 0x2A
Lilia allows underscores within numericals. This allows you to group digits for easy readability.
let one_million = 1_000_000
let one_billion = 1_000_000_000
let license_key = 0xA_FAAC_ED_0F4
local one_million = 1000000
local one_billion = 1000000000
local license_key = 0xAFAACED0F4
Comments are exactly the same as with Lua. Lilia supports short comments and long comments. These are stored in the AST and is preserved during compilation.
Variables in Lilia are similar to Lua.
var ::= Name
A block is a list of statements, which are executed sequentially:
block ::= {stat}
Lilia has empty statements that allow you to separate statements with semicolons.
stat ::= ‘;’
Statements parsing is similar to Lua. This includes some minor corner-cases.
The following code:
a = b + c
(print or io.write)('done')
is parsed as:
a = b + c(print or io.write)('done')
This is because Lilia interprets the open parenthesis as the start of the arugments to a call. To avoid this ambiguity, it is a good practice to always precede with a semicolon statements that start with a parenthesis:
a = b + c
;(print or io.write)('done')
A block can be explicity delimited to produce a single statement:
stat ::= ‘{’ block ‘}’
Chunks in Lilia are similar to Lua. Syntactically, they are equivalent to blocks.
chunk ::= block
Lilia allows multiple assignment. These are similar to Lua and won't be described here.
stat ::= varlist ‘=’ explist
varlist ::= var {‘,’ var}
explist ::= exp {‘,’ exp}
Lilia supports standard Lua control structures with minor additions.
stat ::= while exp ‘{’ block ‘}’
stat ::= repeat ‘{’ block ‘}’ until exp
stat ::= if exp ‘{’ block ‘}’
In Lilia, control structures are scoped using curly braces ({}
).
Lilia also supports additional control structures that are compiled to Lua.
Match statements compare the result of an expression to several cases. In a way, they're almost similar to switch statements in mainstream languages though with more power. Trailing commas are optional.
stat ::= match [var in] exp ‘{’ {matchlabels matchsep} ‘}’
matchlabels ::= case exp do block
| default do block
matchsep ::= ‘,’
match key in get_key() {
case "arrow_up" do move_jump(),
case "arrow_down" do move_crouch(),
case "arrow_left" do move_left(),
case "arrow_right" do move_right(),
default do
move_none()
log("invalid key: ${key}")
}
A goto statement transfers the program control to a label.
stat ::= goto Name
stat ::= label
label ::= ‘::’ Name ‘::’
The behavior of a goto statement is equivalent to Lua. Note that gotos are only available in Lua 5.3 or newer.
A break statement is equivalent to Lua
stat ::= break
Similar behavior to Lua.
stat ::= for Name ‘=’ exp ‘,’ exp [‘,’ exp] ‘{’ block ‘}’
stat ::= for namelist in explist ‘{’ block ‘}’
namelist ::= Name {‘,’ Name}
Similar to Python's and MetaLua's with
statement. This has no relation to
Moonscript's with statements. Note that
Required functions:
pcall
assert
stat ::= with Name in exp as Name ‘{’ block ‘}’
let len
with file in assert(io.open('test.txt', 'r')) as File {
len = #(file:read'*a')
}
local len
do
local function _clause_0()
return assert(io.open('test.txt', 'r'))
end
local function _body_0()
len = #(file:read'*a')
end
do
local err, file = pcall(_clause_0)
if err ~= nil then
File.exit(file)
else
File.enter(file)
pcall(_body_0)
File.exit(file)
end
end
end
There are some syntactical limitations.
return
halts code execution within thewith
block. It does not return outsidewith
.break
does not break the outer loop.
Also there might be some side-effects when coroutines are used.
A variation that acts more like a concise assignment plus scoping.
stat ::= with namelist in exp ‘{’ block ‘}’
with point in get_point() {
point.x += 2
point.y = 10
}
do
local point = get_point()
do
point.x = point.x + 2
point.y = 10
end
end
stat ::= functioncall
Local variables are declared using let
.
stat ::= let namelist [‘=’ explist]
Lilia supports destructuring assignments.
stat ::= dst ‘=’ var
stat ::= let dst ‘=’ var
dst ::= ‘\{’ [dstfieldlist] ‘}’
dstfieldlist ::= field {fieldsep field} [fieldsep]
dstfield ::= ‘{’ exp ‘}’ ‘:’ exp | Name ‘:’ exp | exp
dstindex ::= Name | dstprefixexp ‘[’ exp ‘]’
| dstprefixexp ‘.’ Name | ‘[’ exp ‘]’
dstprefixexp ::= dstindex
let point = [x: 0, y: 3]
let \{x, y} = point
let deep = [a: [b: c: [d: 'hello!']], e: 'hi!']
let hello, hi
\{hello: a['b'].c['d'], hi: e} = deep
local point = {x = 0, y = 0}
local x, y
do
x = point.x
y = point.y
end
local deep = {a: {b: c: {d: 'hello!'}}, e: 'hi!'}
local hello, hi
do
hello = deep.a['b'].c['d']
hi = deep['e']
end
Note: values are accessed twice. Make sure to take this into account when writing code that depends on the number of times a variable is accessed.
let x = 3
x += 5 * 10
let point = {x = 0, y = 0}
point.x *= 2
local x = 3
x = x + (5 * 10)
local point = {x = 0, y = 0}
point.x = point.x * (2)
exp ::= prefixexp
exp ::= nil | false | true
exp ::= Numeral
exp ::= LiteralString
exp ::= functiondef
exp ::= tableconstructor
exp ::= ‘...’
exp ::= exp binop exp
exp ::= exp funcop Name
exp ::= unop exp
prefixexp ::= var | functioncall | ‘(’ exp ‘)’
Lilia supports the standard Lua arithmetic operators and will not be repeated here.
Lilia supports Lua 5.3 bitwise operators and will compile successfully.
The piping operator is a binary operator takes the previous expression and inserts it into a function.
funcop ::= ‘|>’
This is intended to make filtering-style code easier to read.
let point = [x: 3, y: 2]
"${point.x}, ${point.y}" |> print
point |> unpack |> print
local point = {x = 3, y = 2}
print(point.x .. ", " .. point.y)
print(unpack(point))
Lilia supports standard Lua relational operators with some additions.
Lilia supports !=
as an alias for ~=
. This is intended to make writing on
non-English keyboards easier.
Lilia logical operators are the same as with Lua.
Lilia supports the Lua concatenation operator (..
).
yes.
Operator precedence in Lilia is nearly the same as with Lua.
or
and
< > <= >= ~=/!= ==
|>
|
~
&
<< >>
..
+ -
* / // %
unary operators (not # - ~)
^
Table constructors are expressions that create tables. Similar to Lua with the following differences:
- Uses square brackets
[]
instead of curly braces for delimitation. - Uses colons
:
instead of equals sign for assigning values. - Uses curly braces
{}
for writing arbitrary expressions as keys.
tableconstructor ::= ‘[’ [fieldlist] ‘]’
fieldlist ::= field {fieldsep field} [fieldsep]
field ::= ‘{’ exp ‘}’ ‘:’ exp | Name ‘:’ exp | exp
field ::= field [‘;’ uint]
fieldsep ::= ‘,’
uint ::= D+
D ::= '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
Lilia allows you to use semicolons ;
to repeat an arbitrary expression
multiple times. This is useful for pre-allocating tables.
let zeroes = [0; 10]
let something = [1 + 2; 3, foo: 123, {"key with space in name"}: 456]
let weird = [bar: baz(); 3]
let matrix = [[1; 3]; 5]
local zeroes = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
local something = {1 + 2, 1 + 2, 1 + 2, ["foo"]: 123, ["key with space in name"]: 456}
local weird = {bar = baz(), bar = baz(), bar = baz()}
local matrix = {{1, 1, 1}, {1, 1, 1}, {1, 1, 1}, {1, 1, 1}, {1, 1, 1}}
Note that expressions are repeated and thus calculated multiple times during runtime.
Similar to Lua, though with some modifications.
args ::= ‘(’ [explist] ‘)’
args ::= LiteralString
You can no longer omit parenthesis for table constructor arguments. Permitting it would introduce a syntactic ambiguity:
let b = 1 + a[1]
local b = 1 + a[1]
-- or
local b = 1 + a({1})
Switching the table delimiters from square brackets []
to curly braces {}
would introduce another syntactic ambiguity:
if a == b {
-- do nothing
}
if a == b then
-- do nothing
end
-- or
if a == b({ -- do nothing }) then
end
We can require parenthesis on statements ala C, but then the gain of using a relatively rare syntactic sugar is offsetted by the verboseness of oftenly statements.
Standard function definitions are similar to Lua.
functiondef ::= function funcbody
funcbody ::= ‘(’ [parlist] ‘)’ ‘{’ block ‘}’
stat ::= function funcname funcbody
stat ::= local function Name funcbody
You can also omit the function keyword.
stat ::= local Name funcbody
local hello() {
print "Hello World!"
}
local function hello()
print("Hello World!")
end
Parameters are the same.
parlist ::= namelist [‘,’ ‘...’] | ‘...’
Lilia also supports closure declaration expressions. In the context of Lua, these are simply syntax sugar for functions. Short closures have a single expression statement that is automatically returned.
TODO
closuredef ::=
let add = |x| x + x
let compose = |f, g| |x| f(g(x))
-- edge case:
let wow = x | x | x + x
let zoz = x + | x | x + x
local add = function() return x + x end
local compose = function(f, g)
return function(x)
return f(g(x))
end
end
-- edge case:
local wow = x | x | x + x
local zoz = x + function(x) return x + x end
Long closures have a braced block and can have an arbitrary amount of statements.
let say_name = |name| {
print "Hi, I'm ${name}"
}
local say_name = function (name)
print("Hi, I'm " .. tostring(name))
end
Both types of closures must have at least a single parameter.
Taken from Python. This has no relation with the Decorator Pattern from OOP.
funcbody ::= ‘(’ [parlist] ‘)’ ‘<-’ decoratorlist ‘{’ block ‘}’
decoratorlist ::= ‘[’ Name {decoratorsep Name} [decoratorsep] ‘]’
decoratorsep ::= ‘,’
local makebold(fn) {
return function() {
return "<b>${fn()}</b>"
}
}
local makeitalic(fn) {
return function() {
return "<i>${fn()}</i>"
}
}
local hello() <- [makebold, makeitalic] {
return "hello world"
}
hello() |> print -- <b><i>hello world</i></b>
local function makebold(fn)
return function()
return "<b>${fn()}</b>"
end
end
local function makeitalic(fn)
return function() {
return "<i>${fn()}</i>"
end
end
local hello
do
local function _body_0()
return "hello world"
end
hello = makebold(makeitalic(_body0))
end
print(hello()) -- <b><i>hello world</i></b>
TBD
class Vector3D(x, y, z) {
@@_X = string.byte('x')
@@_Y = string.byte('y')
@@_Z = string.byte('z')
@x, @y, @z = x, y, z
::add(b) = Vector3D(@x + b.x, @y + b.y, @z + b.z)
::sub(b) = Vector3D(@x - b.x, @y - b.y, @z - b.z)
::dot(b) = Vector3D(@x * b.x, @y * b.y, @z * b.z)
::__add(b) = @:add(b)
::__sub(b) = @:sub(b)
::__tostring() = "[${@x}, ${@y}, ${@z}]"
-- overriding the index metamethod
::__index(key) {
let mt = getmetatable(@)
let old_index = mt.__index
mt.__index = |key| {
return old_index[key]?
let coords = [nil; 3]
let len = math.max(#key, 3)
for i = 1, len {
match key:byte(i) {
case @@_X do coords[i] = @x,
case @@_Y do coords[i] = @y,
case @@_Z do coords[i] = @z,
default do
error("Invalid key: ${key}")
}
}
let res
if coords[3] == nil {
res = coords |> unpack |> Vector2D
} elseif coords[2] == nil {
res = coords[1]
} else {
res = coords |> unpack |> Vector3D
}
return res
}
}
}
FFI Example:
let ffi = require 'ffi'
ffi.cdef """
char * strndup(const char * s, size_t n);
int strlen(const char *s);
"""
let s1 = 'Hello, world!'
let s_s1 = s1 |> ffi.C.strlen
; "Original: ${s1}" |> print
; "strlen: ${s_s1}" |> print
let s2 = ffi.string(ffi.C.strndup(s1, s_s1), s_s1)
; "Copy: ${s2}" |> print
; "strlen: ${ffi.C.strlen(s2)}" |> print