lowercase

About this project


lowercase is a lightweight C-like programming language that I am currently working on. I got the idea for my own language while learning about compilers in university, where we had to write a simple C compiler.

I always believed that compilers were impossibly difficult for one single person to ever write on their own, but once you learn the basic principles, it actually starts to seem surprisingly doable.

The language is not meant for any large scale projects or as a replacement or enhancement that already exists, it's really just a fun project for myself. The use cases would mostly be small, single file algorithms and programs which benefit from the simplicity of the type creation, as well as the syntactic sugar that is built in.

I wont go into to much detail here as the technical specification of the language is down below in PDF form, but in short, the syntax is primarily C-like, while also borrowing quick type creation similar to OCaml, and a couple modern day features like 'auto' style loops and dynamic lists.

There are also some rather unique types, like super, hyper, and ultra, which are 128, 256, and 512 bit integers respectively. I thought it would be interesting to have native support for these large numbers which are often used in science and cryptography.

Below you'll find some code examples that show off what I hope the language will be capable of, further down are some examples of compiled programs.

lowercase
// Squaring a number. The squared brackets after the parameter x indicate that the argument must be larger than 0. The return type is an unsigned 512 bit integer.
// We can set the name of the return variable beforehand, saving us a return statement and making it clear which variable is supposed to be the result.
function square(i64 x[0, inf]) => (u512 n) {
     n = x * x;
}

Global variables must be readonly and capslock to insure that the programmer is aware of their trickery. Yes, it won't compile if the name isn't all caps :)

lowercase
readonly int HI_IM_GLOBAL = 42;

Fast type creation is a large part of the language. To create a new type, just use the type keyword followed by angle brackets containing the fields with their types. This is the 'simple' notation:

lowercase
type KeyCode = <u8 key, bool isValid>;
type Tree = <Tree left, Tree right, KeyCode value>;
// Usage of a user type
Tree tree = (null, null, (8, true));

We can also use the 'complex' notation to create a type. This allows for functions and operator overloads to be defined

lowercase
type Complex = {
     this = <f64 real, f64 imag>;
     operator *(Complex a, Complex b) => Complex result {
          result = (a.real * b.real) - (a.imag * b.imag);
     }
}

Subroutines are functions that don't take any arguments or return anything. The infinitely keyword is basically a while(true) loop, so it has to be broken out of at some point. iterate is my version of a foreach loop.

lowercase
subroutine awaitInput {
    infinitely {
        if (getKey() != null) {
            break;
        }
    }
    iterate (KeyCode code : getKeycodes()) {
        if (code.isValid) {
             print("Keycode: %d", code.key);
        }
    }
}

Compiled Programs


These are a few examples of programs that the compiler is capable of at the moment. lowercase compiles to x86-64 assembly, which then needs to be linked and compiled again by an assembly compiler such as NASM. I would have liked to create the .exe myself, but that's just one step above what I can do at the moment.

Program 1: Simple print

lowercase
subroutine main {
     print("Hello, World!");
}

This compiles to:

Assembly
section .bss
    __lc__heap_ptr resq 1
section .data
    nl db 0x0a, 0x00
    __string_0 db 'Hello, World!', 0x00
section .text
    global _start
_start:
    call main
    jmp _exit
__lc__strlen:
    mov rax, 0
__lc__strlen_loop:
    cmp byte [rsi + rax], 0
    je __lc__strlen_done
    inc rax
    jmp __lc__strlen_loop
__lc__strlen_done:
    ret
__lc__print:
    push rdi
    push rdx
    call __lc__strlen
    mov rdx, rax
    mov rax, 1
    mov rdi, 1
    syscall
    pop rdx
    pop rdi
    ret
__lc__println:
    call __lc__print
    lea rsi, nl
    call __lc__print
    ret
main:
    push rbp
    mov rbp, rsp
    lea rbx, __string_0
    mov rsi, rbx
    call __lc__println
main_end:
    mov rsp, rbp
    pop rbp
    ret
_exit:
    mov rax, 60
    xor rdi, rdi
    syscall

So, let's try to run it. NASM doesn't really work super well on Windows, so I use WSL2 with Arch to compile it.

compiled

You'll notice a bunch of functions with the prefix __lc__. These are actually hand-written assembly functions that I use to perform certain low-level tasks such as printing to the console and converting numbers to strings. How these are implemented is explained below in the technical details section.

Program 2: Factorial

lowercase
function factorial (int n) => int {
    if (n <= 1) {
        return 1;
    }
    return n * factorial(n - 1);
}

