Skip to content

Instantly share code, notes, and snippets.

@phoenixenero phoenixenero/reference.md
Last active Aug 9, 2016

Embed
What would you like to do?
Lilia, a language that compiles to Lua

Notes

This document uses Lua's variation of the notation for Extended Backus-Naur Form (EBNF).

Language support

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.

Lexical conventions

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

Variables in Lilia are similar to Lua.

var ::= Name

Statements

Blocks

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

Chunks in Lilia are similar to Lua. Syntactically, they are equivalent to blocks.

chunk ::= block

Assignment

Lilia allows multiple assignment. These are similar to Lua and won't be described here.

stat ::= varlist ‘=’ explist
varlist ::= var {‘,’ var}
explist ::= exp {‘,’ exp}

Control Structures

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

For statement

Similar behavior to Lua.

stat ::= for Name ‘=’ exp ‘,’ exp [‘,’ exp] ‘{’ block ‘}’
stat ::= for namelist in explist ‘{’ block ‘}’
	namelist ::= Name {‘,’ Name}

With statement

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 the with block. It does not return outside with.
  • break does not break the outer loop.

Also there might be some side-effects when coroutines are used.

Dumb with statement

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

Function calls as statements

stat ::= functioncall

Local declarations

Local variables are declared using let.

stat ::= let namelist [‘=’ explist]

Destructuring assignment

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

Arithmetic assignment

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)

Expressions

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 ‘)’

Arithmetic operators

Lilia supports the standard Lua arithmetic operators and will not be repeated here.

Bitwise operators

Lilia supports Lua 5.3 bitwise operators and will compile successfully.

Function operators

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))

Relational operators

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.

Logical operators

Lilia logical operators are the same as with Lua.

Concatenation

Lilia supports the Lua concatenation operator (..).

Length operator

yes.

Precedence

Operator precedence in Lilia is nearly the same as with Lua.

or
and
<     >     <=    >=    ~=/!=    ==
|>
|
~
&
<<    >>
..
+     -
*     /     //    %
unary operators (not   #     -     ~)
^

Table constructors

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.

Function calls

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.

Function definitions

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.

Function decorators

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>

Classes

TBD

Sample code

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.