Appearance
Language tutorial for Lumitare
Welcome to the official language tutorial of the Lumitare programming language, where we try to explain the various concepts, mechanisms and tools of this language throughout, successively introducing more and more complex constructs and investigating the interaction between them. Have fun!
Hello World
Let's start with a simple "Hello World" program in its most minimal form.
In the respective directory root you want to create the application in, create a file named hello.lumi and paste the following code into it:
lumi
entrypoint := () => {
"Hello World!" -> write(stdout);
};Now, assuming you installed the Lumitare compiler already, navigate to the directory the source file is located in and invoke the compiler while passing in the file name as its argument!
console
foo@bar: /path/to/dir$ lumi hello.lumi
Lumitare Compiler: Compilation successful!
Lumitare Compiler: The resulting file(s) can be found in '/path/to/dir/out/'!Next we just execute the resulting hello file in the ./out directory and should see the program we just created at work!
console
foo@bar: /path/to/dir$ ./out/hello
Hello World!Line and block comments
Lumitare supports the writing of comments. Comments are pieces of text that get ignored by the compiler when trying to translate the source code into an executable application.
The two basic kinds of comments in Lumitare are the line comment which gets initiated with two slashes (//) and ends at the end of the line and the block comment which gets initiated with a slash and an asterisk (/*) and has to be explicitly closed by an asterisk followed by a slash (*/).
Example:
lumi
//This is a line comment that gets ignored by the compiler...
entrypoint := () => {
/* ...much like...
...this block comment...
...stretching over...
...multiple lines! */
"Hello World!" -> write(stdout);
};Value literals
What we just hardcoded into the source code in the previous example as "Hello World!" is what is called a value literal!
More specifically "Hello World!" is a value literal of the STRING type.
There are also other types of value literals though, like plain integer numbers.
Try to adjust the code from the Hello World example as follows:
lumi
entrypoint := () => {
1337 -> write(stdout);
};After compilation and execution of the resulting file, the expected output of the program would be this:
console
1337Primitive data types
Lumitare has primitive support for the following types with the respective possible value literals or what they represent:
lumi
NONE - represents the lack of any value at all -
NULL null
TRUTH true | false
BYTE 0x00 to 0xFF
I32 -2147483648 to 2147483647
U32 0 to 4294967295
I64 -9223372036854775808 to 9223372036854775807
U64 0 to 18446744073709551615
F32 - a 32-bit floating point number -
F64 - a 64-bit floating point number -
STRING - any kind of character string -
FUNCTION - a callable unit of logic -In practice however these primitive data types are not meant to be used directly, with the interfacing between developer and data rather occuring via their respective wrapper types. More on that later though.
Constant data
Data, the singular of which is a datum, like our STRING value literal "Hello World!" or our I32 value literal 1337 from the earlier examples, can also be bound to an identifier.
Name binding in Lumitare occurs with the define keyword, whereby Lumitare generally doesn't allow the declaration of an identifier without defining its content straight away, with only a few exceptions.
Let's modify the previous example yet again to utilize name binding:
lumi
entrypoint := () => {
define myNumber := 1337;
myNumber -> write(stdout);
};Now the value 1337 is bound as a constant datum to the identifier myNumber and can thus be addressed with this name elsewhere within the same scope. This code should produce the exact same output as our example before did!
console
1337A constant datum can't be reassigned or mutated in any way! It represents completely immutable data which, for as long as the scope it got defined in exists, will be bound to the identifiers it was assigned to!
Furthermore there is the limitation that an identifier pointing at a constant datum has to abide the following naming convention:
Must start with a lower case character.
Can only contain lower case and upper case characters, as well as the digits of the decimal system.
Or put another way, the following regular expression is strictly enforced for identifiers pointing at constant data: ^[a-z][a-zA-Z0-9]*$
Function basics
Functions are callable units of logic with a well-defined interface and behavior that can be invoked multiple times.
In the previous examples we have repeatedly made use of a function, namely a function that was getting bound to the applications entrypoint, which is where a Lumitare application looks for any kind of a callable unit (read: function) that it can execute to start executing the application as a whole!
The reserved entrypoint identifier is one of the few exceptions alluded to earlier where a declaration of a datum can occur without an immediate definition, since it gets declared by the compiler itself with the developer then having to assign a constant datum that is of the function type.
Instead of the direct assignment of the function to be executed by the Lumitare application, we could've also written the following:
lumi
define myEntryPointFunction := () => {
define myNumber := 1337;
myNumber -> write(stdout);
};
entrypoint := myEntryPointFunction;This is possible because in Lumitare functions are considered to be first-class citizens, meaning they can be bound to identifiers, be used as input values for other functions and also get returned by them. More on what input and return values of functions are at a later point though.
Arithmetic operations
Lumitare also supports basic arithmetic operations, because what would be a computer that couldn't compute anything, right?!
The basic operations are as follows:
lumi
a + b Addition
a - b Subtraction
a * b Multiplication
a / b Division
a % b ModuloThe resulting values of these operations can also be bound to identifiers or used as input or return values for functions.
Let's modify our example from earlier and try it out!
lumi
entrypoint := () => {
define someNumber := 3;
define otherNumber := 5;
(someNumber + otherNumber) -> write(stdout);
define multiplicationResult := someNumber * otherNumber;
multiplicationResult -> write(stdout);
};Note: Due to the way the -> (stream piping) operator works, the inline addition of (someNumber + otherNumber) has to be encapsulated by parentheses. More on streams and piping at a later point though, since that is an advanced topic!
The expected result of this example code is:
console
8
15Arithmetic relationals
As much as Lumitare supports arithmetic operations, it also supports basic arithmetic relationals! Relationals are used to compare the values of two numbers with each other.
The supported relationals are as follows:
lumi
a = b a equals b
a != b a doesn't equal b
a < b a is less than b
a <= b a is less than or equal to b
a > b a is greater than b
a >= b a is greater than or equal to bThe comparison of two numbers with an arithmetic relational thereby always returns a TRUTH value, meaning either true if the given relation holds true for the two values or false if it doesn't.
Again, back to our example:
lumi
entrypoint := () => {
define someNumber := 3;
define otherNumber := 5;
//We changed the addition from the last example to a comparison here
(someNumber > otherNumber) -> write(stdout);
//And also the multiplicationResult has now become a comparisonResult!
define comparisonResult := someNumber < otherNumber;
comparisonResult -> write(stdout);
};The expected output for this code would be:
console
false
trueBasic if-then construct functionality
The if-then construct is a control flow mechanism which allows the creation of a logical branch in execution of a given programs logic that only gets traversed if a given condition is met.
A condition is hereby any kind of expression that evaluates to either true or false or is the respective value literal itself.
Note: For those developers coming from other languages it is important to note here that the if statement in Lumitare is called a discrete if! That means there is no else if or even else statement to accompany it! If conditional evaluation is required where one would use else if or else in other languages, Lumitare has an extremely powerful conceptualization of how the if keyword in conjunction with the then keyword is supposed to work, which eliminates the need for else or else if clutter in code bases!
In its simplest form, the if-then construct works exactly as known and expected from other languages.
In its very basic form it looks like this:
lumi
if condition then {
/* code that gets executed if condition evaluates to true */
};We will now successively, over the course of the next couple sections, change our example code from earlier to explore the different functionalities of the if-then construct bit by bit! So let's get at it:
lumi
entrypoint := () => {
define someNumber := 3;
define otherNumber := 5;
if someNumber > otherNumber then {
someNumber .. " is greater than " .. otherNumber .. "!" -> write(stdout);
};
if someNumber < otherNumber then {
someNumber .. " is less than " .. otherNumber .. "!" -> write(stdout);
};
};The expected output of this program would be:
console
3 is less than 5!Parantheses grouping and the distributive property
As is the case with so many other languages as well, one is able to group expressions via the usage of parentheses. These act both as a logical grouping for the visual parsing by humans reading through the code, as well as a safe-guard to ensure the compiler knows exactly how to interpret certain conditionals and expressions which might be ambiguous in their meaning.
The reason why this is important is that far from every operation a computer can execute actually possesses the distributive property, especially in relation to expressions of a different kind!
Distributive property hereby means, just in the plain mathematical sense, that it is possible to change up the way a given term is grouped by parentheses and thus the order in which it is evaluated.
In Lumitare it is generally advisable, if it might be even slightly ambiguous, to use parentheses for the grouping of expressions to ensure the compiler and runtime do exactly what the developer expects in regards to the evaluation order of a given expression!
Just consider the following two example code snippets representing syntactically valid expressions, yet semantically one works just fine while the other results in an error! The explanation as to why will be given inside the second code snippet itself in the form of a block comment.
- Code snippet A:
lumi
if (a + b) > c then {
/* this works and either gets executed or not */
}- Code snippet B:
lumi
if a + (b > c) then {
/* this fails, due to attempted arithmetic operation with incompatible types! */
/* assuming a := 5, b := 3 and c := 7, it is logically meaningless to write the */
/* expression 5 + (3 > 7) since (3 > 7) will evaluate to a TRUTH value, not a number */
}If you leave away the parentheses, the compiler will try its best to infer the actual meaning behind the expression, but it won't always be able to easily infer that, since evaluatable expressions, due to their nature in general purpose programming languages like Lumitare can get quite complex. Lumitare, as will be explored further in the subsequent sections, tries to guide and aid the developer to prevent producing overly complex source code with the powerful toolkit and general guidelines it provides, but realistically speaking it is not always possible to do so under all circumstances and if one really wants it is possible to write ugly code in any language!
Scope-binded aliasing with is
The next concept we want to explore here in order to produce source code that is easier to maintain is scope-binded aliasing with the is keyword.
The is keyword basically takes whatever it is handed on the right side of it and, within the scope it is used, assigns it an identifier that is handed to it on its left side.
It is important to note here that this only works in certain contexts, like inside a conditional or the definition head of specific constructs, with other usage possibilities being mentioned and explored later on together with the respective contexts they are available in!
How does this work? Well, let's take a look at an example and go back to our code from earlier!
lumi
entrypoint := () => {
define someNumber := 3;
define otherNumber := 5;
if x is someNumber < otherNumber then {
x .. " is less than " .. otherNumber .. "!" -> write(stdout);
};
};In this example we are aliasing the someNumber identifier, within the scope of the if-then constructs block, with the identifier name x and can thusly address someNumber as if we would've called it x to begin with!
And while it might not be obvious as of now, this will be an important feature later on for easily writing and visually parsing more complex use-cases for conditionals and the is keyword!
Also note that this is a good example of how, at least for people unfamiliar with this concept of scope-binded aliasing, it can be quite important to group terms together properly with parentheses if the term itself might appear ambiguous and there doesn't exist a distributive property for the respective operators used inside the term!
So the refined version and preferred version of aboves code would look like this:
lumi
entrypoint := () => {
define someNumber := 3;
define otherNumber := 5;
if (x is someNumber) < otherNumber then {
x .. " is less than " .. otherNumber .. "!" -> write(stdout);
};
};Here it is made explicit and thus clear to the reader that x is someNumber which is < otherNumber! Very easy to pass and readable, thus leading to good and easily maintainable code!
The wildcard identifier _
The following concept might appear a little bit difficult to understand at first, but in the following section we will explore, with examples, how it can be very power- and useful to easily write good and clean code that is easy to understand, yet very expressive at the same time!
A fact that you will have to accept for now is that, in certain scopes and contexts, the _ identifier can be used to serve as a wildcard! That means, it acts as a stand-in for basically saying any possible value at all or a lack thereof!
Since this theoretic explanation might be a bit too abstract to really grasp the concept or why it is so power- and useful as was claimed, let's get into the next section and see it in action!
Mutually exclusive if basics
While in the example of the simplest form of the if-then construct both conditional branches had their respective conditionals evaluated and the execution only executing one of them due to the logic inside the respective conditionals, which is called mutually exhaustive branching, often times we also require to have mutually exclusive branching like the else if and else keywords produce in other languages.
As was stated before though, these keywords are lacking in Lumitare! Instead, if the if keyword is followed by a code block and lacks the then keyword in its usual place, it will be treated as a mutually exclusive block, called an exclusive if! An exclusive if contains several possible branches to traverse, the conditions for which are given in front of the then keyword leading to the individual code blocks or expressions that are supposed to be evaluated exclusively.
To better visualize it, here is how it would look on a conceptual level:
lumi
if {
conditionA then {
/* execution if conditionA evaluates to true */
};
conditionB then {
/* execution if conditionB evaluates to true */
};
conditionC then {
/* execution if conditionC evaluates to true */
};
};As one can see, unlike with the simple if-then construct, this exclusive if construct gets defined by starting off with the if keyword and following that up by the code block containing the respective branches and their conditions and expressions, which is called an exclusive branch block. The then keyword hereby wanders into said exclusive branch block and is used to separate a given condition from the expression it is the trigger for if the given condition evaluates to true.
This exclusive if on a fundamental level basically functions like the switch-case known from other languages, but with some important differences, most notably that it works by break-by-default! This means that while evaluating the individual conditions from top to bottom, as soon as a condition is found that evaluates to true, that specific branch is chosen and the respective expression evaluated, ignoring all potential branches that follow beneat it!
Furthermore this exclusive if allows for more complex conditionals that guard the respective branches than just a mere comparison of a passed-in value with a given case value.
Now let's try it out with our example source code and modify it some more!
lumi
entrypoint := () => {
define someNumber := 3;
define otherNumber := 5;
if {
someNumber > otherNumber then {
someNumber .. " is greater than " .. otherNumber .. "!" -> write(stdout);
};
someNumber < otherNumber then {
someNumber .. " is less than " .. otherNumber .. "!" -> write(stdout);
};
someNumber = otherNumber then {
someNumber .. " is equal to " .. otherNumber .. "!" -> write(stdout);
};
};
};The expected output would still be the following:
console
3 is less than 5!However, now, with the three different conditions and the way the exclusive if evaluates its exclusive branch block, it will evaluate the first condition someNumber > otherNumber, which evaluates to false, then the second condition someNumber < otherNumber, which evaluates to true and thus letting the control flow enter this branch, evaluate it, and then breaking out of the entire construct, not even bothering to evaluate the third condition someNumber = otherNumber!
Extended exclusive if
But, as was claimed before by stating that keywords like else if and else aren't required in Lumitare, then where is the default case in the example above? Well, there are at least two obvious possibilities to implement a default case, equivalent to the else block of the standard if-elseif-else statements from other languages, in this exclusive branch block aside from constructing the logical conditions, like in the example, in a way that obviously only permits one condition to be true. One of these obvious ways, namely just having a conditional containing the true value literal, is NOT RECOMMENDED though and HEAVILY DISCOURAGED due to it being a bad coding practice!
So here is how one would implement a default case or else block in the exclusive branch block of the if-then construct:
lumi
entrypoint := () => {
define someNumber := 3;
define otherNumber := 5;
if {
someNumber > otherNumber then {
someNumber .. " is greater than " .. otherNumber .. "!" -> write(stdout);
};
someNumber < otherNumber then {
someNumber .. " is less than " .. otherNumber .. "!" -> write(stdout);
};
_ then {
someNumber .. " is equal to " .. otherNumber .. "!" -> write(stdout);
};
};
};As one can see, the recommended way to create an exclusive branch that gets executed if no other condition inside the exclusive branch block evaluates to true is utilizing the aforementioned wildcard operator _!
Aliasing and destructuring basics
The example in the previous section still looks a bit annoying to read though, so let's refine it a bit and showcase the power of another keyword introduced earlier, the is scope-binded aliasing!
lumi
entrypoint := () => {
define someNumber := 3;
define otherNumber := 5;
if [x, y] is [someNumber, otherNumber] {
x > y then {
x .. " is greater than " .. y .. "!" -> write(stdout);
};
x < y then {
x .. " is less than " .. y .. "!" -> write(stdout);
};
_ then {
x .. " is equal to " .. y .. "!" -> write(stdout);
};
};
};So what exactly have we done here? We have pulled both identifiers, someNumber and otherNumber into an ordered pair or 2-tuple, as it is known in mathematics, and then aliased the individual values via the is keyword and a principle called destructuring, which allows us to alias the individual values of some ordered data structure, more details on what that is later on, individually! Afterwards we were then able to, limited to the scope of the exclusive branch block use the aliased identifiers x and y as stand-ins for the original identifiers someNumber and otherNumber respectively!
If this would be it though, then this would still not merit having a specific is keyword reserved in the language, so there must be more to it, right?! Correct!
Let's explore what we can do with the concepts and capabilities of the if-then construct that we have learnt about so far already!
lumi
entrypoint := () => {
define someNumber := 3;
define otherNumber := 5;
if [x, y] is [someNumber, otherNumber] {
[x, y] = [y, x] then {
x .. " and " .. y .. " are exactly equal!" -> write(stdout);
};
x = 12 then {
x .. " exactly 12!" -> write(stdout);
};
y < 12 then {
y .. " is less than 12!" -> write(stdout);
};
_ then {
"The heck do I know what is going on at that point!" -> write(stdout);
};
};
};The expected output from this example would be:
console
5 is less than 12!This was already pretty nifty, huh?! And there is even more to come as to what is possible with the if-then construct!
Logical connectives
As is to expected from a general purpose programming language, there also are logical connectives.
Logical connectives are operators that can connect multiple logical statements with one another to form a more complex expression overall and are especially useful to prevent repeating the same executable logic over and over again, either with simple if-then statements or within the exclusive if block.
So, for instance, if we have some logic that is to be executed either way if someNumber is less than 4 or if someNumber is greater than 6, but not for any value inbetween, instead of building some annoying to build, hard to read and terrible to maintain contraption consisting of if-then, exclusive if or even a nested combination of those, we can just use a logical connective instead!
Lumitare, for the time being, supports the following logical connectives:
lumi
a AND b Only evaluates to true if both, a and b, evaluate to true as well!
a OR b Evaluates to true if either, a or b, evaluate to true.
a NAND b Only evaluates to false if both, a and b, evaluate to true, otherwise always true!
a NOR b Only evaluates to true if both, a and b, evaluate to false, otherwise always false!
a XOR b Evaluates to true if a and b have opposite values, otherwise false!
a XNOR b Evaluates to true if a and b have the same values, otherwise false!Furthermore the following special case of a unary logical connective, meaning a logical connective only accepting a single argument, exists:
lumi
!a Negates the value of a, thus evaluating to its opposite!So let's try to build an if-then statement which executes its respective code if someNumber is less than 4 or if someNumber is greater than 6, but not for anything inbetween utilizing these connectives!
lumi
entrypoint := () => {
define someNumber := 3;
if someNumber < 4 OR someNumber > 6 then {
someNumber .. " is either less than 4 or greater than 6!" -> write(stdout);
};
};Should produce the expected result:
console
3 is either less than 4 or greater than 6!There we go! In the next section we will get to know another cool feature of the if-then construct! Stay tuned!
if-then guard conditions
Aside from its usages in its simplest form or as a mutually exclusive branching mechanism, the if-then construct can furthermore be used as a guard condition! A guard condition is a condition that needs to evaluate to true for something else to happen.
At first that might seem like exactly what the average if statements in other languages do with their branching behaviour, right? WRONG! Well... At least somewhat. The distinction in formulation here is subtle! While the normal if statement in other languages or in its simplest form or as a mutually exclusive branching mechanism in Lumitare execute a given block of code if a condition evaluates to true, using the if-then construct as a guard means the following:
It can be written inline.
It can act as a guard for behaviour that would occur definitively without the
if-thenand make this behaviour only happen conditionally!
That sounds pretty abstract though, so how about an example? Consider the following expression:
lumi
define someNumber := 3;
define otherNumber := if someNumber < 5 then 42;This expression will assign the number 42 to the otherNumber constant if, and only if, someNumber is less than 3! Pretty cool, huh?!
You might wonder though, what happens if someNumber >= 5 were to be true instead? For this the usage of if-then as a guard introduces a certain safety mechanism called Optional wrapping.
Explaining all the intricacies of Optional wrapping would be beyond the scope of this explanation for the time being, but, on a very basic level, it means that whenever if-then is used as a guard, the value that might or might not be returned by the expression to the right-hand side of the then keyword will be wrapped into an Optional wrapper type, which provides a safe interface to check for the existence of a value and ensures that the Lumitare contract of a given datum identifier always containing at least some value, even if it only is an empty Optional wrapper is upheld!
While we could at this point again explore the possibilities of what this feature of the if-then construct acting as a guard condition implies, we will instead, for now, close this chapter. Congratulations! You made it through the first chapter of this language tutorial for Lumitare!
See you in the next chapter, where we will start exploring the difference between stateless and statefulness, the very basics of the type system Lumitare uses under the hood, what mutability of data means within this language, basic looping with the while keyword, and various other things!