Serenum Release 3 Programming Basics

This tutorial assumes you are running Serenum release 3. In order to install release 3 on your Serenum 1 device, first unzip the release 3 download. Then use rufus on windows or dd on linux to write the serena.bin file to the sdcard. Once that is complete you should be able to boot release 3. You can confirm success by running effunde sd/version.txt -u after booting.

With release 3 setup, to begin navigate to the root/sd/ directory and run mkfile and scr as indicated below.

scrīniō: root/sd/
)> mkfile my_program.lbc
scrīniō: root/sd/
)> scr my_program.lbc

Now use the editor to write these contents into the file. For the characters with lines over them, these are called macrons, use the tilde key below escape on your US keyboard. It functions as a dead-macron key. If you press it then press the ‘a’ key you will type an ‘ā’.

#particulam inveni "common/basis_serēna.lbc"

peregrīna procedūra noob_main()
{
    printf("Hello World.\n",);
}

Now save and exit using Ctrl-S and Ctrl-Q. You will now compile, assemble and run this program using the commands below.

clb . "my_program.lbc" output.asm
cra "-u" start.asm output.asm runme.pb
runme.pb

Congratulations you should now have seen the program output ‘Hello World.’. If this is not the case or you got stuck somewhere, send me an email at sam@samhsmith.com. The way this works is as follows. We give clb access to the current directory. We give it a string indicating which file to start compiling from. Finally we tell it to put the output in output.asm. The program is assembled into an executable by cra. The “-u” tells cra that start.asm is encoded with UTF-8 instead of the default encoding cleartext. Finally we run the runme.pb executable. You can read more about the executable format here.

Moving on, I will now go through the basics of the brevis language using example programs.

Declaring of variables works like it does in C. In this example we declare to variables, add them, then print the result to the console using printf. This printf works similar to C’s execept we do not need to specify the type. Notice that a trailing comma is required after every procedure parameter. This is enforced to improve grepability.

#particulam inveni "common/basis_serēna.lbc"

peregrīna procedūra noob_main()
{
    n64 a = 5;
    n64 b = 6;
    printf("a + b = {}\n", a + b,);
}

There are 3 basic datatypes in Serenum release 3, n64, s64, and n8. But crucially there are also the composite types, slices pointers and arrays. Slice is a bad name. In Brevis it is therefore called a spatium instead. One spatium several spatia. Strings are encoded as a spatium of bytes, []n8. In this example we print a signed number and use a format string from a variable.

#particulam inveni "common/basis_serēna.lbc"

peregrīna procedūra noob_main()
{
    s64 a = -16;
    []n8 format_string = "a = {}\n";
    printf(format_string, a,);
}

Loops are constructed in Brevis using the keyword dum. This is latin for while. In this example we construct an array and then it out in reverse order. array.m gives us the size of the array, m is short for magnitūdō or it’s english descendent magnitude. Notice here that after the final iteration of the loop, i underflows causing the loop condition to fail.

#particulam inveni "common/basis_serēna.lbc"

peregrīna procedūra noob_main()
{
    [3]n64 array;
    array[0] = 6;
    array[1] = 8;
    array[2] = 20;
    
    n64 i = array.m - 1;
    dum i < array.m
    {
        printf("{}\n", array[i],);
        i -= 1;
    }
}

Now let’s go through some if and else statements. In Brevis they are sī, sīve and sī nōn. These mean ‘if’, ‘or if’, and ‘if not’ in latin. Boolean operators are && and || like in C.

#particulam inveni "common/basis_serēna.lbc"

peregrīna procedūra noob_main()
{
    s64 num_a = -5;
    s64 num_b = 7;
    
    sī num_a > 0
    {
        printf("Case A.\n",);
    }
    sīve num_b > 0
    {
        printf("Case B.\n",);
    }
    sīve num_b == -10 && num_a != -2
    {
        printf("Case C.\n",);
    }
    sī nōn
    {
        printf("Case D.\n",);
    }
}

There are arena allocators in Brevis similar to what Ryan Fleury describes in his article on the subject. We even have scratch arenas built in. This example gets a scratch arena and uses it to allocate a spatium of signed integers.

#particulam inveni "common/basis_serēna.lbc"

peregrīna procedūra noob_main()
{
    @SitulaMemoriae scratch = get_scratch_situla();
    
    []s64 integers = situlā_adlocā<s64,>(scratch, 5,);
}

