Edit on GitHub

Overview

This is a quick overview of the language features that are currently implemented by the examples.

All of the features and examples presented here should work with the latest Nelua version.

A note for Lua users

Most of Nelua’s syntax and semantics are similar to Lua, so if you know Lua, you probably know Nelua. However, Nelua has many additions, such as type notations, to make code more efficient and to allow metaprogramming. This overview will try to focus on those additions.

There is no interpreter or VM, all of the code is converted directly into native machine code. This means you can expect better efficiency than Lua. However, this also means that Nelua cannot load code generated at runtime. The user is encouraged to generate code at compile-time using the preprocessor.

Although copying Lua syntax and semantics with minor changes is a goal of Nelua, not all Lua features are implemented yet. Most of the dynamic parts, such as tables and handling dynamic types at runtime, are not implemented yet. So at the moment, using records instead of tables and using type notations is required.

A note for C users

Nelua tries to expose most of C’s features without overhead, so expect to get near-C performance when coding in the C style; that is, using type notations, manual memory management, pointers, and records (structs).

The semantics are not exactly the same as C semantics, but they are close. There are slight differences (like initializing to zero by default) to minimize undefined behaviors and other differences to maintain consistency with Lua semantics (like integer division rounding towards negative infinity). However, there are ways to use C semantics when needed.

The preprocessor is much more powerful than C’s preprocessor because it is part of the compiler which runs in Lua. This means you can interact with the compiler during parsing. The preprocessor should be used for code specialization, making generic code, and avoiding code duplication.

Nelua compiles everything into a single readable C file. If you know C, it is recommended that you read the generated C code to learn more about exactly how the compiler works and what code it outputs.

Hello world

A simple hello world program is just the same as in Lua:

print 'Hello world!'

Comments

Comments are just like Lua:

-- one line comment
--[[
  multi-line comment
]]
--[=[
  multi line comment. `=` can be placed multiple times
  in case you have `[[` `]]` tokens inside the comment.
  it will always match its corresponding token
]=]

Variables

Variables are declared and defined like in Lua, but you may optionally specify a type:

local b = false -- of deduced type 'boolean', initialized to false
local s = 'test' -- of deduced type 'string', initialized to 'test'
local one = 1 --  of type 'integer', initialized to 1
local pi: number = 3.14 --  local pi: number = 3.14 --  of type 'number', initialized to 3.14
print(b,s,one,pi) -- outputs: false test 1 3.14

The compiler takes advantage of types for compile-time and runtime checks, as well as to generate efficient code to handle the specific type used.

Type deduction

When a variable has no specified type on its declaration, the type is automatically deduced and resolved at compile-time:

local a -- type will be deduced at scope end
a = 1
a = 2
print(a) -- outputs: 2
-- end of scope, compiler deduced 'a' to be of type 'integer'

The compiler does the best it can to deduce the type for you. In most situations it should work, but in some corner cases you may want to explicitly set a type for a variable.

In the case of different types being assigned to the same variable, the compiler deduces the variable type to be the any type, a type that can hold anything at runtime. However support for any type is not fully implemented yet, thus you will get a compile error. In the future with proper support for any type usual Lua code with dynamic typing will be compatible with Nelua.

Zero initialization

Variables declared but not defined early are always initialized to zeros by default:

local b: boolean -- variable of type 'boolean', initialized to 'false'
local i: integer -- variable of type 'integer', initialized to 0
print(b, i) -- outputs: false 0

Nelua encourages the Zero Is Initialization (ZII) idiom and it’s used in all of its standard libraries. The language does not have constructor or destructors (RAII) in favor of this idiom.

Zero initialization can be optionally disabled using the <noinit> annotation. Although not advised, one could do this for micro optimization purposes.

Auto variables

A variable declared as auto has its type deduced early based only on the type of its first assignment:

local a: auto = 1 -- a is deduced to be of type 'integer'

-- uncommenting the following will trigger the compile error:
--   constant value `1.0` is fractional which is invalid for the type 'int64'
--a = 1.0

print(a) -- outputs: 1

Auto variables were not intended to be used in variable declarations like in the example above, because in most cases you can omit the type and the compiler will automatically deduce it. This can be used, however, if you want the compiler to deduce early. The auto type was mainly created to be used with polymorphic functions.

Compile-time variables

Compile-time variables have their values known at compile-time:

local a <comptime> = 1 + 2 -- constant variable of value '3' evaluated and known at compile-time
print(a) -- outputs: 3

The compiler takes advantage of compile-time variables to generate efficient code, because compile-time variables can be processed at compile-time. Compile-time variables are also useful as compile-time parameters in polymorphic functions.

Const variables

Const variables can be assigned once at runtime, however, they cannot mutate:

local x <const> = 1
local a <const> = x
print(a) -- outputs: 1

-- uncommenting the following will trigger the compile error:
--   error: cannot assign a constant variable
--a = 2

The const annotation can also be used for function arguments.

The use of <const> annotation is mostly for aesthetic purposes. Its usage does not affect efficiency.

To-be-closed variables

A to-be-closed variable behaves like a constant local variable, except that its value is closed whenever the variable goes out of scope, including normal block termination, exiting its block by a break or return.

Here, to close a value means to call its __close metamethod:

local AbstractFile = @record{
  name: string
}

function AbstractFile.open(name: string): AbstractFile
  local file = AbstractFile{name=name}
  print('opened', file.name)
  return file
end

function AbstractFile:__close() -- define __close metamethod
  print('closed', self.name)
end

do
  local file <close> = AbstractFile.open('dummy') -- prints 'opened dummy'
  -- when scope ends, AbstractFile:__close() is called and prints 'closed dummy'
end

To-be-closed variables is meant to be used for releasing resources in a deterministic manner on scope termination. It is an evolution of defer statement. The syntax and functionality is inspired by the similar statement in the Lua language. It is guaranteed to be executed in reverse order before any return, break or continue statement.

Multiple variables assignment

Multiple variables can be assigned in a single statement:

local a, b = 1, 2
print(a, b) -- outputs: 1 2
b, a = a, b -- swap values
print(a, b) -- outputs: 2 1

Temporary variables are used in a multiple assignment, so the values can be safely swapped.

Symbols

Symbols are named identifiers for functions, types, and variables.

Local symbol

Local symbols are only visible in the current and inner scopes:

do
  local a = 1
  do
    print(a) -- outputs: 1
  end
end
-- uncommenting this would trigger a compiler error because `a` is not visible:
-- a = 1

Global symbol

Global symbols are visible in other source files. They can only be declared in the top scope:

global global_a = 1
global function global_f()
  return 'f'
end

If the above is saved into a file in the same directory as globals.nelua, then we can run:

require 'globals'
print(global_a) -- outputs: 1
print(global_f()) -- outputs: f

Unlike Lua, to declare a global variable you must explicitly use the global keyword.

Control flow

Nelua provides the same control flow mechanisms as Lua, plus some additional ones to make low level programming easier, like switch, defer, and continue statements.

If

If statements work just like in Lua:

local a = 1 -- change this to 2 or 3 to trigger other ifs
if a == 1 then
  print 'is one'
elseif a == 2 then
  print 'is two'
else
  print('not one or two')
end

Switch

The switch statement is similar to C:

local a = 1 -- change this to 2 or 3 to trigger other ifs
switch a do
case 1 then
  print 'is 1'
case 2, 3 then
  print 'is 2 or 3'
else
  print 'else'
end

The case expression can only contain integral numbers known at compile-time. The compiler can generate more optimized code when using a switch instead of using many if statements for integers.

Note that, unlike C, there is no need to use “break” on each case statement, this is done automatically.

Do

Do blocks are useful for creating arbitrary scopes to avoid collision of variable names:

