The Language

This is a quick overview of the language. I do not feel like writing a full tutorial or language reference, but this is a good place to start.

Metis is heavily based off of Lua and Python, so I will be comparing Metis with them often.

Variables

Local variables are declared with let:

let x = 5

Global variables are declared with let global:

let global x = 5

The difference is that local variables are only visible in the current scope, while global variables can be accessed from anywhere. Global variables are also stored in a global table, so they can be accessed from other files.

Variables may be updated using the = operator:

x = 6

Variables may also be updated using the +=, -=, *=, /=, //=, %=, &=, |=, ^=, <<=, >>=, >>>=, and ?:= operators. These operators serve as the combination of assignment and their respective binary operator. For example, x += 1 is the same as x = x + 1. ?:= is officially called the walris operator, and acts like a compounded ?: operator; x ?:= 1 will set x to 1 if x is null, and leave it unchanged otherwise.


let x = 5
x += 1
print(x) # 6

let y = { "z" = 5 }
y.z += 1
print(y.z) # 6

Functions

Functions are declared with fn:

fn add(a, b) return a + b end

Short, single expression functions can be declared with =:

fn add(a, b) = a + b

Functions can be called with ():

add(1, 2)

Functions can be declared global by prepending global:

global fn add(a, b) return a + b end

They follow the same scoping rules as variables.

Functions can also have upvalues, which are variables that are from an enclosing scope. For example:


let x = 5

fn printX()
    print(x)
end

printX() # 5

Upvalues can also be mutated:


let x = 5

fn addToX(y)
    x = x + y
end

addToX(5)
print(x) # 10

If a function is called with missing arguments, the missing arguments are set to null:


fn add(a, b, c)
    print(a)
    print(b)
    print(c)
end

add(1, 2) # 1, 2, null

This can be used to provide default values with the help of the ?: operator:


fn add(a, b, c)
    a = a ?: 0
    b = b ?: 0
    c = c ?: 0
    print(a)
    print(b)
    print(c)
end

add(1, 2) # 1, 2, 0

If the first argument is named self, then that function is considered a self function. When getting a self function from a table, the table is passed as the first argument:


let x = { "a" = 1, "b" = 2 }

fn x.getA(self)
    return self.a
end

print(x.getA()) # 1

Anonymous Functions

The syntax above is actually syntax sugar for assigning an anonymous function to a variable. Anonymous functions are declared with fn, but without a name:

fn(a, b) return a + b end
fn(a, b) = a + b

Functions are first class values, so they can be assigned to variables:

let add = fn(a, b) = a + b

Data Types

Metis has the following data types: null, boolean, number, string, list, table, callable, native, bytes, error, and coroutine. You can use the type function to get the type of a value:

type(5) # "number"

Data types may also have metatables, which are tables that are used to look up missing keys. Metatables may also have metamethods, which are functions that are called when certain operations are performed on a value. For example, the + operator is actually a metamethod called __plus__ that is called when the + operator is used on a value.

Null

The null type has a single value, null. It is used to represent the absence of a value. It cannot be called, indexed, or iterated over. It is similar to nil in Lua. Due to the fact that it has a single instance, it is clearer (and faster) to use is/is not instead of ==/!= to check for null:


let x = null
if x is null
    print("x is null")
end

Booleans

The boolean type has two values, true and false. Its principal use is inside conditional expressions. Unlike other literals, true, false, and null are global variables; they can be reassigned, but this is not recommended.

Numbers

The number type represents a double precision floating point number. It is similar to number in Lua. There is no integer type, but you can use the math.floor function to convert a number to an integer. There are multiple ways to write numbers:


let x = 5
let y = 5.0
let z = 5e0

Strings

The string type represents a sequence of characters. Strings are immutable, so you cannot change them after they are created. String literals are enclosed in double quotes:

let x = "hello"

The following escape sequences are supported:

\nnewline
\rcarriage return
\ttab
\bbackspace
\fform feed
\\backslash
\"double quote
\xnnhexadecimal escape
\unnnnunicode escape

Lists

The list type represents a sequence of values. Lists are mutable, so you can change them after they are created. List literals are enclosed in square brackets:

let x = [1, 2, 3]

Lists can be indexed with []:

x[0] # 1

Lists can be iterated over with for:


for v in x
    print(v)
end

Tables

The table type represents a mapping from keys to values. They are also called “maps” or “dictionaries” in other languages. Tables are mutable, so you can change them after they are created. Table literals are enclosed in curly braces:

let x = { "a" = 1, "b" = 2 }

Tables can be indexed with []:

x["a"] # 1

Alternatively, you can use the . operator:

x.a # 1

Tables cannot be iterated over directly, but you can iterate over their keys or values:


for k in x:keys()
    print(k)
end


for v in x:values()
    print(v)
end

Callables

The callable type represents a function or a native function. It is similar to function in Lua. Callables can be called with ():


let x = fn(a, b) = a + b
x(1, 2) # 3

Natives

Natives are values which are backed by an object not written in Metis. They are similar to userdata in Lua. The object returned by string.builder is an example of a native (it is backed by a java.lang.StringBuilder object).

Bytes

The bytes type represents a sequence of bytes. Bytes are mutable, so you can change them after they are created. Byte literals are enclosed in single quotes:

let x = 'hello'

Bytes can be indexed with []:

x[0] # 104

Bytes can be iterated over with for:


for v in x
    print(v)
end

Bytes can be converted to strings using decode, and strings can be converted to bytes using encode:


let x = 'hello'
let y = x:decode() # "hello"
let z = y:encode() # 'hello'

Errors

The error type represents an error. Errors have a type, a message, and an optional “companion table”. Errors are created with the keyword error:

let err = error TheTypeOfTheError("a message") : { "key" = "value" }

Errors can be thrown with raise:

raise err

Errors can be caught with try:


try
    raise err
catch err = TheTypeOfTheError
    print("caught " + str(err))
    print(err.message)
    print(err.key)
end

Coroutines

The coroutine type represents a coroutine. It is similar to thread in Lua. Coroutines can be created with coroutine.create:


let x = coroutine.create(fn()
    print("hello")
end)

Coroutines can be run with coroutine.run:

x.run() # "hello"

Operators

Metis has a set of operators that may or may not be overloaded. They are listed in order of decreasing precedence:

[], (), and .: these are used to index tables and call callables. There is no way to overload calling, but as . is syntax sugar for [], . and [] can be overloaded using the metamethod __index__.

**: this is used to raise a number to a power. It can be overloaded using __pow__.

not, ~, and unary -: these are used to negate booleans and numbers, and to perform a bitwise NOT on numbers and bytes for ~. Only - and ~ can be overloaded using __neg__ and __bnot__.

*, /, //, and %: these are used to multiply, divide, floor divide, and modulo numbers. They can be overloaded using __times__, __div__, __floordiv__, and __mod__.

+ and -: these are used to add and subtract numbers. + is also used for concatenating strings and adding to lists and tables. They can be overloaded using __plus__ and __minus__.

<<, >>, and >>>: these are used to shift numbers left, right, and right with sign extension. They can be overloaded using __shl__, __shr__, and __shru__.

&: this is used to compute the bitwise AND of numbers and bytes objects. It can be overloaded using __band__.

|: this is used to compute the bitwise OR of numbers and bytes objects. It can be overloaded using __bor__.

^: this is used to compute the bitwise XOR of numbers and bytes objects. It can be overloaded using __bxor__.

..< and ..=: exclusive and inclusive range. Can be overloaded with __range__ and __inclRange__.

?:, also known as the elvis operator, is used to provide a default value in case a value is null. For example, x ?: 1 will return 1 is x is null, and x otherwise. It cannot be overloaded.

in, not in, is, and is not: these are used to check if a value is in a list or table, or if two values are the same exact object. in and not in can be overloaded using __contains__.

<, <=, >, and >=: these are used to compare values. They all use the same metamethod for overloading: __cmp__. The metamethod should return a negative number if the left value is less than the right value, a positive number if the left value is greater than the right value, and zero if the left value is equal to the right value.

== and !=: these are used to check if two values are equal or not. They can be overloaded using __eq__.

and: returns true if both values are true, and false otherwise. It cannot be overloaded. It also short circuits, so if the left value is false, the right value is not evaluated.

or: returns true if either value is true, and false otherwise. It cannot be overloaded. It also short circuits, so if the left value is true, the right value is not evaluated.

? else is the ternary operator. It is used as a shorter form of if:

x ? 1 else 2

The above code returns 1 if x is true, and 2 otherwise. It cannot be overloaded.

Control Flow

If Statements

If statements are declared with if:


if x
    print("x is true")
end

If statements can have an else clause:


if x
    print("x is true")
else
    print("x is false")
end

In order to facilitate chaining, elif is also supported:


if x
    print("x is true")
elif y
    print("y is true")
else
    print("x and y are false")
end

While Loops

While loops are declared with while and a condition:


while x
    print("x is true")
end

They loop until the condition is false.

For Loops

For loops are declared with for, a variable name, in, and an iterable:


for x in [1, 2, 3]
    print(x)
end

All iterables implement the __iter__ metamethod, which returns an iterator. The iterator implements the next and hasNext metamethods (note the absence of underscores). next returns the next value, and hasNext returns true if there are more values, and false otherwise. The for loop calls next (and assigns it to the loop variable) until hasNext returns false.

Try Statements

Try statements are declared with try. They contain one or more catch clauses, and optionally a finally clause:


try
    raise error TheTypeOfTheError("a message") : { "key" = "value" }
catch err = TheTypeOfTheError
    print("caught " + str(err))
    print(err.message)
    print(err.key)
finally
    print("finally")
end

finally is always executed, even if an error is raised. catch clauses are executed for the type of the error raised. If the error is not caught, it is re-raised. Binding the error to a variable, such as the err variable above, is optional; the line can be written as catch TheTypeOfTheError.

Break and Continue

If you need to exit a loop early, you can use break:


for x in [1, 2, 3, 4]
    if x == 3
        break
    end
    print(x)
end

# Prints 1 and 2

If you need to skip the rest of the loop body, you can use continue:


for x in [1, 2, 3, 4]
    if x == 3
        continue
    end
    print(x)
end

# Prints 1, 2, and 4

Modules

Metis has a module system similar to Python. Modules have the same name as the file they are declared in. All globals in the module are part of its exports. Modules are loaded with the import statement:

import moduleName

This will load the module as a table and assign it to a variable with the same name as the module. The module table will have the same members as the modules exports, or globals in the module. For example, if the module is called foo, it will be assigned to a variable called foo.

Another thing you can do is global imports with global import:

global import moduleName

This will load the module and assign it to a global variable with the same name as the module. This can be used for implementing transient modules as such:


# foo.metis
global import bar

# bar.metis
let global x = 1

# main.metis
import foo
print(foo.bar.x) # 1

Standard Library

The standard library is split into two main parts: the core library and the standard library. The core library is always loaded, and contains functions and modules that are necessary for the language to function. For example, the print function is in the core library. The core library is documented here.

The standard library is not always loaded, and contains all the other modules that are not necessary for the language to function. For example, the math module is in the standard library. The standard library is documented here.

There is not much more to Metis, so I will end this here. If you have any questions, feel free to ask me in The Garbage Collector (ping @Seggan).