###**======------==---=#%%@@%#*+--------==
        ###**=======---===---=%%@%###%%%#=------==
        ###*+=======-----=---%@@%#***#%@%%+------=
        ###*+========---==--+@@@#*****%@@@%=-----+
        ###*+=======----==--#@@%##*****#%@@#-:---+
        ###*+=======----==--#@@#*******+#@@%=----=
        ###*+=========--==--#@@***##**=+#%@%+---=+
        ###*+====-==-----=--+@@%***###**%@@%+---=+
        ###**====-==-----=---+@@@#******%@@@#*====
        #**#*=========--===--+#@@@%#***#%%%@##*===
        #*##*+=========-==--*@@@%%##****##%%%+::::
        #*#**+=========-:----=*==+++**##=+==-::-::
        

Jiayi (they/them)
carabinerr@proton.me
to neocities

<-- fasto

--> interpreter

The compiler is built on RISCV-32 assembly and we already have a system set in place for running one main thread. The code is quite long, but the main parts to understand is how we assemble the instructions to make them compile at the end.

Starting point: How we generate instructions

To compile a given typed program, we pass the TypedProg to the compile function, which outputs a list of instructions that the RARS simulator will execute. compileFun then compiles all our functions in the TypedProg, and for that to happen we need to compile each expression in a function.

The function compileExp is responsible for generating instructions for each expression. It takes an expression, which at this point has already been typed by the typechecker, a variable table, which we use to look up where each local variable (mapping variable names to registers), and a register to put the final result in. When compiling bigger expressions, we combine smaller ones. Each sub-expression needs somewhere to store its result so the next instruction can use it.

compileFun is the core of the code generation part of compilation.It compiles the full function declaration into a list of instructions.

The function arguments are already residing in hardware registers, and we use getArgs to move them into local variables, we get the resulting instructions and variable table.

We then create a temporary register for the function result, which we want to be in register Rrv. We then compile the function body expression with compileExp, which results correspondingly is saved in rtmp.

We then allocate registers for the variables in our expression, reusing registers when values are no longer needed. We preserve the return register Rrv and spill any variables to the stack if there are more live variables available than there are registers available. Thus we will get:

The final instructions of the function body using real registers, with LW/SW inserted for spilled variables. Highest register used (maxr) and the count of how many values got spilled.

We then call stackSave to generate instructions to save registers. In particular we loop for all the callee-saved registers used and add a SW instruction to save each of them on the stack and then a LW to restore then later.

Finally we combine the instruction list for our function declaration.

First we push our return address to the stack, so we can return safely, then we store callee-saved registers and move the stackpointer (Rsp) accordingly to officially 'push' the values onto the stack.

Then we add the function body instructions and after we have run them, we have to restore our callee-saved registers, so we adjust our Rsp so it pops the values and then restore them to the registers.

Finally we can restore our return address, and jump back to caller through the address.

Circling back, we can now compile a typed program using compileFun.

Overall idea for adding concurrency

Each thread has its own stack but all threads share the heap, allowing them to access and update common data. Each thread is represented by a thread management block that stores its state, such as the stack pointer and links to other threads. Threads can communicate and synchronize using channels, which act as shared queues for sending and receiving values. When a thread blocks or yields, a context switch occurs: the current thread’s state is saved, and the next runnable thread is restored and executed.

To realise this, we need the following considerations and implementations.

Heap and stackoverflow

Spawning a thread

Lambda functions

Context-Switching

Implementation of Channels (and in extension send, receive)

Await and exiting a thread