If you only want to allocate only one thing you can allocate a spatium of size 1 and then get it’s index. Index is the latin word for pointer. Spatium actually just means a space, in this case a space in memory. So spatium.i will give us the index, aka pointer, to the beginning of the allocated space. In this example we define a structure called Node, allocate one and then assign to it’s members.

#particulam inveni "common/basis_serēna.lbc"

structūra Node
{
    @Node next_node;
    n64 name_len;
    [112]n8 name_buf;
}

peregrīna procedūra noob_main()
{
    @SitulaMemoriae scratch = get_scratch_situla();
    
    @Node my_node = situlā_adlocā<Node,>(scratch, 1,).i;
    
    my_node.next_node% = 0;
    my_node.name_len% = 2;
    my_node.name_buf[0]% = 'H';
    my_node.name_buf[1]% = 'i';
}

There is some complicated and novel stuff going on here. Two different types of pointer offset operations and a dereference postfix. In this expression, my_node.next_node%, we start with the identifier invokation of my_node. This produces a value of type @Node, a pointer, or index in latin, to a node. We then perform a member access on the pointer which acts as an offset operation. my_node.next_node has the type @@Node because it’s pointing to the location in memory where the structure’s member is stored. The percent sign then dereferences this value and allows us to assign to that slot in memory.

The expression my_node.name_buf[0]% is different though. We start the same way, my_node.name_buf has the type @[112]n8. Just as we could access the members of a struct through a pointer, we can do the same but for the elements in the array. Therefore, my_node.name_buf[0] has the type @n8. Notice though that these offset operations only do maths on the pointer value, they do not perform any read or writes to memory. They are only convenient ways to operate on pointers. The reason this is done instead of automatically dereferencing like other modern languages do, is because there is a one to one mapping between percent signs and memory operations.

Next we expand the previous example by making a linked list of nodes and then traversing them. I also define a procedure that does the traversing and returns how many nodes there are in the list.

#particulam inveni "common/basis_serēna.lbc"

structūra Node
{
    @Node next_node;
    n64 name_len;
    [112]n8 name_buf;
}

peregrīna procedūra noob_main()
{
    @SitulaMemoriae scratch = get_scratch_situla();
    
    @Node my_node1 = situlā_adlocā<Node,>(scratch, 1,).i;
    @Node my_node2 = situlā_adlocā<Node,>(scratch, 1,).i;
    @Node my_node3 = situlā_adlocā<Node,>(scratch, 1,).i;
    @Node my_node4 = situlā_adlocā<Node,>(scratch, 1,).i;
    my_node1.next_node% = my_node2;
    my_node2.next_node% = my_node3;
    my_node3.next_node% = my_node4;
    my_node4.next_node% = 0;
    
    printf("The linked list length is {}\n", how_long(my_node1,),);
}

procedūra how_long(@Node first_node,) -> n64
{
    n64 counter = 0;
    dum @Node current_node = first_node; current_node != 0
    {
        #prōlāta { current_node = current_node.next_node%; }
        
        counter += 1;
    }
    #refer counter;
}

This should not need much explaination other than the special dum and #prōlāta statement. All the semicolon terminated statements after the dum keyword and before the condition expression are not part of the loop but are scoped such that the can only be accessed from what is inside the loop. You can think of the loop as actually having two scopes, one outer scope that contains iterator values and that which is persistent across iterations but should not be leaked to the environment. And the inner scope which is what runs every loop iteration. The #prōlāta statement is exactly like Odin’s defer except that what is being deferred must always be contained by curly braces.

The final example is a program that takes a single file as command line parameter and prints it. The file is assumed to be unicode and that is not the native text encoding of Brevis, hence the need for the {u} to print unicode. Once you’ve built the program, print the source file by passing it as the first argument when you run runme.pb.

#particulam inveni "common/basis_serēna.lbc"

variābile [10]KArgument arg_buf;
peregrīna procedūra noob_main()
{
    []KArgument args = imp_get_args(arg_buf[0..],);
    adfirma(args.m == 1,);
    adfirma(args[0].arg_type% == 3,);
    
    []n8 file_contents = alloc_load_entire_file(args[0].int_value%,);
    
    printf("{u}", file_contents,);
}

Future blog posts will contain deeper knowledge about the programing language and the system. The contents will be determined by what the Serenum users ask me first.