subroutine main {
     factorial(5);
}

This compiles to:

Assembly
section .bss
    __lc__heap_ptr resq 1
section .data
    nl db 0x0a, 0x00
section .text
    global _start
_start:
    call main
    jmp _exit
factorial:
    push rbp
    mov rbp, rsp
    sub rsp, 4
    mov dword [rbp - 4], edi
    mov ebx, dword [rbp - 4]
    mov rcx, 0
    cmp rbx, rcx
    sete dl
    cmp dl, 0
    je L0_if_else
    mov rax, 1
    jmp factorial_end
    jmp L1_if_end
L0_if_else:
    mov ebx, dword [rbp - 4]
    mov ecx, dword [rbp - 4]
    mov rdx, 1
    sub rcx, rdx
    mov rdi, rcx
    push rbx
    call factorial
    pop rbx
    mov rcx, rax
    imul rbx, rcx
    mov rax, rbx
    jmp factorial_end
L1_if_end:
factorial_end:
    mov rsp, rbp
    pop rbp
    ret
main:
    push rbp
    mov rbp, rsp
    mov rdi, 5
    call factorial
    mov rbx, rax
main_end:
    mov rsp, rbp
    pop rbp
    ret
_exit:
    mov rax, 60
    xor rdi, rdi
    syscall

Now, unfortunately lowercase does not yet have the modern day technology of printing integers. Therefore, we're gonna have to check that this program works in a more archaic way. We'll debug the program in gdb and check the value of the register rax at the end of the main function.

First, we compile the program:

Bash
nasm -f elf64 -o factorial.o factorial.asm
ld -o factorial factorial.o

Then we run it in gdb with a breakpoint at the main_end:

Bash
gdb ./factorial
(gdb) break main_end
(gdb) run
(gdb) print $rax

And here's the result!

compiled

Technical details


Now, I could go into detail about how to write a compiler from scratch here, but there are already many resources that explain it way better, and this page is already pretty long. But in short, a compiler has a lexer, a parser and an emitter.

The lexer is responsible for turning the source code into tokens. So for example, in lowercase, the expression '1+1' would be turned into the tokens D_NUMBER[1], PLUS[ + ], D_NUMBER[1].

The parser then takes this list of tokens and attempts to put them into a structure known as an Abstract Syntax Tree (AST). This is a data structure that represents the source code in a recursive way, so that we can traverse it to generate our code.

Finally, the emitter steps through the AST to generate the code for each of the nodes. Below is an example of a full compilation process for a simple program:

compiled

Lastly, I'd like to talk about the assembly functions that I mentioned earlier. To be honest, I'm not sure how most languages today solve this kind of problem, but for me it just made sense to have a library of hand-written assembly functions that the compiler accesses when it needs to perform some stereotypical low-level task, such as printing.

I created a class called LibraryFunction that stores the properties of the function:

C#
[AttributeUsage(AttributeTargets.Method])
public class LibraryFunction : Attribute
{
     public string LcName { get; } // The assembly name, such as '__lc__strlen'
     public string[]? RequiredFunctions { get; } // The names of other functions that this function requires
     public string[]? ArgumentRegisters { get; } // The names of the registers that this function expects the arguments to be in
     public string[]? Parameters { get; } // What type of parameters this function expects
     public string? ReturnType { get; } // The return type
     public bool IsReference { get; } // If the returned value is an address
     public bool IsInternalOnly { get; } // If this function is only available to the compiler (such as error & abort functions)


     public LibraryFunction (string LcName, ...

We can then apply this attribute to a method in the compiler:

C#
[LibraryFunction(__lc__print, new string[] { "__lc__strlen" }, new string[] { "rsi" }, new string[] { "text:string" }, "void", false)]
public static void print(CodeEmiiter emitter) {
     emitter.EmitLabel(new Label("__lc__print"
     emitter.PushRegisters(Register.rdi, Register.rdx);
     emitter.EmitInstruction("call", "__lc__strlen");
     emitter.EmitInstruction("mov", "rdx", "rdi");
     emitter.EmitInstruction("mov", "rax", "1");
     emitter.EmitInstruction("mov", "rdi", "1");
     emitter.EmitInstruction("syscall");
     emitter.PopRegisters(Register.rdx, Register.rdi);
     emitter.EmitInstruction("ret");
}

Technical Specification


Below you'll find the PDF of the technical specification of the language, which goes into even further detail of the language.