do
  local a = 0
  print(a) -- outputs: 0
end
do
  local a = 1 -- can declare variable named a again
  print(a) -- outputs: 1
end

Defer

The defer statement is useful for executing code upon scope termination.

do
  defer
    print 'world'
  end
  print 'hello'
end
-- outputs 'hello' then 'world'

Defer is meant to be used for releasing resources in a deterministic manner on scope termination. The syntax and functionality is inspired by the similar statement in the Go language. It is guaranteed to be executed in reverse order before any return, break or continue statement.

Goto

Gotos are useful to get out of nested loops and jump between lines:

local haserr = true
if haserr then
  goto getout -- get out of the loop
end
print 'success'
::getout::
print 'fail'
-- outputs only 'fail'

While

While is just like in Lua:

local a = 1
while a <= 5 do
  print(a) -- outputs 1 2 3 4 5
  a = a + 1
end

Repeat

Repeat also functions as in Lua:

local a = 0
repeat
  a = a + 1
  print(a) -- outputs 1 2 3 4 5
  local stop = a == 5
until stop

Note that, like Lua, a variable declared inside a repeat scope is visible inside its condition expression.

Numeric For

Numeric for is like in Lua, meaning it is inclusive of the first and the last elements:

for i = 0,5 do
  -- i is deduced to 'integer'
  print(i) -- outputs 0 1 2 3 4 5
end

Like in Lua, numeric for loops always evaluate the begin, end, and step expressions just once. The iterate variable type is automatically deduced using the begin and end expressions only.

Exclusive For

The exclusive for is available to create exclusive for loops. They work using comparison operators ~= <= >= < >:

for i=0,<5 do
  print(i) -- outputs 0 1 2 3 4
end

Stepped For

The last parameter in for syntax is the step. Its counter is always incremented with i = i + step. By default the step is always 1. When using negative steps, a reverse for loop is possible:

for i=5,0,-1 do
  print(i) -- outputs 5 4 3 2 1 0
end

For In

Like in Lua, you can iterate values using an iterator function:

require 'iterators'

local a: [4]string = {"a","b","c","d"}
for i,v in ipairs(a) do
  print(i,v) -- outputs: 0 a, 1 b, 2 c, 3 d
end

Nelua provides some basic iterator functions that works on most containers, check the iterators module documentation for more of them.

Continue

The continue statement is used to skip to the next iteration of a loop:

for i=1,10 do
  if i<=5 then
    continue
  end
  print(i) -- outputs: 6 7 8 9 10
end

Fallthrough

The fallthrough statement can be used to explicitly fall through into the next case block of a switch:

local a = 1
switch a do
case 1 then
  print '1'
  fallthrough -- next case block will be executed
case 2 then
  print '2'
end
-- outputs '1' followed by '2'

This functionality is like fallthrough in C switches, but in Nelua it must be explicit.

Break

The break statement is used to immediately exit a loop:

for i=1,10 do
  if i>5 then
    break
  end
  print(i) -- outputs: 1 2 3 4 5
end

Do expression

Sometimes is useful to create an expression with statements on its own in the middle of another expression, this is possible with the (do end) syntax:

local i = 2
local s = (do
  local res: string
  if i == 1 then
    res = 'one'
  elseif i == 2 then
    res = 'two'
  else
    res = 'other'
  end
  in res -- injects final expression result
end)
print(s) -- outputs: two

This construct is used internally by the compiler to implement other features, thus the motivation behind this was not really the syntax (it can be verbose), but the meta programming possibilities that such feature offers.

Primitive types

Primitives types are the basic types built into the compiler.

Boolean

local a: boolean -- variable of type 'boolean' initialized to 'false'
local b = false
local c = true
print(a,b,c) -- outputs: false false true

The boolean is defined as a bool in the generated C code.

Number

Number literals are defined like in Lua:

local dec = 1234 -- variable of type 'integer'
local bin = 0b1010 -- variable of type 'uint8', set from binary number
local hex = 0xff -- variable of type 'integer', set from hexadecimal number
local char = 'A'_u8 -- variable of type 'uint8' set from ASCII character
local exp = 1.2e-100 -- variable of type 'number' set using scientific notation
local frac = 1.41 -- variable of type 'number'
print(dec,bin,hex,char,exp,frac)

local pi = 0x1.921FB54442D18p+1 -- hexadecimal with fractional and exponent
print(pi) -- outputs: 3.1415926535898

The integer is the default type for integral literals without suffix. The number is the default type for fractional literals without suffix.

You can use type suffixes to force a type for a numeric literal:

local a = 1234_u32 -- variable of type 'int32'
local b = 1_f32 -- variable of type 'float32'
local c = -1_isize -- variable of type `isize`
print(a,b,c) --outputs: 1234 1.0 -1

The following table shows Nelua primitive numeric types and their related types in C:

Type C Type Suffixes
integer int64_t _i _integer
uinteger uint64_t _u _uinteger
number double _n _number
byte uint8_t _b _byte
isize intptr_t _is _isize
int8 int8_t _i8 _int8
int16 int16_t _i16 _int16
int32 int32_t _i32 _int32
int64 int64_t _i64 _int64
int128* __int128 _i128 _int128
usize uintptr_t _us _usize
uint8 uint8_t _u8 _uint8
uint16 uint16_t _u16 _uint16
uint32 uint32_t _u32 _uint32
uint64 uint64_t _u64 _uint64
uint128* unsigned __int128 _u128 _uint128
float32 float _f32 _float32
float64 double _f64 _float64
float128* __float128 _f128 _float128

* Only supported by some C compilers and architectures.

The types isize and usize are usually 32 bits wide on 32-bit systems, and 64 bits wide on 64-bit systems.

When you need an integer value you should use integer unless you have a specific reason to use a sized or unsigned integer type. The integer, uinteger and number are intended to be configurable. By default they are 64 bits for all architectures, but this can be customized by the user at compile-time via the preprocessor when needed.

String

String points to an immutable contiguous sequence of characters.

local str1: string -- empty string
local str2 = "string 2" -- variable of type 'string'
local str3: string = 'string 3' -- also a 'string'
local str4 = [[
multi
line
string
]]
print(str1, str2, str3) -- outputs: "" "string 2" "string 3"
print(str4) -- outputs the multi line string

Internally string just holds a pointer to a buffer and a size. It’s buffer is null terminated (‘\0’) by default to have more compatibility with C.

Like in Lua, string is immutable. If the programmer wants a mutable string, he should use the stringbuilder module.

String escape sequence

Strings literals defined between quotes can have escape sequences following same rules as in Lua:

local ctr = "\n\t\r\a\b\v\f" -- escape control characters
local utf = "\u{03C0}" -- escape UTF-8 code
local hex = "\x41" -- escape hexadecimal byte
local dec = "\65" -- escape decimal byte
local multiline1 = "my\z
                    text1" -- trim spaces and newlines after '\z'
local multiline2 = "my\
text2" -- escape new lines after '\' to '\n'
print(utf, hex, dec, multiline1) -- outputs: π A A mytext1
print(multiline2) -- outputs "my" and "text2" on a new line

Array

An array is a list with a size that is fixed and known at compile-time:

local a: [4]integer = {1,2,3,4}
print(a[0], a[1], a[2], a[3]) -- outputs: 1 2 3 4

local b: [4]integer
print(b[0], b[1], b[2], b[3]) -- outputs: 0 0 0 0
local len = #b -- get the length of the array, should be 4
print(len) -- outputs: 4

When passing an array to a function as an argument, it is passed by value. This means the array is copied. This can incur some performance overhead. Thus when calling functions, you may want to pass arrays by reference using the reference operator when appropriate.

Array with inferred size

When declaring and initializing an array the size on the type notation can be optionally omitted as a syntax sugar:

