LIL is a Language Make games, nicely

Basic Values

Integers

There are four kinds of integers: i8, i16, i32, i64 and i128, which are whole numbers, with no decimals. The i stands for integer and the number is how many bits wide the value is.

var.i8 foo: 127;

Floats

There are two kinds of floating point types: f32 and f64, which are real numbers. The f stands for floating point and the number is how many bits wide the value is.

var.f32 bar: 65.3456;

Percentages:

In certain places, you can express a value as a percentage. The type is written the same as the other number types, but appending the percentage sign % at the end. The same sign needs to be at the end of a number literal for it to be a percentage.

width: 25%;

For example, storing a percentage in a var declaration where we force the type of the percentage to be a floating point, even though the literal value doesn't seem to be:

var.f64% myWidth: 100%;

Booleans:

Booleans are expressed using the identifiers true and false, and represent the logical values of 1 and 0, respectively.

var enabled: true;

Strings:

You use single '' or double "" quotes around the text that is in your string.

var hello: "Hello world";

Special symbols and escapes are written using a forward slash \ in front of a special character or using the same quote type as the string.

There is no difference between using one kind of quotes or another, just use whatever needs less escaping, like "Can't" instead of 'Can\'t'.

Here is a list of the characters and their meaning. It is not complete, look in further chapters for the whole list.

  • \\ the backslash itself
  • \n new line
  • \t tab
  • \b backspace
  • \0 C string termination character

String functions:

Use the percent character % followed by an identifier to insert another value into the string you are writing.

var myText: "This is the content";
var html: "<div>%myText</div>";

If you need to write out something more complex than a simple identifier, you can use curly braces and everything between them will be escaped. In this example, the app will print Welcome Mr. Joe Lilamassa to the standard output.

var name: "Joe";
var surname: "Lilamassa";
fn makeName(var name; var surname) {
	return "%name %surname";
}
print "Welcome Mr. %{ makeName(name, surname) }."

C Strings

Many times you have to interface with external code which expects old school character buffers like in the C language. To make things nicer, there is the cstr type, which is a sequence of characters with \0 at the end.

To write a literal c string in the source code, you can use the backtick symbols: `contents of string`.

Regular LIL strings have a cstr property that you can access which will give you the underlying character buffer.

var.str myString: "this is my LIL string";
functionWantingACString( myString.cstr );

Identifiers:

Identifiers are plain words, without any symbols, like foo, myThing or _x3, for example. They have to start with an underscore or a letter. They represent names of things like variables, properties or classes.

There are some reserved words that can't be used as an identifier, such as:

  • var
  • vvar
  • const
  • class
  • alias
  • type
  • true
  • false
  • if
  • else
  • switch
  • case
  • default
  • return
  • finally
  • for
  • loop
  • continue
  • repeat
  • break
  • null

Comments

Comments are used to add information to the source code which is intended to be read only by the humans reading the code. They start either with two forward slashes // or a forward slash and a star /*.

In the case of the double slash, we call them single line comments, because it is interpreted that everything between the slashes and the end of the line is the content of the comment. For example:

//this is a comment and it ends here

On the other hand, if the slash and star is used, the source code that follows will be interpreted as a comment until the oposite symbol is found, a star and a slash: */. For example:

/* this is the comment and
it can span multiple
lines until the end */

Expressions

There are the basic math operations of sum, subtraction, multiplication and division, using the +, -, * and / symbols, respectively. There is also the modulo operator %, which returns the remainder of a division.

var.i64 myNum: 100 + 50;
var.i64 myNum2: myNum * 2;

Expressions can be wrapped in parenthesis (<lhs> <sign> <rhs>), in some cases to desambiguate, and others just to increase legibility.

Comparisons

Comparisons are expressions that return a boolean value containing wether the condition was met. There are the following types of comparisons:

  • equal =
  • bigger than >
  • bigger or equal >=
  • lower than <
  • lower or equal <=

This is an example where comparisons are used together with if else statements:

if (myVar <= 100) {
	//do something, smaller values
} else if ((myVar > 100) AND (myVar <= 200)) {
	//do something else, mid range
} else {
	//highest values
}

Unary expressions

Often you want to directly apply the result of an expresssion to the same variable that is being read from. The word unary means "one operand", and the syntax merges expressions and assignments, all in one:

theValue +: 100;
myVal *: 2;
foo /: 5;

If you come from C, it's easy to know how to write these, just substitute = by :.

Function definitions

Functions are written using the fn keyword, followed by the name of the function and then optionally followed by variable declarations wrapped in parenthesis fn doStuff ( var myArg ) { <statements...> }, and then a list of commands inside a block delimited by curly braces { }.

fn sumOfTwoNumbers (var.i32 numA; var.i32 numB) {
	return numA + numB;
};

Function calls

The functions that have been declared with a name are called using that name, followed by a pair of parentheses containing the arguments to be passed in to the function. The arguments themselves are separated by either commas or semicolons, and a trailing one is allowed.

When you write just the values, they need to be in the same order as in the declaration. When written using assignments they can be in any order.

Variable declarations

