Minilang has a simple syntax, similar to Lua, Pascal and Oberon-2. Keywords are in lower case, statements are delimited by semicolons ;, these can be and are usually omitted at the end of a line.

As Minilang is designed to be embedded into larger applications, there is no implicit requirement to store Minilang code in source files with any specific extension (or indeed to store Minilang in files at all). However, .mini is used for the sample and test scripts and there are highlighters created for GtkSourceview and Visual Studio Code that recognize this extension.

The following is an example of Minilang code showing an implementation of the Fibonacci numbers.

 1fun fibonacci(N) do
 2   if N <= 0 then
 3      error("RangeError", "N must be postive")
 4   elseif N <= 2 then
 5      ret 1
 6   else
 7      var A := 1, B := 1
 8      for I in 2 .. (N - 1) do
 9         var C := A + B
10         A := B
11         B := C
12      end
13      ret B
14   end
17for I in 1 .. 10 do
18   print('fibonacci({I}) = {fibonacci(I)}\n')
21print('fibonacci({0}) = {fibonacci(0)}\n')

This produces the following output:

fibonacci(1) = 1
fibonacci(2) = 1
fibonacci(3) = 2
fibonacci(4) = 3
fibonacci(5) = 5
fibonacci(6) = 8
fibonacci(7) = 13
fibonacci(8) = 21
fibonacci(9) = 34
fibonacci(10) = 55
Error: N must be postive


Line comments start with :> and run until the end of the line.

Block comments start with :< and end with >: and can be nested.

1:> This is a line comment.
4   This is a block comment
5   spanning multiple lines
6   that contains another :< comment >:

Identifiers and Keywords

Identifiers start with a letter or underscore and can contain letters, digits and underscores. Minilang is case sensitive.


The following identifiers are reserved as keywords.

1_ and case debug def do each else elseif end exit for fun
2if in is it let loop meth must next nil not old on or ref
3ret susp switch then to until var when while with

Whitespace and Line Breaks

Minilang code consists of declarations (variables and functions) and expressions to evaluate. The bodies of complex expressions such as if, for, etc, can contain multiple declarations and expressions, in any order. Both semicolons ; and line breaks can be used to separate declarations and expressions, however if a line break occurs where a token is required then it will be ignored. Other whitespace (spaces and tabs) have no significance other than to separate tokens or within string literals.

For example the following are equivalent as the semicolons are replaced by line breaks:

1do print("Hello "); print("world"); end
4   print("Hello ")
5   print("world")

The following are also equivalent as the line break occurs after an infix operator where at least one more token is required to complete the expression:

1let X := "Hello " + "world"
3let X := "Hello " +
4   "world"

However the following code is not equivalent to the code above as the line break occurs before the infix operator and hence no token is required to complete the expression:

1let X := "Hello "
2   + "world"

Instead the above code is equivalent to following where semicolons have been added to show the separate declaration and expression (with a prefix operation):

1let X := "Hello ";
3+ "world";


A block in is a group of expressions and declarations. A block returns the result of the last expression in the block. Every block creates a new identifier scope; identifiers declared in a block are not visible outside that block (although they are visible within nested blocks). Some constructs such as the bodies of if-expressions, for-expressions, etc, are always blocks. A do-expression wraps a block into a single expression.

When any code is loaded in Minilang, it is implicitly treated as a block.

 1var X := do
 2   let Y := 7
 3   print("Y = ", Y, "\n")
 4   Y - 5
 7if X = 2 then
 8   let Y := 10
 9   let Z := 11
10   print("X = ", X, "\n")
11   print("Y = ", Y, "\n")
12   print("Z = ", Z, "\n")
Y = 7
X = 2
Y = 10
Z = 11

The code above has three blocks;

  1. the body of do-expression,

  2. the then-clause of the if-expression,

  3. the top-level block containing the entire code.

The identifier X is declared in the top-level block and so is visible throughout the code. The identifier Y is declared twice in two separate blocks, each block sees its local definition. Finally, the identifier Z is only declared in the then-block and is only visible there.


All identifiers in Minilang (other than those provided by the compiler / embedding) must be explicitly declared. Declarations are only visible within their containing block and can be referenced before their actual declaration. This allows (among other things) mutually recursive functions.