local a: []integer = {1,2,3,4} -- array size will be 4
print(#a) -- outputs: 4

Do not confuse this syntax with dynamic arrays, the array size will still be fixed and determined at compile time.

Multidimensional array

An array can also be multidimensional:

local m: [2][2]number = {
  {1.0, 2.0},
  {3.0, 4.0}
}
print(m[0][0], m[0][1]) -- outputs: 1.0 2.0
print(m[1][0], m[1][1]) -- outputs: 3.0 4.0

Enum

Enums are used to list constant values in sequential order:

local Weeks = @enum{
  Sunday = 0,
  Monday,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday
}
print(Weeks.Sunday) -- outputs: 0

local a: Weeks = Weeks.Monday
print(a) -- outputs: 1

The programmer must always initialize the first enum value. This choice was made to makes the code more clear when reading.

Record

Records store variables in a block of memory:

local Person = @record{
  name: string,
  age: integer
}

-- typed initialization
local a: Person = {name = "Mark", age = 20}
print(a.name, a.age)

-- casting initialization
local b = (@Person){name = "Paul", age = 21}
print(b.name, b.age)

-- ordered fields initialization
local c = (@Person){"Eric", 21}
print(c.name, c.age)

-- late initialization
local d: Person
d.name = "John"
d.age  = 22
print(d.name, d.age)

Records are directly translated to C structs.

Union

Union store multiple variables in a shared memory block:

local IntOrFloat = @union{
  i: int64,
  f: float64,
}
local u: IntOrFloat = {i=1}
print(u.i) -- outputs: 1
u.f = 1
print(u.f) -- outputs: 1.0f
print(u.i) -- outputs some garbage integer

You are responsible for saving the current stored type in the union somewhere else to know what current field is valid for reading, otherwise you can read garbage data. Unions are directly translated to C unions.

Pointer

A pointer points to a region in memory of a specific type:

local n = nilptr -- a generic pointer, initialized to nilptr
local p: pointer -- a generic pointer to anything, initialized to nilptr
local i: *integer -- pointer to an integer

Pointers are directly translated to C raw pointers. Unlike C, pointer arithmetic is disallowed. To do pointer arithmetic you must explicitly cast to and from integers.

Unbounded Array

An array with size 0 is an unbounded array, that is, an array with unknown size at compile time:

local a: [4]integer = {1,2,3,4}

-- unbounded array only makes sense when used with pointer
local a_ptr: *[0]integer
a_ptr = &a -- takes the reference of 'a'
print(a_ptr[1])

An unbounded array is useful for indexing pointers, because unlike C, you cannot index a pointer unless it is a pointer to an unbounded array.

Unbounded arrays are unsafe, because bounds checking is not possible at compile time or runtime. Use the span to have bounds checking.

Function type

The function type, mostly used to store callbacks, is a pointer to a function:

local function add_impl(x: integer, y: integer): integer
  return x + y
end

local function double_add_impl(x: integer, y: integer): integer
  return 2*(x + y)
end

local add: function(x: integer, y: integer): integer
add = add_impl
print(add(1,2)) -- outputs 3
add = double_add_impl
print(add(1,2)) -- outputs 6

The function type is just a pointer, thus can be converted to/from generic pointers with explicit casts.

Span

Span, also known as “fat pointers” or “slices” in other languages, are pointers to a block of contiguous elements of which the size is known at runtime:

require 'span'
local arr = (@[4]integer) {1,2,3,4}
local s: span(integer) = &arr
print(s[0], s[1]) -- outputs: 1 2
print(#s) -- outputs 4

The advantage of using a span instead of a pointer is that spans generate runtime checks for out of bounds access, so oftentimes code using span is safer. The runtime checks can be disabled in release builds.

Niltype

The niltype is the type of nil.

The niltype is not useful by itself, it is only useful when using with unions to create the optional type or for detecting nil arguments in polymorphic functions.

Void

The void type is used internally for the generic pointer, that is, *void and pointer types are all equivalent.

The void type can also be used explicitly mark that a function has no return:

local function myprint(): void
  print 'hello'
end
myprint() -- outputs: hello

The compiler can automatic deduce function return types thus this is usually not needed.

The “type” type

The type type is the type of a symbol that refers to a type. Symbols with this type are used at compile-time only. They are useful for aliasing types:

local MyInt: type = @integer -- a symbol of type 'type' holding the type 'integer'
local a: MyInt -- variable of type 'MyInt' (actually an 'integer')
print(a) -- outputs: 0

In the middle of statements the @ token is required to precede a type expression. This token signals to the compiler that a type expression comes after it.

Size of a type

You can use the operator # to get the size of any type in bytes:

local Vec2 = @record{x: int32, y: int32}
print(#Vec2) -- outputs: 8

Implicit type conversion

Some types can be implicitly converted. For example, any scalar type can be converted to any other scalar type:

local i: integer = 1
local u: uinteger = i
print(u) -- outputs: 1

Implicit conversion generates runtime checks for loss of precision in the conversion. If this happens the application crashes with a narrow casting error. The runtime checks can be disabled in release builds.

Explicit type conversion

The expression (@type)(variable) is used to explicitly convert a variable to another type.

local i = 1
local f = (@number)(i) -- convert 'i' to the type 'number'
print(i, f) -- outputs: 1 1.0

If a type is aliased to a symbol then it is possible to convert variables by calling the symbol:

local MyNumber = @number
local i = 1
local f = MyNumber(i) -- convert 'i' to the type 'number'
print(i, f) -- outputs: 1 1.0

Unlike implicit conversion, explicit conversions skip runtime checks:

local ni: integer = -1
-- the following would crash with "narrow casting from int64 to uint64 failed"
--local nu: uinteger = ni

local nu: uinteger = (@uinteger)(ni) -- explicit cast works, no checks are done
print(nu) -- outputs: 18446744073709551615

Operators

Unary and binary operators are provided for creating expressions:

print(2 ^ 2) -- pow, outputs: 4.0
print(5 // 2) -- integer division, outputs: 2
print(5 / 2) -- float division, outputs: 2.5

All Lua operators are provided:

Name Syntax Operation
or a or b conditional or
and a and b conditional and
lt a < b less than
gt a > b greater than
le a <= b less or equal than
ge a >= b greater or equal than
ne a ~= b not equal
eq a == b equal
bor a | b bitwise OR
band a & b bitwise AND
bxor a ~ b bitwise XOR
shl a << b bitwise logical left shift
shr a >> b bitwise logical right shift
asr a >>> b bitwise arithmetic right shift
bnot ~a bitwise NOT
concat a .. b concatenation
add a + b arithmetic add
sub a - b arithmetic subtract
mul a * b arithmetic multiply
div a / b arithmetic division
idiv a // b arithmetic floor division
tdiv a /// b arithmetic truncate division
mod a % b arithmetic floor division remainder
tmod a %%% b arithmetic truncate division remainder
pow a ^ b arithmetic exponentiation
unm -a arithmetic negation
not not a boolean negation
len #a length
deref $a pointer dereference
ref &a memory reference

All the operators follow Lua semantics, i.e.:

  • / and ^ promotes numbers to floats.
  • // and % rounds the quotient towards minus infinity.
  • << and >> are logical shifts and you can do negative or large shifts.
  • and, or, not, ==, ~= can be used between any variable type.
  • Integer overflows wrap around.

These additional operators are not available in Lua, they are used for low-level programming and follow C semantics:

  • /// and %%% rounds the quotient towards zero (like C division and modulo on integers).
  • >>> arithmetic right shift (like C right shift on signed integers).
  • $ dereference a pointer (like C dereference).
  • & reference a memory (like C reference).

Functions

Functions are declared as in Lua, but arguments and returns can have their types explicitly specified:

local function add(a: integer, b: integer): integer
  return a + b
end
print(add(1, 2)) -- outputs 3

Return type inference

The return type can be automatically deduced when not specified:

local function add(a: integer, b: integer)
  return a + b -- return is of deduced type 'integer'
end
print(add(1, 2)) -- outputs 3

Recursive calls

Functions can call themselves recursively:

local function fib(n: integer): integer
  if n < 2 then return n end
  return fib(n - 2) + fib(n - 1)
end
print(fib(10)) -- outputs: 55

Function that do recursive calls must explicitly set the return type, i.e, the compiler cannot deduce the return type.

Multiple returns

Functions can have multiple return values as in Lua:

local function get_multiple()
  return false, 1
end

local a, b = get_multiple()
-- a is of type 'boolean' with value 'false'
-- b is of type 'integer' with value '1'
print(a,b) -- outputs: false 1

Multiple returns can optionally be explicitly typed:

local function get_multiple(): (boolean, integer)
  return false, 1
end

local a, b = get_multiple()
print(a,b) -- outputs: false 1

Multiple returns are efficient and packed into C structs in the code generator.

Anonymous functions

A function can be declared without a name as an expression, this kind of function is called anonymous function:

local function g(x: integer, f: function(x: integer): integer)
  return f(x)
end

local y = g(1, function(x: integer): integer
  return 2*x
end)

print(y) -- outputs: 2

Unlike Lua an anonymous function cannot be a closure, that is, it cannot use variables declared in upper scopes unless the top most scope.

Nested functions

A function can be declared inside another function:

local function f()
  local function g()
    return 'hello from g'
  end
  return g()
end

print(f()) -- outputs: hello from g

The function will be visible only in inner scopes.

Unlike Lua a nested function cannot be a closure, that is, it cannot use variables declared in upper scopes unless the top most scope.

Top scope closures

Functions declared in the top scope work as top scope closures. They have access to all local variables declared beforehand:

local counter = 1 -- 'a' lives in the heap because it's on the top scope
local function increment() -- a top scope closure
  -- counter is an upvalue for this function, we can access and modify it
  counter = counter + 1
end
print(counter) -- outputs 1
increment()
print(counter) -- outputs 2

Unlike Lua, when declaring functions in the top scope, the compiler takes advantage of the fact that top scope variables are always accessible in the program’s static storage memory to create lightweight closures without needing to hold an upvalue reference or to use a garbage collector. Therefore they are very lightweight and do not incur costs like a closure nested in a function would.

Variable number of arguments

A function can have variable number arguments:

local function f(...: varargs)
  print(...)
end
f(1, true) -- outputs: 1 true

local function sum(...: varargs)
  local s: integer
  ## for i=1,select('#', ...) do -- iterate over all arguments
    s = s + #[select(i, ...)]# -- select argument at index `i`
  ## end
  return s
end
print(sum(1, 2, 3)) -- outputs: 6

Functions with variable number of arguments will be polymorphic (see below).

The preprocessor is used to specialize the function at compile time. One specialization occur for every different number of arguments or argument types, thus there are no branching or costs at runtime when making functions with variable number of arguments.

Polymorphic functions

Polymorphic functions, or poly functions in short in the sources, are functions which contain arguments whose proprieties can only be known when calling the function at compile time. They are defined and processed later when calling it for the first time. They are used to specialize the function for different arguments types:

local function add(a: auto, b: auto)
  return a + b
end

local a = add(1,2)
-- call to 'add', a function 'add(a: integer, b: integer): integer' is defined
print(a) -- outputs: 3
local b = add(1.0, 2.0)
-- call to 'add' with different types, function 'add(a: number, b: number): number' is defined
print(b) -- outputs: 3.0

In the above, the auto type is used as a generic placeholder to replace the function argument with the incoming call type. This makes it possible to create a generic function for multiple types.

Polymorphic functions are memoized, that is, only defined once for each kind of specialization.

Later we will show how polymorphic functions are more useful when used in combination with the preprocessor.

Record functions

A record type can have functions defined for it. This makes it possible to create functions that are to be used only within the record:

local Vec2 = @record{x: number, y: number}

function Vec2.create(x: integer, y: integer): Vec2
  return (@Vec2){x, y}
end

local v = Vec2.create(1,2)
print(v.x, v.y) -- outputs: 1.0 2.0

Record methods

A method is function defined for record that takes a reference to the record as its first argument. This first argument is visible as self inside the method. For defining or calling a method the colon token : must be used, just like in Lua.

local Rect = @record{x: number, y: number, w: number, h: number}

function Rect:translate(x: number, y: number)
  -- 'self' here is of the type '*Rect'
  self.x = self.x + x
  self.y = self.y + y
end

function Rect:area()
  -- 'self' here is of the type '*Rect'
  return self.w * self.h
end

local v = Rect{0,0,2,3}
v:translate(2,2)
print(v.x, v.y) -- outputs: 2.0 2.0
print(v:area()) -- outputs: 6.0

When calling methods on records, the compiler automatically takes care to automatically reference or dereference the object being called.

Record metamethods

Some special methods using the __ prefix are used by the compiler to define behaviors on certain operations with the record type. They are called metamethods and are similar to Lua metamethods:

require 'math'

local Vec2 = @record{x: number, y: number}

-- Called on the binary operator '+'
function Vec2.__add(a: Vec2, b: Vec2)
  return (@Vec2){a.x+b.x, a.y+b.y}
end

-- Called on the unary operator '#'
function Vec2:__len()
  return math.sqrt(self.x*self.x + self.y*self.y)
end

local a: Vec2 = {1, 2}
local b: Vec2 = {3, 4}
local c = a + b -- calls the __add metamethod
print(c.x, c.y) -- outputs: 4.0 6.0
local len = #c -- calls the __len metamethod
print(len) -- outputs: 7.211102550928

Complete list of metamethods that can be defined for records:

Name Syntax Kind Operation
__lt a < b binary less than
__le a <= b binary less or equal than
__eq a == b binary equal
__bor a | b binary bitwise or
__band a & b binary bitwise and
__bxor a ~ b binary bitwise xor
__shl a << b binary bitwise logical left shift
__shr a >> b binary bitwise logical right shift
__asr a >>> b binary bitwise arithmetic right shift
__bnot ~a unary bitwise not
__concat a .. b binary concatenation
__add a + b binary arithmetic add
__sub a - b binary arithmetic subtract
__mul a * b binary arithmetic multiply
__div a / b binary arithmetic division
__idiv a // b binary arithmetic floor division
__tdiv a /// b binary arithmetic truncate division
__mod a % b binary arithmetic floor division remainder
__tmod a %%% b binary arithmetic truncate division remainder
__pow a ^ b binary arithmetic exponentiation
__unm -a unary arithmetic negation
__len #a unary length
__index a[b] indexing array index
__atindex a[b] indexing array index via reference
__tostring tostring(a) cast explicit/implicit cast to string
__convert   cast implicit cast from anything
__gc   gc called when collected by the GC
__close   close called when <close> variables goes out of scope
__next next(a) iterator used by next
__mnext mnext(a) iterator used by mnext
__pairs pairs(a) iterator used by pairs
__mpairs mpairs(a) iterator used by mpairs

Record globals

Sometimes it is useful to declare a global variable inside a record type, using the record as a “namespace”:

global Globals = @record{} -- record used just for name spacing
global Globals.AppName: string
Globals.AppName = "My App"
print(Globals.AppName) -- outputs: My App

Record globals can be used to encapsulate modules, like tables are used to make modules in Lua.

Calls with nested records

You can define and later initialize complex records structures in a Lua-like style:

local WindowConfig = @record{
  title: string,
  pos: record{
    x: integer,
    y: integer
  },
  size: record{
    x: integer,
    y: integer
  }
}
local function create_window(config: WindowConfig)
  print(config.title, config.pos.x, config.pos.y)
end

-- the compiler knows that the argument should be parsed as WindowConfig
-- notice that 'size' field is not set, so its initialized to zeros
create_window({title="hi", pos={x=1, y=2}})

Memory management

By default Nelua uses a garbage collector to allocate and deallocate memory on its own. However, it can be disabled with the pragma nogc via the command line using -P nogc or in the sources:

## pragmas.nogc = true -- tells the compiler that we don't want to use the GC
require 'string' -- the string class will be implemented without GC code
local str = tostring(1) -- tostring needs to allocates a new string
print(str) -- outputs: 1
## if pragmas.nogc then -- the GC is disabled, must manually deallocate memory
str:destroy() -- deallocates the string
## end
print(str) -- the string was destroyed and is now empty, outputs nothing

Notice that when disabling the garbage collector the coding style is different from the usual Lua style, since you now need to think of each allocation and deallocation, including strings, otherwise memory in your application will leak. Thus it is best to leave the GC enabled when you need rapid prototyping.

Disable the GC if you want to control the memory on your own for performance reasons, if you know how to deal with memory management and don’t mind the additional cognitive load when coding.

Allocating memory

Nelua provides many allocators to assist in managing memory. The most important one is the allocators.default.

require 'string'
require 'memory'

-- this will actually require 'allocators.gc' because GC is enabled
require 'allocators.default'

local Person = @record{name: string, age: integer}
local p: *Person = default_allocator:new(@Person)
p.name = "John"
p.age = 20
print(p.name, p.age)
p = nilptr
-- we don't need to deallocate, the GC will do this on its own when needed!

The default_allocator is an alias to gc_allocator or general_allocator depending if the GC is enabled or not.

When the GC is enabled, you must always allocate memory that contains pointers using gc_allocator (or default_allocator) instead of any other allocator, because it marks the allocated memory region for scanning for references.

Allocating memory manually

For doing manual memory management, you can use the general purpose allocator, which is based on the system’s malloc and free functions:

## pragmas.nogc = true -- disables the GC
require 'string'
require 'memory'
require 'allocators.general'

local Person = @record{name: string, age: integer}
local p: *Person = general_allocator:new(@Person) -- allocate the appropriate size for Person
p.name = tostring("John") -- another allocation here
p.age = 20
print(p.name, p.age)
p.name:destroy() -- free the string allocation
general_allocator:delete(p) -- free the Person allocation
p = nilptr

Dereferencing and referencing

The operator & is used to get a reference to a variable, and the operator $ is used to access the reference.

local a = 1
local ap = &a -- ap is a pointer to a
$ap = 2
print(a) -- outputs: 2
a = 3
print($ap) -- outputs: 3
print(ap) -- outputs memory address of a

Automatic referencing and dereferencing

The compiler can perform automatic referencing or dereferencing for records and arrays *only on function calls:

local Person = @record{name: string, age: integer}

local function print_info_byref(p: *Person)
  print(p.name, p.age)
end
local function print_info_bycopy(p: Person)
  print(p.name, p.age)
end

local p: Person = {"John", 20}
print_info_byref(p) -- the referencing with `&` is implicit here
local pref: *Person = &p
print_info_bycopy(pref) -- the dereferencing with `$` is implicit here

For instance, the above code is equivalent to:

local Person = @record{name: string, age: integer}

local function print_info_byref(p: *Person)
  print(p.name, p.age)
end
local function print_info_bycopy(p: Person)
  print(p.name, p.age)
end

local p: Person = {"John", 20}
print_info_byref(&p)
local pref: *Person = &p
print_info_bycopy($pref)

The above example is not very useful by itself, but permits auto referencing when doing method calls:

local Person = @record{name: string, age: integer}

-- note that this function only accept pointers
function Person.print_info(self: *Person)
  print(self.name, self.age)
end

local p: Person = {"John", 20}
p:print_info() -- perform auto referencing of 'p' when calling here
Person.print_info(p) -- equivalent, also performs auto referencing

The automatic referencing and dereferencing mechanism allows the use of unary operators, binary operators, function calls, or method calls by value or by reference, for records or arrays.

Meta programming

The language offers advanced features for metaprogramming by having a full Lua preprocessor at compile time that can generate and manipulate code when compiling.

Preprocessor

At compile time a Lua preprocessor is available to render arbitrary code. It works similarly to templates in the web development world, because it emits code written between its statements.

Lines beginning with ## and between ##[[ ]] are Lua code evaluated by the preprocessor:

local a = 0
## for i = 1,4 do
  a = a + 1 -- unroll this line 4 times
## end
print(a) -- outputs 4

##[[
local something = false
if something then
]]
  print('hello') -- prints hello when compiling with "something" defined
##[[ end ]]

For instance, the above code compiles exactly as:

local a = 0
a = a + 1
a = a + 1
a = a + 1
a = a + 1
print(a)

Using the Lua preprocessor, you can generate arbitrary code at compile-time.

Emitting AST nodes (statements)

It is possible to manually emit AST nodes for statements while preprocessing:

##[[
-- create a macro that injects a custom node when called
local function print_macro(str)
  local node = aster.Call{{aster.String{str}}, aster.Id{"print"}}
  -- inject the node where this macro is being called from
  inject_statement(node)
end
]]

