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.
| operators | meaning |
|---|---|
f(x) buf[i] e.field | call, byte at buf + i, field of a layout |
-x !x ~x | negation, 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.
| builtin | what 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 inw | port 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.
| builtin | what it does |
|---|---|
win_info() win_max() | the window's size right now, then the largest it can ever be |
fill blit present | draw a rectangle, copy pixels, show a frame |
key_poll() mouse_poll() | the next key, the mouse's state |
readfile writefile | load 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.