There are 3 types of declaration:

  1. var Name binds Name to a new variable with an initial value of nil. Variables can be reassigned using Name := Expression. A variable declaration can optionally include an initial expression to evaluate and assign to the variable var Name := Expression, this is equivalent to var Name; Name := Expression.

  2. let Name := Expression binds Name to the result of evaluating Expression. Name cannot be reassigned later in the block, hence the intial expression is required.

  3. def Name := Expression binds Name to the result of evaluating Expression. Unlike a let-declaration, Expression is evaluated once only when the code is first loaded. Consequently, Expression can only contain references to identifiers that are visible at load time (e.g. global identifiers or other def-declarations).

Declarations are visible in nested blocks (including nested functions), unless they are shadowed by another declaration.

 1print('Y = {Y}\n') :> Y is nil here
 3var Y := 1 + 2
 5print('Y = {Y}\n') :> Y is 3 here
 7var X
10   X := 1 :> Sets X in surrounding scope
13print('X = {X}\n')
16   var X :> Shadows declaration of X
17   X := 2 :> Assigns to X in the previous line
18   print('X = {X}\n')
21print('X = {X}\n')
Y =
Y = 3
X = 1
X = 2
X = 1

Function Declarations

Functions are first class values in Minilang, they can be assigned to variables or used to initialize identifiers. For convenience, instead of writing let Name := fun(Args...) Body, we can write fun Name(Args...) Body. For example:

1fun fact(N) do
2   if N < 2 then
3      return 1
4   else
5      return N * fact(N - 1)
6   end

Note that this shorthand is only for let-declarations, if another type of declaration is required (var or def) then the full declaration must be written.

Compound Declarations

Minilang provides no language support for modules, classes and probably some other useful features. Instead, Minilang allows for these features to be implemented as functions provided by the runtime, with evaluation at load time to remove any additional overhead from function calls. Minilang provides some syntax sugar constructs to simplify writing these types of declaration.

Imports, Classes, etc.

A declaration of the form Expression: Name(Args...) is equivalent to def Name := Expression(Args...). This type of declaration is useful for declaring imported modules, classes, etc. For example:

1import: utils("lib/")
3class: point(:X, :Y)

More details can be found in Modules and Classes.

Exports, etc.

A declaration of the form Expression: Declaration is equivalent to Declaration; Expression("Name", Name) where Name is the identifier in the Declaration. Any type of declaration (var, let, def, fun or another compound declaration) is allowed. This form is useful for declaring exports. For example:

1export: fun add(X, Y) X + Y
3export: var Z

Compound declarations can be combined. For example, the following code creates and exports a class.

1export: class: point(:X, :Y)

Destructuring Declarations

Multiple identifiers can be declared and initialized with contents of a single aggregrate value (such as a tuple, list, map, module, etc). This avoids the need to declare a temporary identifier to hold the result. There are two forms of destructing declaration. Note that both forms can be used with var, let or def, for brevity only the let forms are shown below.

  1. let (Name₁, Name₂, ...) := Expression. Effectively equivalent to the following:

    1let Temp := Expression
    2let Name₁ := Temp[1]
    3let Name₂ := Temp[2]
  2. let (Name₁, Name₂, ...) in Expression. Effectively equivalent to the following:

    1let Temp := Expression
    2let Name₁ := Temp["Name₁"]
    3let Name₂ := Temp["Name₂"]


Other than declarations, everything else in Minilang is an expression (something that can be evaluated).


The simplest expressions are single values.




1, -257. Note that the leading - is parsed as part of a negative number, so that 2-1 (with no spaces) will be parsed as 2 -1 (and be invalid syntax) and not 2 - 1.


1.2, .13, -1.3e5.


true, false.


"Hello world!n", 'X = {X}'. Strings can be written using double quotes or single quotes. Strings written with single quotes can have embedded expressions (between { and }) and may span multiple lines (the line breaks are embedded in the string).

Regular Expressions

r".*.c" (case sensitive), ri".*.c" (case insenstive). Minilang uses TRE as its regular expression implementation, the precise syntax supported can be found here