## print_macro('hello')

The above code compiles exactly as:

print('hello')

For a complete list of AST shapes that can be created using the aster module read the AST definitions file or the syntax definitions spec file for examples.

Emitting AST nodes (expressions)

It is possible to manually emit AST nodes for expressions while preprocessing:

local a = #[aster.Number{1}]#
print(a) -- outputs: 1

The above code compiles exactly as:

local a = 1
print(a) -- outputs: 1

Expression replacement

For placing values generated by the preprocessor you can use #[ ]#:

local deg2rad = #[math.pi/180.0]#
local hello = #['hello' .. 'world']#
local mybool = #[false]#
print(deg2rad, hello, mybool) -- outputs: 0.017453 helloworld false

The above code compiles exactly as:

local deg2rad = 0.017453292519943
local hello = 'helloworld'
local mybool = false
print(deg2rad, hello, mybool)

Name replacement

For placing identifier names generated by the preprocessor you can use #| |#:

local #|'my' .. 'var'|# = 1
print(myvar) -- outputs: 1

local function foo1() print 'foo' end
#|'foo' .. 1|#() -- outputs: foo

local Weekends = @enum{ Friday=0, Saturday, Sunday }
print(Weekends.#|'S'..string.lower('UNDAY')|#)

The above code compiles exactly as:

local myvar = 1
print(myvar)

local function foo1() print 'foo' end
foo1()

local Weekends = @enum{ Friday=0, Saturday, Sunday }
print(Weekends.Sunday)

Preprocessor templated macros

A macros can be created by declaring a function in the preprocessor with its body containing normal code:

## function increment(a, amount)
  -- 'a' in the preprocessor context is a symbol, we need to use its name
  -- 'amount' in the preprocessor context is a lua number
  #|a.name|# = #|a.name|# + #[amount]#
## end
local x = 0
## increment(x, 4)
print(x)

The above code compile exactly as:

local x = 0
x = x + 4
print(x)

Statement replacement macros

A preprocessor function can be called as if it were a runtime function in the middle of a block, it will serve as replacement macro for statements:

## local function mul(res, a, b)
  #[res]# = #[a]# * #[b]#
## end

local a, b = 2, 3
local res = 0
#[mul]#(res, a, b)
print(res) -- outputs: 6

The above code compiles exactly as:

local a, b = 2, 3
local res = 0
res = a * b
print(res) -- outputs: 6

Expression replacement macros

A preprocessor function using in statement can be called as if it were a runtime function in the middle of a statement, it will serve as replacement macro for an expression:

## local function mul(a, b)
  in #[a]# * #[b]#
## end

local a, b = 2, 3
local res = #[mul]#(a, b)
print(res) -- outputs: 6

The above code compiles exactly as:

local a, b = 2, 3
local res = a * b
print(res) -- outputs: 6

Preprocessor macros emitting AST nodes

Creating macros using the template rendering mechanism in the previous example is handy, but has limitations and is not flexible enough for all cases. For example, suppose you want to create an arbitrarily sized array. In this case you will need to manually emit AST nodes:

##[[
-- Create a fixed array initializing to 1,2,3,4...n
local function create_sequence(attr_or_type, n)
  local type
  if traits.is_type(attr_or_type) then -- already a type
    type = attr_or_type
  elseif traits.is_attr(attr_or_type) then -- get a type from a symbol
    type = attr_or_type.value
  end
  -- check if the inputs are valid, in case of wrong input
  static_assert(traits.is_type(type), 'expected a type or a symbol to a type')
  static_assert(traits.is_number(n) and n > 0, 'expected n > 0')
  -- create the InitList ASTNode, it's used for any braces {} expression
  local initlist = aster.InitList{pattr = {
    -- hint the compiler what type this braces should be evaluated
    desiredtype = types.ArrayType(type, n)}
  }
  -- fill expressions
  for i=1,n do
    -- convert any Lua value to the proper ASTNode
    initlist[i] = aster.value(i)
  end
  return initlist
end
]]

