MESYETI's Website

<- Home
<- Log

Day 15 - Callisto on Fox32

I decided that now that Callisto is a presentable language, it's finally the time to return to making a Fox32 backend for Callisto. If you don't know, Fox32 is a fantasy computer. This means it has a CPU architecture (among other hardware) designed by its creator, ryfox. A fox designing a computer is very cool.

This means I can take you, the reader, on a journey through the journey of writing a Callisto backend. First, I create a backend class. There are some types needed for each backend, so I define a Word type. This type contains data for defined functions. It contains the function type, contents (if it's an inline function, this means that when its called, its contents are simply inserted at where the function is called), whether the function throws an exception, and the parameters of the function.

Next, there are some functions from the base backend class that I have to implement. I will leave most of them blank for now, so I can implement them later. First of all, the GetVersions function. If you don't know about Callisto versions, then read this. This will return the versions on for this platform. Here they are:

override string[] GetVersions() => [
	// CPU features
	"Fox32", "LittleEndian", "16Bit", "32Bit",

	// OS features
	"Fox32OS", "IO", "Exit"
];

Then, there's the FinalCommands function. This defines what commands need to be used to assemble the compiled code into an executable. In the Fox32 backend, it calls the assembler to create the executable, then a ryfs script to create a disk image. (If the make-img backend option is passed)

Then, there's the MaxInt function. This just returns the biggest integer this platform can store. As Fox32 is 32-bit, this is the unsigned 32-bit integer limit.

Then, there's the Init function. This defines the assembly code that goes at the top of the program. First, it calls the standard library's init function, which I will go into later. It allocates 512 cells for the data stack, which on a 32-bit platform is 2048 bytes. After that, there is a similar function called End. This calls the standard library's exit function, and then defines the init function. The init function is defined at the end because at the start, the compiler isn't certain that the standard library's init function exists, as the compiler hasn't seen its definition yet. Then, it allocates space for global variables, and then includes the .def files for the fox32 rom and OS functions.

Now to compile AST nodes. The first is CompileWord. This just calls a defined function, so it just compiles to a call instruction. Next, is the CompileInteger function. This is also very simple. First, it writes the integer into memory at the data stack pointer's address. The data stack pointer is in register r31. Then, there's the CompileFuncDef function. This adds a label with the function's name, and then calls CompileNode on each node in the function's contents. After that, there is the CompileLet function. This takes the info from the let node and defines a global, and appends that to the globals array. It's just globals for now because I need them for printing, you'll see why later.

Printing A

Printing 'A' is the first thing I do when writing a backend. Since there is currently no core library implementation for Fox32 (because the backend is very incomplete), I will have to define some functions myself in the test file. First, I need a place to store the terminal stream.

let u32 __fox32_stream

Now to make the init function, this is called at the start of the program.

inline unsafe __fox32_program_init begin asm
	"mov r1, [rsp + 4]"
	"mov [__global___us____us__fox32__us__stream], r1"
end end

The OS puts the terminal's stream at the top of the stack. I have to add 4 because the return address for the caller of __init is at the top of the stack. I take that value and store it in the stream variable. Now for the exit function

inline unsafe __fox32_program_exit begin asm
	"call end_current_task"
end end

Now, to make the print function. This sets the buffer address to the top of the data stack, the size as 1, and the stream as what was stored in the variable before:

func unsafe man print_ch cell ch begin asm
	"sub r31, 4"
	"mov r0, 1"
	"mov.32 r1, [__global___us____us__fox32__us__stream]"
	"mov r2, r31"
	"call write"
end end

Now all the functions that have to be defined are done. I can now just call the function with 'A', then again with a new line:

'A' print_ch
10 print_ch

Now to try running it

Next, I will work on if & while loops, and then local variables