[1, 2, 3], ["a", 1.23, [nil]]. The values in a list can be of any type including other lists and maps.


{"a" is 1, 10 is "string"}. The keys of a map have to be immutable and comparable (e.g. numbers, strings, tuples, etc). The values can be of any type.


(1, 2, 3), ("a", 1.23, [nil]). Like lists, tuples can contain values of any type. Tuple differ from lists by being immutable; once constructed the elements of a tuple cannot be modified. This allows them to be used as keys in maps. They can also be used for destructing assignments,


:length, :X, <>, +, :"[]". Methods consisting only of the characters !, @, #, $, %, ^, &, *, -, +, =, |, \\, ~, `, /, ?, <, > or . can be written directly without surrounding :" and ".


fun(A, B) A + B. If the last argument to a function or method call is an anonymous function then the following shorthand can be used: f(1, 2, fun(A, B) A + B) can be written as f(1, 2; A, B) A + B.

Dates and Times

T"2022-02-27", T"1970-01-01T12:34:56", T"1970-01-01T12:34:56+06". The optional time component can be separated by either T or a space \ ``. Timezones can be specified using ``+NN or -NN, and UTC can be specified using Z, if no timezone is specified then the time is interpreted in the local timezone.

Imaginary Numbers

2i, -0.5i. If Minilang is built with support for complex number, the suffix i can be used to denote \(\sqrt{-1}\). The identifier i is also defined as 1i but is not treated as a keyword (so code that uses i as an identifier will not break if run by a version of Minilang built with support for complex numbers).


U"1c0fec87-31bd-40b8-bc04-527f4b171412". If Minilang is built with support for UUIDs, UUID literals can be written directly in code.

Conditional Expressions

The expression A and B returns nil if the value of A is nil, otherwise it returns the value of B.

The expression A or B returns A if the value of A is not nil, otherwise it returns the value of B.


Both and-expressions and or-expressions only evaluate their second expression if required.

The expression not A returns nil if the value of A is not nil, otherwise it returns some (a value whose only notable property is being different to nil).

If Expressions

The if-expression, if ... then ... else ... end evaluates each condition until one has a value other than nil and returns the value of the selected branch. For example:

1var X := 1
2print(if X % 2 = 0 then "even" else "odd" end, "\n")

will print odd.

Multiple conditions can be included using elseif.

 1for I in 1 .. 100 do
 2   if I % 3 = 0 and I % 5 = 0 then
 3      print("fizzbuzz\n")
 4   elseif I % 3 = 0 then
 5      print("fizz\n")
 6   elseif I % 5 = 0 then
 7      print("buzz\n")
 8   else
 9      print(I, "\n")
10   end

The else-clause is optional, if omitted and every condition evaluates to nil then the if-expression returns nil.

Switch Expressions

There are two types of switch-expression in Minilang. The basic switch-expression chooses a branch based on an integer value, with cases corresponding to 0, 1, 2, ..., etc.

1switch Expression
3   Block
5   Block
7   Block

When evaluated, Expression must evaluate to a non-negative integer, n. The n + 1-th case block is then evaluated if present, otherwise the else block option is evaluated. If no else block is present, then the value nil is used.

The general switch-expression selects a branch corresponding to a value based on a specific switch provider.

1switch Expression: Provider
2case Expressions do
3   Block
4case Expressions do
5   Block
7   Block

Minilang includes switch providers for several basic types including numbers, strings and types. More details can be found in Switch Expressions.

Loop Expressions

A loop-expression, loop ... end evaluates its code repeatedly until an exit-expression is evaluated: exit Value exits a loop and returns the given value as the value of the loop. The value can be omitted, in which case the loop evaluates to nil.