local a = #[create_sequence(integer, 10)]#

The above code compiles exactly as:

local a = (@[10]integer){1,2,3,4,5,6,7,8,9,10}

Code blocks as arguments to preprocessor functions

Blocks of code can be passed to macros by surrounding them inside a function:

##[[
function unroll(count, block)
  for i=1,count do
    block()
  end
end
]]

local counter = 1
## unroll(4, function()
  print(counter) -- outputs: 1 2 3 4
  counter = counter + 1
## end)

The above code compiles exactly as:

local counter = 1
print(counter)
counter = counter + 1
print(counter)
counter = counter + 1
print(counter)
counter = counter + 1
print(counter)
counter = counter + 1

Generic code via the preprocessor

Using macros it is possible to create generic code:

## function Point(PointT, T)
  local #|PointT|# = @record{x: #|T|#, y: #|T|#}
  function #|PointT|#:squaredlength()
    return self.x*self.x + self.y*self.y
  end
## end

## Point('PointFloat', 'float64')
## Point('PointInt', 'int64')

local pa: PointFloat = {x=1,y=2}
print(pa:squaredlength()) -- outputs: 5.0

local pb: PointInt = {x=1,y=2}
print(pb:squaredlength()) -- outputs: 5

Preprocessing on the fly

While the compiler is processing you can view what the compiler already knows to generate arbitrary code:

local Weekends = @enum{ Friday=0, Saturday, Sunda }
## for i,field in ipairs(Weekends.value.fields) do
  print(#[field.name .. ' ' .. tostring(field.value)]#)
## end

The above code compiles exactly as:

local Weekends = @enum{ Friday=0, Saturday, Sunday }
print 'Friday 0'
print 'Saturday 1'
print 'Sunday 2'

You can even manipulate what has already been processed:

local Person = @record{name: string}
## Person.value:add_field('age', primtypes.integer) -- add field 'age' to 'Person'
local p: Person = {name='Joe', age=21}
print(p.age) -- outputs '21'

The above code compiles exactly as:

local Person = @record{name: string, age: integer}
local p: Person = {name='Joe', age=21}
print(p.age) -- outputs '21'

The compiler is implemented and runs using Lua, and the preprocessor is actually a Lua function that the compiler is running, so it is even possible to modify or inject code into the compiler itself on the fly.

Preprocessing polymorphic functions

Polymorphic functions can be specialized at compile time when used in combination with the preprocessor:

local function pow(x: auto, n: integer)
## static_assert(x.type.is_scalar, 'cannot pow variable of type "%s"', x.type)
## if x.type.is_integral then
  -- x is an integral type (any unsigned/signed integer)
  local r: #[x.type]# = 1
  for i=1,n do
    r = r * x
  end
  return r
## elseif x.type.is_float then
  -- x is a floating point type
  return x ^ n
## end
end

local a = pow(2, 2) -- use specialized implementation for integers
local b = pow(2.0, 2) -- use pow implementation for floats
print(a,b) -- outputs: 4 4.0

-- uncommenting the following will trigger the compile error:
--   error: cannot pow variable of type "string"
--pow('a', 2)

Preprocessor code blocks

Arbitrary Lua code can be put inside preprocessor code blocks. Their syntax starts with ##[[ or ##[=[ (any number of = tokens you want between the brackets) and ends with ]] or ]=] (matching the number of = tokens previously used):

-- this is a preprocessor code block
##[[
function my_compiletime_function(str)
  print(str) -- print at compile time
end
]]

-- call the function defined in the block above
## my_compiletime_function('hello from preprocessor')

As shown in the last line, functions defined inside of the preprocessor code blocks can be evaluated arbitrarily from any part of the code, at any point, using ##.

Although said block was defined for a single module, it will be available for all modules required afterwards, because declarations default to the global scope in Lua. If you would like to avoid polluting other module’s preprocessor environments, declare its functions as local.

Preprocessor function modularity

Suppose you want to use the same preprocessor function from multiple Nelua modules. As explained in the preprocessor code blocks section, one idea is to declare everything in that block as global so that everything would also be available in the preprocessor evaluation of other modules.

For example, in module_A.nelua:

##[[
-- this function is declared as global, so it'll be available on module_B.nelua
function foo()
  print "bar"
end
]]

Then, in module_B.nelua:

require 'module_A'
-- even though foo is not declared in this file, since it's global, it'll be available here
## foo()

Although this seems harmless, it can get messy if you define a function with the same name in different modules. It also means you’re relying on global scope semantics from the preprocessor, which might be unpredictable or brittle due to evaluation order.

Fortunately, there’s a more modular approach for code reuse which does not rely on global scope. Simply create a standalone Lua module and require it on all Nelua modules where you want to use it.

The previous example would be refactored as follows:

1. Create a foo.lua (or any name you want) file and paste your code there:

local function bar()
  print "bar"
end

return { bar = bar }

2. Then, in any source codes that uses that module:

## local foo = require "foo"

## foo.bar()

Aside from modularity, this has the benefit of your preprocessor code being simply Lua code which can leverage all of your editor’s tooling and configuration, such as a code formatter, syntax highlighter, completions, etc.

If the Lua module is not in the same directory where the compiler is running from, then require will fail to find it. To solve this you can set your system’s LUA_PATH environment variable to a pattern which matches that directory, for example, executing export LUA_PATH="/myprojects/mymodules/?.lua" in your terminal (notice the ?.lua at the end).

Preprocessor utilities

The preprocessor comes with some pre-defined functions to assist metaprogramming.

static_error

Used to throw compile-time errors:

##[[
-- check the current Lua version in the preprocessor
if _VERSION ~= 'Lua 5.4' then
  static_error('not using Lua 5.4, got %s', _VERSION)
end
]]

static_assert

Used to throw compile-time assertions:

-- check the current Lua version in the preprocessor
## static_assert(_VERSION == 'Lua 5.4', 'not using Lua 5.4, got %s', _VERSION)

Generics

A generic is a special type created using a preprocessor function that is evaluated at compile time to generate a specialized type based on compile-time arguments. To do this the generalize macro is used. It is hard to explain in words, so take a look at this full example:

-- Define a generic type for creating a specialized FixedStackArray
## local make_FixedStackArray = generalize(function(T, maxsize)
  -- alias compile-time parameters visible in the preprocessor to local symbols
  local T = #[T]#
  local MaxSize <comptime> = #[maxsize]#

  -- Define a record using T and MaxSize compile-time parameters.
  local FixedStackArrayT = @record{
    data: [MaxSize]T,
    size: isize
  }

  -- Push a value into the stack array.
  function FixedStackArrayT:push(v: T)
    if self.size >= MaxSize then error('stack overflow') end
    self.data[self.size] = v
    self.size = self.size + 1
  end

  -- Pop a value from the stack array.
  function FixedStackArrayT:pop(): T
    if self.size == 0 then error('stack underflow') end
    self.size = self.size - 1
    return self.data[self.size]
  end

  -- Return the length of the stack array.
  function FixedStackArrayT:__len(): isize
    return self.size
  end

  -- return the new defined type to the compiler
  ## return FixedStackArrayT
## end)

-- define FixedStackArray generic type in the scope
local FixedStackArray: type = #[make_FixedStackArray]#

do -- test with 'integer' type
  local v: FixedStackArray(integer, 3)

  -- push elements
  v:push(1)
  v:push(2)
  v:push(3)
  -- uncommenting would trigger a stack overflow error:
  -- v:push(4)

  -- check the stack array length
  assert(#v == 3)

  -- pop elements checking the values
  assert(v:pop() == 3)
  assert(v:pop() == 2)
  assert(v:pop() == 1)
  -- uncommenting would trigger a stack underflow error:
  -- v:pop()
end

do -- test with 'number' type
  local v: FixedStackArray(number, 3)

  -- push elements
  v:push(1.5)
  v:push(2.5)
  v:push(3.5)
  -- uncommenting would trigger a stack overflow error:
  -- v:push(4.5)

  -- check the stack array length
  assert(#v == 3)

  -- pop elements checking the values
  assert(v:pop() == 3.5)
  assert(v:pop() == 2.5)
  assert(v:pop() == 1.5)
  -- uncommenting would trigger a stack underflow error:
  -- v:pop()
end

Generics are powerful for specializing efficient code at compile time based on different compile-time arguments. They are used in many places in the standard library to, for example, create the vector sequence and span classes. Generics are similar to C++ templates.

Generics are memoized, that is, they are evaluated and defined just once for the same compile-time arguments.

Concepts

Concepts are a powerful system used to specialize polymorphic functions with efficiency at compile-time.

An argument of a polymorphic function can use the special concept type defined by a preprocessor function that, when evaluated at compile time, decides whether the incoming variable type matches the concept requirements.

To create a concept, use the preprocessor function concept:

local an_scalar = #[concept(function(attr)
  -- the first argument of the concept function is an Attr,
  -- attr are stores different attributes for the incoming symbol, variable or node,
  -- we want to check if the incoming attr type matches the concept
  if attr.type.is_scalar then
    -- the attr is an arithmetic type (can add, subtract, etc)
    return true
  end
  -- the attr type does not match this concept
  return false
end)]#

local function add(x: an_scalar, y: an_scalar)
  return x + y
end

print(add(1, 2)) -- outputs 3

-- uncommenting the following will trigger the compile error:
--   type 'boolean' could not match concept 'an_scalar'
-- add(1, true)

When the concepts of a function are matched for the first time, a specialized function is defined just for those incoming types, thus the compiler generates different functions in C code for each different match. This means that the code is specialized for each type and is handled efficiently because the code does not need to do runtime type branching (the type branching is only done at compile time).

The property type.is_scalar is used here to check the incoming type. All the properties defined by the compiler to check the incoming types can be seen here.

Specializing with concepts

A concept can match multiple types, thus it is possible to specialize a polymorphic function further using a concept:

require 'string'

local an_scalar_or_string = #[concept(function(attr)
  if attr.type.is_stringy then
    -- we accept strings
    return true
  elseif attr.type.is_scalar then
    -- we accept scalars
    return true
  end
  return false
end)]#

local function add(x: an_scalar_or_string,
                   y: an_scalar_or_string)
  ## if x.type.is_stringy and y.type.is_stringy then
    return x .. y
  ## else
    return x + y
  ## end
end

-- add will be specialized for scalar types
print(add(1, 2)) -- outputs 3
-- add will be specialized for string types
print(add('1', '2')) -- outputs 12

The compiler only defines new different specialized functions as needed, i.e. specialized functions for different argument types are memoized.

Specializing concepts for records

Sometimes you may want to check whether a record matches a concept. To do this you can set a field on its type to later check in the concept, plus you can use it in the preprocessor to assist in specializing code:

local Vec2 = @record{x: number, y: number}
-- Vec2 is an attr of the "type" type, Vec2.value is it's holded type
-- we set here is_Vec2 at compile-time to use later for checking whether a attr is a Vec2
## Vec2.value.is_Vec2 = true

local Vec2_or_scalar_concept = #[concept(function(attr)
  -- match in case of scalar or Vec2
  return attr.type.is_scalar or attr.type.is_Vec2
end)]#

-- we use a concepts on the metamethod __add to allow adding Vec2 with numbers
function Vec2.__add(a: Vec2_or_scalar_concept, b: Vec2_or_scalar_concept)
  -- specialize the function at compile-time based on the argument type
  ## if a.type.is_Vec2 and b.type.is_Vec2 then
    return (@Vec2){a.x + b.x, a.y + b.y}
  ## elseif a.type.is_Vec2 then
    return (@Vec2){a.x + b, a.y + b}
  ## elseif b.type.is_Vec2  then
    return (@Vec2){a + b.x, a + b.y}
  ## end
end

local a: Vec2 = {1, 2}
local v: Vec2
v = a + 1 -- Vec2 + scalar
print(v.x, v.y) -- outputs: 2 3
v = 1 + a -- scalar + Vec2
print(v.x, v.y) -- outputs: 2 3
v = a + a -- Vec2 + Vec2
print(v.x, v.y) -- outputs: 2 4

Concepts with logic

You can put some logic in your concept to check for any kind of proprieties that the incoming attr should satisfy, and to return compile-time errors explaining why the concept didn’t match:

-- Concept to check whether a type is indexable.
local indexable_concept = #[concept(function(attr)
  local type = attr.type
  if type.is_pointer then -- accept pointer to containers
    type = type.subtype
  end
  -- we accept arrays
  if type.is_array then
    return true
  end
  -- we expect a record
  if not type.is_record then
    return false, 'the container is not a record'
  end
  -- the record must have a __index metamethod
  if not type.metafields.__index then
    return false, 'the container must have the __index metamethod'
  end
  -- the record must have a __len metamethod
  if not type.metafields.__len then
    return false, 'the container must have the __len metamethod'
  end
  -- concept matched all the imposed requirements
  return true
end)]#

-- Sum all elements of any container with index beginning at 0.
local function sum_container(container: indexable_concept)
  local v: integer = 0
  for i=0,<#container do
    v = v + container[i]
  end
  return v
end

-- We create our customized array type.
local MyArray = @record{data: [10]integer}
function MyArray:__index(i: integer)
  return self.data[i]
end
function MyArray:__len()
  return #self.data
end

local a: [10]integer = {1,2,3,4,5,6,7,8,9,10}
local b: MyArray = {data = a}

-- sum_container can be called with 'a' because it matches the concept
-- we pass as reference using & here to avoid an unnecessary copy
print(sum_container(&a)) -- outputs: 55

-- sum_container can also be called with 'b' because it matches the concept
-- we pass as reference using & here to avoid an unnecessary copy
print(sum_container(&b)) -- outputs: 55

Concept that infers to another type

Sometimes it is useful to infer a concept to a different type from the incoming attr. For example, suppose you want to specialize a function that optionally accepts any kind of scalar, but you really want it to be implemented as a number:

local facultative_number_concept = #[concept(function(attr)
  if attr.type.is_niltype then
    -- niltype is the type when the argument is missing or when we use 'nil'
    -- we accept it because the number is facultative
    return true
  end
  -- instead of returning true, we return the desired type to be implemented,
  -- the compiler will take care to implicitly cast the incoming attr to the desired type,
  -- or throw an error if not possible,
  -- here we want to force the function using this concept to implement as a 'number'
  return primtypes.number
end)]#

local function get_number(x: facultative_number_concept)
  ## if x.type.is_niltype then
    return 0.0
  ## else
    return x
  ## end
end

print(get_number(nil)) -- prints 0.0
print(get_number(2)) -- prints 2.0

Facultative concept

Facultative concepts are commonly used, thus there is a shortcut for creating them. For instance, the previous code is equivalent to this:

local function get_number(x: facultative(number))
  ## if x.type.is_niltype then
    return 0
  ## else
    return x
  ## end
end

print(get_number(nil)) -- prints 0
print(get_number(2)) -- prints 2

Use this when you want to specialize optional arguments at compile-time without any runtime costs.

Overload concept

Using concepts to overload functions for different incoming types at compile time is a common use, so there is also a shortcut for creating overload concepts:

local function foo(x: overload(integer,string,niltype))
  ## if x.type.is_integral then
    print('got integer ', x)
  ## elseif x.type.is_string then
    print('got string ', x)
  ## else
    print('got nothing')
  ## end
end

foo(2) -- outputs: got integer 2
foo('hello') -- outputs: got string hello
foo(nil) -- outputs: got nothing

Use this when you want to specialize different argument types at compile time without runtime costs.

Annotations

Annotations are used to prompt the compiler to behave differently during code generation.

Function annotations

local function sum(a: integer, b: integer) <inline> -- C inline function
  return a + b
end
print(sum(1,2)) -- outputs: 3

Variable annotations

local a: integer <noinit>-- don't initialize variable to zero
a = 0 -- manually initialize to zero
print(a) -- outputs: 0

local b <volatile> = 1 -- C volatile variable
print(b) -- outputs: 1

C interoperability

Nelua provides many utilities to interoperate with C code.

Importing C functions

To import a C function you must use the <cimport> annotation:

-- import "puts" from C library
local function puts(s: cstring <const>): cint <cimport>
  -- cannot have any code here, because this function is imported
end

puts('hello') -- outputs: hello

The above code generates exactly this C code:

/* ------------------------------ DECLARATIONS ------------------------------ */
int puts(const char* s);
static int nelua_main(int argc, char** argv);
/* ------------------------------ DEFINITIONS ------------------------------- */
int nelua_main(int argc, char** argv) {
  puts("hello");
  return 0;
}

Notice that the puts function is declared automatically, i.e., there is no need to include the header that declares the function.

Importing C functions declared in headers

Sometimes you need to import a C function that is declared in a C header, specially if it is declared as a macro:

-- `nodecl` is used because this function doesn't need to be declared by Nelua,
-- as it will be declared in <stdio.h> header
-- `cinclude` is used to make the compiler include the header when using the function
local function puts(s: cstring <const>): cint <cimport, nodecl, cinclude '<stdio.h>'>
end

puts('hello') -- outputs: hello

The above code generates exactly this C code:

#include <stdio.h>
/* ------------------------------ DECLARATIONS ------------------------------ */
static int nelua_main(int argc, char** argv);
/* ------------------------------ DEFINITIONS ------------------------------- */
int nelua_main(int argc, char** argv) {
  puts("hello");
  return 0;
}

Notice that the nodecl is needed when importing any C function that is declared in a C header, otherwise the function will have duplicate declarations.

Including C files with defines

Sometimes you need to include a C file while defining something before the include:

-- link SDL2 library
## linklib 'SDL2'
-- define SDL_MAIN_HANDLED before including SDL2
## cdefine 'SDL_MAIN_HANDLED'
-- include SDL2 header
## cinclude '<SDL2/SDL.h>'

-- import some constants defined in SDL2 header
local SDL_INIT_VIDEO: uint32 <cimport, nodecl>

-- import functions defined in SDL2 header
local function SDL_Init(flags: uint32): int32 <cimport, nodecl> end
local function SDL_Quit() <cimport, nodecl> end

SDL_Init(SDL_INIT_VIDEO)
SDL_Quit()

Importing C functions using a different name

The <cimport> annotation uses the same name as its symbol name, but it is possible to import the function under a different name:

-- we pass the C function name as a parameter for `cimport`
local function c_puts(s: cstring): cint <cimport 'puts', nodecl, cinclude '<stdio.h>'>
end

c_puts('hello') -- outputs: hello

Linking a C library

When importing a function from a C library you also need to link the library, to do this use the linklib function in the preprocessor:

-- link the SDL2 library when compiling
## linklib 'SDL2'

local function SDL_GetPlatform(): cstring <cimport> end

print(SDL_GetPlatform()) -- outputs your platform name (Linux, Windows, ...)

Notice that we didn’t need to include the SDL header in the above example, we could, but we let Nelua declare the function.

Passing C flags

It is possible to add custom C flags when compiling via the preprocessor:

##[[
if FAST then -- release build
  cflags '-Ofast' -- C compiler flags
  ldflags '-s' -- link flags
else -- debug build
  cflags '-Og'
end
]]

If we run the above example with nelua -DFAST example.nelua the C compiler will compile with the cflags -Ofast otherwise -Og.

Emitting raw C code

Sometimes to do low level things in C, or to avoid Nelua’s default semantics, you may want to emit raw C code:

local function do_stuff()
  -- make sure `<stdio.h>` is included
  ## cinclude '<stdio.h>'

  -- emits in the directives section of the generated C file
  ## cinclude [[#define HELLO_MESSAGE "hello from C"]]

  -- emits in the declarations section of the generated C file
  ## cemitdecl [[static const char* get_hello_message();]]

  -- emits in the definitions section of the generated C file
  ## cemitdefn [[const char* get_hello_message() { return HELLO_MESSAGE; }]]

  -- emits inside this function in the generated C file
  ##[==[ cemit [[
    printf("%s\n", get_hello_message());
  ]] ]==]
end

do_stuff()

Nelua can emit C code in 4 different sections:

  • In the directives section with cinclude, this is where C include and defines are emitted.
  • In the declarations section with cemitdecl, this is where functions and variables names are declared.
  • In the definitions section with cemitdefn, this is where functions are defined.
  • In the current scope with cemit, this emits in the current scope context, and local variables should be accessible.

Exporting named C functions

You can use Nelua to create C libraries. When doing this, you may want to fix the name of the generated C function and export it:

-- `cexport` marks this function to be exported
-- `codename` fix the generated C code name
local function foo() <cexport, codename 'mylib_foo'>
  return 1
end

The above code generates exactly this C code:

/* ------------------------------ DECLARATIONS ------------------------------ */
extern int64_t mylib_foo();
/* ------------------------------ DEFINITIONS ------------------------------- */
int64_t mylib_foo() {
  return 1;
}

C primitives

For importing C functions, additional primitive types are provided for compatibility:

Type C Type Suffixes
cshort short _cshort
cint int _cint
clong long _clong
clonglong long long _clonglong
cptrdiff ptrdiff_t _cptrdiff
cchar char _cchar
cschar signed char _cschar
cuchar unsigned char _cuchar
cushort unsigned short _cushort
cuint unsigned int _cuint
culong unsigned long _culong
culonglong unsigned long long _culonglong
csize size_t _csize
clongdouble long double _clongdouble
cstring char* _cstring

Use these types for importing C functions only. For normal code, use the other Nelua primitive types.

Libraries »