A variable declaration defines a a space in memory that can hold a value. You declare it with a name which you will use later to access its content. It can be initialized with a value or left empty and assigned later.

To declare a variable you write the var keyword, followed by some whitespace, followed by the name of the variable. Then a colon : and a value or a semicolon to denote the end.

var foo: 1;

You can provide the type of the variable, so no guessing needs to take place, by writing a dot . after the var keyword.

var.i8 foo: 1;

Assignments

In LIL the assignment is written with a colon :, not an equal sign. In the following example we declare a variable named myString, then check if foo is equal to 1 (a single = is a comparator) and then assign a different string depending on the case. The compiler will use type inference to know that myString is a variable of the correct type.

var myString;
if foo = 1 {
	myString: "Hello world";
} else {
	myString: "Foo was not one";
}

Classes

These are like a "blueprint" to make identical, but separate, copies of various other values put together as a unit, which we call object instances.

They are written using the class keyword, followed by whitespace, then the "object symbol" @, and then the name of the class. A semicolon afterwards is allowed but not required. For example:

class @myClass {
	// more stuff here
}

Member variables (fields)

Since an object is a group of values, you have to specify each field as a variable declaration. For example, the following class contains an integer, a floating point value and another object of class @string:

class @myClass2 {
	var.i64 id;
	var.f32 value;
	var.@string description;
}

Member functions (methods)

When you put functions inside a class they are called methods. These are said to be "called on" an object instance. You typically use a value path ending in a function call to invoke them. Inside the method the special selector object @self is available, which is a pointer to the object instance itself.

The next example shows a class with a method which takes a boolean as argument and prints a message if true is passed:

class @myClass3 {
	var.@string message;
	fn printMessage (var.bool areYouSure) {
		if areYouSure {
			print @self.message;
		}
	};
}

var myObjectInstance: @myClass3 { message: "This is the message" };
myObjectInstance.printMessage(true);

Value paths

Value paths are used to access properties and methods of objects. They are written as a series of components of a larger unit:

First comes either a name or one of the special selector objects, like @self or @root.

If there are more components, you use a dot . as a separator and then the next piece. Whitespace between components and the dot is not allowed.

For example:

var myString: "hello";
print myString.length;

Here we are calling the length method of the myString object

Pointers

Pointers are the representation of a memory address. You can address individual bytes and do additions and subtractions on them. They usually carry a type, which means that when you dereference a pointer (load its contents), the values at the memory location will be interpreted as being a value of such type. There is also the secial type any which can be implicitly converted to other pointer types when they are assigned.

//define the local variable we want to address
var.i64 myNumber: 123;

//take the address of myNumber
var.ptr(i64) myPtr: pointerTo myNumber;

//alter the value
doSomethingThatChangesTheNumber(myPtr);

//prints the changed value
print valueOf myPtr;

Casts

To cast a value means converting it to another type. You use the fat arrow followed by the resulting type. Note that not all values can be converted.

theValue => type;

(getAge() + 20) => i32;

When you cast an integer to a floating point number, for example, or when converting between bit widths, like going from i32 to i64, the values are transformed. If you convert between pointer types, the loaded values will be interpreted as the new type directly, which is usually called type punning.

Some operations might be lossy. For example, if you convert an f64 into a f32 there is a loss of precision. If you were to convert back to a f64 you could end up with a different value with less decimals.

var.f64 myFloatVar: 100.435659888428;
var.i32 myInteger: myFloatVar => i32; //100
doStuffWith32BitFloat( myFloatVar => f32 )

Multiple types

var.i32|f32|bool|null foo: true;

Namespaces

//variable namespace
myVar, foo, printf
//class namespace
@root, @value, @array
//snippet namespace
#snippet mySnippet, #paste mySnippet

Alias, types and conversions

alias myPointerTy => ptr(ptr(i8));
type m => i64

conversion var.m theValue => mm {
	return theValue * 1000;
}

Arrays

var myArr: 1, 2, 3, 4, 5;
var.[3 x i32] foo: 100, 200, 300;
var.@array(f64) myArray: [];
var bar: []f32;

Flow control

for
if/else
loop
if cast
switch
type switch
finally

Flow control calls

return
repeat
continue
break

The @root object

@root {
	width: 1440;
	height: 900;
	background: #0;
}
var root: $(@root);

Rules

target {
	property: value;
}

Selectors

name
:filter
::state
combinators

Filters

foo:first { }
bar:last { }
baz:even { }

States

foo::enabled { }
bar::target { }
baz::whatever { }

Combinators

foo + bar { }
baz - zig { }
zag = zug { }

Instructions

#new
#0 / #FF / #CCC / #F0F1 / #FF00FF1 / #FF00FF05
#arg
#configure / #getConfig
#import / #needs / #export
#if
#sippet / #paste
#bug
#expand
#resource
#gpu

Snippets

#snippet myButton {
	width: 200;
	height: 400;
	background: #C;
}

@root {
	#new @container test {
		#paste myButton;
	}
}

Foreign languages

<llvm>

define i32 @foo(i32 %numA, i32 %numB) {
entry:
  %0 = add i32 %numB, %numA
  ret i32 %0
}

</llvm>