coff / language

the c0 language

1one type

c0 has a single value type, int, a 64-bit signed integer. numbers, characters, truth values, addresses, strings and functions are all an int. a string literal is the address of its bytes, a function name used as a value is the address of the function and memory is reached by passing an address to a builtin. there is no unsigned type and no array type.division is signed and rounds toward zero, so code that packs bits into a value keeps them where the sign bit cannot get in the way, or uses the shifts and masks instead of division.

2a program

a program is global variables, functions and declarations, in any order. it starts at main, which takes nothing and returns an int that becomes the exit code of the process. there is no runtime underneath it. the code coff emits starts with a few instructions that call main and exit with what it returned.

int total = 0;

int add(int a, int b) {
    return a + b;
}

int main() {
    for (int i = 0; i < 5; i += 1) {
        total = add(total, i);
    }
    return total;
}

function

int name(int a, int b) { ... }
void name() { ... }
(int, int) name(int a) { ... }

a function takes up to sixteen parameters. the first six arrive in registers and the rest on the stack. functions can recurse. a void function returns with a bare return;. a function declared to return (int, int) hands back two values, see below.

variable

int x;
int y = x + 1;
Point p = alloc(sizeof(Point));

a variable outside a function is a global and lives for the whole run. one inside a block lives until the end of that block and may shadow a name from outside it. a global's initializer can be any constant expression, which the compiler works out. instead of int a variable can be given a layout's name as its type, explained under layouts.

3statements

if

if (condition) statement
if (condition) statement else statement

a condition is true when it is not zero.

while

while (condition) statement

for

for (init; condition; step) statement

init runs once and may declare a variable, which then belongs to the loop alone. the statement runs while the condition holds. the step runs after every pass. continue in a for goes on to the step, the same as in C, so it cannot skip the increment by accident.

break and continue

break;
continue;

leave the innermost loop, or go straight to its next pass.

assignment

x = expression;
buf[i] = expression;
e.field = expression;
x += 1;   x -= n;   x *= 2;   x /= 2;   x %= 10;

assignment is a statement and not an expression, so it cannot hide inside a condition. x += n means exactly x = x + (n). the rest of the family works the same way.

two return values

(int, int) divide(int a, int b) {
    return a / b, a % b;
}

q, r = divide(17, 5);

a function can return two values, which travel in two registers. such a function has to return two values on every path. its result can only be taken apart into two variables, never used as a plain value. this replaced a habit of passing results out through globals.

defer

int load(int name) {
    int buf = 0;
    defer dealloc(buf);
    buf = alloc(4096);
    if (readfile(name, buf, 4096) < 0) { return 0 - 1; }
    return parse(buf);
}

defer statement; runs the statement every time the function returns, deferred statements last-written first. it is decided when the program is compiled, not as it runs. a defer runs even if it sat under an if that was false, even on a return that comes before it in the text. its expressions are worked out at the moment it runs, so it sees variables as they are then. that is why the example sets buf to 0 first, since giving back 0 does nothing. a deferred statement may not itself return, break, continue or defer.

return

return;
return expression;
return a, b;

a call on its own, f(1);, is a statement too. a block in braces groups statements wherever one is expected.

4operators

the operators are C's and they bind the way C binds them, the odd parts included. tightest first.

operatorsmeaning
f(x) buf[i] e.fieldcall, byte at buf + i, field of a layout
-x !x ~xnegation, 1 when x is 0 and 0 otherwise, bitwise not
* / %multiply, divide rounding toward zero, remainder with the sign of the left side
+ -add, subtract
<< >>shifts, >> keeps the sign of a negative number
< <= > >=comparisons, 1 or 0
== !=equality, 1 or 0
&bitwise and, looser than == just like in C
^bitwise exclusive or
|bitwise or
&&and, the right side only evaluated when it decides the answer
||or, the same way

-7 % 2 is -1, as in C. there is no ++, no ?: and no comma operator.

5literals and comments

integers in decimal or hex (255, 0xFF), characters ('a', '\n') and strings ("hello\n"), with the escapes \n, \t, \\ and \". a string is stored with a zero byte after it and its value is its address. // starts a comment that runs to the end of the line.

6layouts

c0 has no structs. it has layout, which gives names to a run of 8-byte offsets, so store64(e + 16, v) can be written e.vx = v. a layout makes no new kind of value. everything is still an int. a field is a load or a store at an offset.

layout Entity {
    int x;
    int y;
    int vx;
    int vy;
}

int main() {
    Entity e = alloc(sizeof(Entity));
    e.x = 10;
    e.vx = 1;
    return e.x + e.vx;
}

sizeof(Entity) is the field count times 8, known when the program is compiled. a variable declared with a layout's name, like Entity e above, has its fields looked up in that layout. a field the layout does not have is a compile error. a variable declared plain int can still use fields, through one table of field names shared by every layout in the program. that only works for a name every layout puts at the same offset. the compiler refuses one they disagree on.

7splitting a program up

include

include "jenna.c0";

pastes another file in as text, before the compiler reads a single token. the file is looked for next to the file including it and then in each -I directory. each file is pasted at most once, so two files can both include a third. a file that ends up including itself is an error.

embed

embed logo "logo.spr";

puts a file's bytes in the program as data. logo is their address and logo_size their length. the compiler never opens the file, the assembler does, so the compiler's output still depends only on the text it was given.

extern

extern int kernel_end;

names a symbol defined somewhere else, by the linker or by hand-written assembly. used as a value it is the symbol's address.

8functions as values

a function's name without a call after it is its address. calling through a variable that holds one calls whatever it points at. there is no check that the number of arguments fits, since a value carries no description of what it points at. this is how a program keeps a table of handlers or hands a callback to a library.

int twice(int x) { return x * 2; }

int apply(int f, int v) { return f(v); }

int main() { return apply(twice, 21); }

9builtins

there is no standard library. a small set of builtins covers what the language cannot say by itself, each one compiled straight to a few instructions or a system call.

builtinwhat it does
alloc(n)n bytes of zeroed memory, as an address
load8(a) store8(a, v)read or write one byte
load32(a) store32(a, v)read or write four bytes, read zero-extended
load64(a) store64(a, v)read or write eight bytes
print(s)write a zero-terminated string to standard output
write(fd, p, n) read(fd, p, n)the system calls of the same names
open_read(path) open_write(path) close(fd)open a file for reading, create or truncate one for writing, close it
exit(code)end the process at once
argc() argv(i)the argument count and the address of argument i
outb inb outw inwport input and output, for code that runs in a kernel

with --elf a program is built for moonshot's ring 3 instead, and the file and port builtins are refused there. it gets these instead, each a moonshot system call.

builtinwhat it does
win_info() win_max()the window's size right now, then the largest it can ever be
fill blit presentdraw a rectangle, copy pixels, show a frame
key_poll() mouse_poll()the next key, the mouse's state
readfile writefileload and save a file
dealloc(p)give back what alloc handed out
sound glyph ticks()queue audio, read the kernel's font, read the clock
syscall(n, a, b, c, d, e)any system call by number, so a new one needs no compiler change

win_info and win_max look like a pair and are not. a program sizes its frame buffer once from win_max, which never changes. it draws only inside win_info, which changes whenever the window manager rearranges the screen.