1var I := 1
2print('Found fizzbuzz at I = {loop
3   if I % 3 = 0 and I % 5 = 0 then
4      exit I
5   end
6   I := I + 1

A next-expression jumps to the start of the next iteration of the loop.

If an expression is passed to exit, it is evaluated outside the loop. This allows control of nested loops, for example: exit exit Value or exit next.

A while-expression, while Expression, is equivalent to if not Expression then exit end. Similarly, an until-expression, until Expression, is equivalent to if Expression then exit end. An exit value can be specified using while Expression, Value or until Expression, Value.

For Expressions

The for expression, for Value in Collection do ... end is used to iterate through a collection of values.

1for X in [1, 2, 3, 4, 5] do
2   print('X = {X}\n')

If the collection has a key associated with each value, then a second variable can be added, for Key, Value in Collection do ... end. When iterating through a list, the index of each value is used as the key.

1for Key, Value in {"a" is 1, "b" is 2, "c" is 3} do
2   print('{Key} -> {Value}\n')

For loops can also use destructing assignments to simplify iterating over collections of tuples, lists, etc.

1for Key, (First, Second) in {"a" is (1, 10), "b" is (2, 20), "c" is (3, 30)} do
2   print('{Key} -> {First}, {Second}\n')

A for loop is also an expression (like most things in Minilang), and can return a value using exit, while or until. Unlike a basic loop expression, a for loop can also end when it runs out of values. In this case, the value of the for loop is nil. An optional else clause can be added to the for loop to give a different value in this case.

1var L := [1, 2, 3, 4, 5]
3print('Index of 3 is {for I, X in L do until X = 3, I end}\n')
4print('Index of 6 is {for I, X in L do until X = 6, I end}\n')
5print('Index of 6 is {for I, X in L do until X = 6, I else "not found" end}\n')
Index of 3 is 3
Index of 6 is
Index of 6 is not found


For loops are not restricted to using lists and maps. Any value can be used in a for loop if it is sequence, i.e. can generate a sequence of values (or key / value pairs for the two variable version).

In order to loop over a range of numbers, Minilang has a range type, created using the .. operator.

1for X in 1 .. 5 do
2   print('X = {X}\n')
X = 1
X = 2
X = 3
X = 4
X = 5

The default step size is 1 but can be changed using the :by method.

1for X in 1 .. 10 by 2 do
2   print('X = {X}\n')
X = 1
X = 3
X = 5
X = 7
X = 9

Minilang provides many other types of sequences as well as functions that construct new sequences from others. More details can be found in Sequences.


Functions in Minilang are first class values. That means they can be passed to other functions and stored in variables, lists, maps, etc. Functions have access to variables in their surrounding scope when they were created.

The general syntax of a function is fun(Name₁, Name₂, ...) Expression. Calling a function is achieved by the traditional syntax Function(Expression, Expression, ...).

1let add := fun(A, B) A + B
2let sub := fun(A, B) A - B
4print('add(2, 3) = {add(2, 3)}\n')
add(2, 3) = 5

Note that Function can be a variable containing a function, or any expression which returns a function.

1var X := (if nil then add else sub end)(10, 3) :> 7
3let f := fun(A) fun(B) A + B
5var Y := f(2)(3) :> 5

As a shorthand, the code var Name := fun(Name₁, Name₂, ...) Expression can be written as fun Name(Name₁, Name₂, ...) Expression. Internally, the two forms are identical.

1fun add(A, B) A + B

Although a function contains a single expression, this expression can be a block expression, do ... end. A block can contain any number of declarations and expressions, which are evaluated in sequence. The last value evaluated is returned as the value of the block. A return expression, ret Expression, returns the value of Expression from the enclosing function. If Expression is omitted, then nil is returned.

1fun fact(N) do
2   var F := 1
3   for I in 1 .. N do
4      F := F * I
5   end
6   ret F

When calling a function which expects another function as its last parameter, the following shorthand can be used:

1f(1, 2, fun(A, B) do
2   ret A + B

can be written as

1f(1, 2; A, B) do
2   ret A + B


Minilang functions can be used as generators using suspend expressions, susp Key, Value. If Key is omitted, nil is used as the key. The function must return nil when it has no more values to produce.

 1fun squares(N) do
 2   for I in 1 .. N do
 3      susp I, I * I
 4   end
 5   ret nil
 8for I, S in squares(10) do
 9   print('I = {I}, I² = {S}\n')
I = 1, I² = 1
I = 2, I² = 4
I = 3, I² = 9
I = 4, I² = 16
I = 5, I² = 25
I = 6, I² = 36
I = 7, I² = 49
I = 8, I² = 64
I = 9, I² = 81
I = 10, I² = 100


Every value in Minilang has an associated type. The type of a value can be obtained by calling type(Value).

1print(type(10), "\n")
2print(type("Hello"), "\n")
3print(type(integer), "\n")
4print(type(type), "\n")

Types are displayed as their names enclosed between << and >>. Note that type is itself a type (whose type is itself, type). Most types can be called as functions which return instances of that type based on the arguments passed.

For example:

boolean(X), integer(X), real(X), number(X), string(X), regex(X)

Convert X to an integer, real, number (integer or real), string or regular expression respectively.

list(X), map(X)

These expect X to be sequence and the values (and keys) produced by X into a list or map respectively.

tuple(X₁, X₂, ...)

Constructs a new tuple with values X₁, X₂, ....

method(X), method()

Returns the (unique) method with name X. If no name is passed then a completely new anonymous method is returned.


Returns the type of X.


Returns a new stringbuffer.


Minilang can optionally be built with support for generic types such as list[integer], map[string, tuple[string, number]], etc. More details can be found in Types.

Classes, Enums and Flags

Minilang can optionally be built with support for user-defined types, using the class, enum or flags types. More details can be found in Classes.


Methods are first class objects in Minilang. They can be created using a colon : followed by one or more alphanumeric characters, or any combination of characters surrounded by quotes.

Methods consisting of only the characters !, @, #, $, %, ^, &, *, -, +, =, |, \, ~, `, /, ?, <, > or . can be written directly, without any leading : or quotes.

Methods behave as atoms, that is two methods with the same characters internally point to the same object, and are thus identically equal.

3:"write" :> same as previous method

Methods provide type-dependant function calls. Each method is effectively a mapping from lists of types to functions. When called with arguments, a method looks through its entries for the best match based on the types of all of the arguments and calls the corresponding function. More information on how methods work can be found in Methods.

1var L := []
2:put(L, 1, 2, 3)
3print('L = {L}\n')
L = 1 2 3

For convenience (i.e. similarity to other OOP languages), method calls can also be written with their first argument before the method. Thus the code above is equivalent to the following:

1var L := []
2L:put(1, 2, 3)
3print('L = {L}\n')

Methods with only symbol characters or that are valid identifiers can be invoked using infix notation. The following are equivalent:

1+(A, B)
2A + B
4+(A, *(B, C))
5A + (B * C)
7list(1 .. 10 limit 5)
8list(:limit(..(1, 10), 5))


Minilang allows any combination of symbol characters (listed above) as well as any identifier to be used as an infix operator. As a result, there is no operator precedence in Minilang. Hence, the parentheses in the last example are required; the expression A + B * C will be evaluated as (A + B) * C.


Minilang has optional support for macros, which allow code to be generated or modified during compilation.

When the compiler encounters any function call in code Func(Arg₁, Arg₂, ...) (here Func and Argᵢ are expressions), it checks if Func can be evaluated to a constant. If Func can be evaluated to a constant, and that constant is a macro then it applies the macro to Arg₁, Arg₂, ... as expression values. The macro must return another expression value which the compiler then compiles in place of the original function call.

Expression values can be constructed using the syntax :{Expr, Name₁ is Expr₁, Name₂ is Expr₂, ...}. The optional Nameᵢ is Exprᵢ pairs define named expressions which can be referenced in Expr as :$Nameᵢ.

Macros can be created using the macro constructor, the example below uses the compound declaration form described above. Note that macro expects a function, hence the ; in the definition of test.

 1macro: test(; Expr) :{do
 2   let X := 10
 3   let Y := "Hello world"
 4   :$Expr
 5end, Expr is Expr}
 7test(print('X = {X}, Y = {Y}\n'))
 9:> expands to
12   let X := 10
13   let Y := "Hello world"
14   print('X = {X}, Y = {Y}\n')

See Macros for more information.



The special built in value nil denotes the absence of any other value. Variables have the value nil before they are assigned any other value. Likewise, function parameters default to nil if a function is called with fewer arguments than parameters.


Although Minilang has boolean values, conditional and looping statements treat only nil as false and any other value as true.

 1:> Nil
 4if nil then
 5   print("This will not be seen!")
 8if 0 then
 9   print("This will be seen!")

Comparison operators such as =, >=, etc, return the second argument if the comparison is true, and nil if it isn't. Comparisons also return nil if either argument is nil, allowing comparisons to be chained.

11 < 2 :> returns 2
21 > 2 :> returns nil
41 < 2 < 3 :> returns 3
51 < 0 < 3 :> return nil


Numbers in Minilang are either integers (whole numbers) or reals (decimals / floating point numbers).

Integers can be written in standard decimal notation. Reals can be written in standard decimal notation, with either e or E to denote an exponent in scientific notation. If a number contains either a decimal point . or an exponent, then it will be read as a real number, otherwise it will be read as an integer.

They support the standard arithmetic operations, comparison operations and conversion to or from strings.

1:> Integers
 1:> Arithmetic
 21 + 1 :> 2
 32 - 1.5 :> 0.5
 42 * 3 :> 6
 54 / 2 :> 2
 63 / 2 :> 1.5
 75 div 2 :> 2
 85 mod 2 :> 1
10:> Comparison
111 < 2 :> 2
121 <= 2 :> 2
131 = 1.0 :> 1.0
141 > 1 :> nil
151 >= 1 :> 1
161 != 1 :> nil
18:> Conversion
19integer("1") :> 1
20real("2.5") :> 2.5
21number("1") :> integer 1
22number("1.1") :> real 1.1

See also number.


Minilang provides integer and real ranges. The Min .. Max operator returns an inclusive range from Min to Max.

1:> Construction
21 .. 10
310 .. 100 by 10
41 .. 10 by 0.5
51 .. 100 in 9

See also range.


Strings can be written in two ways:

Regular strings are written between double quotes ", and contain regular characters. Special characters such as line breaks, tabs or ANSI escape sequences can be written using an escape sequence \n, \t, etc.

Complex strings are written between single quotes ' and can contain the same characters and escape sequences as regular strings. In addition, they can contain embedded expressions between braces { and }. At runtime, the expressions in braces are evaluated and converted to strings. To include a left brace { in a complex string, escape it \{.

 1:> Regular strings
 2"Hello world!"
 3"This has a new line\n", "\t"
 5:> Complex strings
 6'The value of x is \'{x}\''
 7'L:length = {L:length}\n'
 9:> Conversion
10string(1) :> "1"
11string([1, 2, 3]) :> "[1, 2, 3]"
12string([1, 2, 3], ":") :> "1:2:3"

Returns the I-th character of String as a string of length 1.

String[I, J]

Returns the sub-string of String starting with the I-th character up to but excluding the J-th character. Negative indices are taken from the end of String. If either I or J it outside the range of String, or I > J then nil is returned.

See also string.

Regular Expressions

Regular expressions can be written as r"expression", where expression is a POSIX compatible regular expression.

1:> Regular expressions
4:> Conversion
5regex("[A-Za-z_]*") :> r"[A-Za-z_]*"

See also string.


Lists are extendable ordered collections of values, and are created using square brackets, [ and ]. A list can contain any value, including other lists, maps, etc.

 1:> Construction
 2let L1 := [1, 2, 3, 4]
 3let L2 := list(1 .. 10)
 5:> Indexing
 7L1[2] := 100
10:> Slicing
11L1[1, 3]
12L1[2, 4] := [11, 12]
14:> Methods
15L1:put(5, 6)
17L1:push(0, -1)
20L1 + L2
22:> Iteration
23for V in L1 do
24   print('V = {V}\n')
26for I, V in L1 do
27   print('I = {I}, V = {V}\n')
29for V in L1 do
30   V := old + 1

See also list.


Maps are extendable collections of key-value pairs, which can be indexed by its keys. Maps are created using braces { and }. Keys can be of any immutable type supporting equality testings (typically numbers and strings), and different types of keys can be mixed in the same map. Each key can only be associated with one value, although values can be any type, including lists, other maps, etc.

:> Construction
let M1 := {"A" is 1, "B" is 2}
let M2 := map("banana")

:> Indexing
M1["C"] := 3
print('D -> {M1["D", fun() 4]}\n')

:> Methods
M1:insert("E", 5)

See also map.