Skip to content

Latest commit

 

History

History
234 lines (198 loc) · 9.54 KB

C0.md

File metadata and controls

234 lines (198 loc) · 9.54 KB

C0

The codin compiler translates Odin to a subset of C called C0. C0 can be compiled by any regular C compiler but is a much smaller and better defined language that allows Odin to be translated to C "safely" without invoking undefined behavior. C0 is treated more like an intermediate representation (IR). C0 is not a single-static-assignment (SSA) IR.

Instructions

C0 defines a multitude of instructions which everything is translated to. The codin compiler emits a special "prelude" which implements these instructions as functions which are forced-inlined allowing it to be compiled by a regular C compiler without any of the overhead of the function call under minimal optimization.

As an example, consider the following expression in Odin where every variable here is a i32.

a := b + c * d;

This would be translated to the following C0

wri32(&a, addi32(b, muli32(c, d)));

You can see that every instruction is typed.

The special prelude which codin would emit for the generated C0 so that it can be compiled by a regular C compiler would look something like the following.

#if defined(_MSC_VER)
#define FORCE_INLINE __forceinline
#else
#define FORCE_INLINE __attribute__((always_inline)) inline
#endif

FORCE_INLINE void wri32(i32 *dst, i32 src) {
	*dst = src;
}

FORCE_INLINE i32 addi32(i32 lhs, i32 rhs) {
	const i64 a = lhs;
	const i64 b = rhs;
	const i64 s = a + b;
	return (i32)(sum & INT64_C(0xffffffff));
}

FORCE_INLINE i32 muli32(i32 lhs, i32 rhs) {
	const i64 a = lhs;
	const i64 b = rhs;
	const i64 s = a * b;
	return (i32)(sum & INT64_C(0xffffffff));
}

You can see the addi32 and muli32 functions emitted are slightly different to how one would expect to write them, this is because Odin requires signed integer underflow to wrap. These functions implement that behavior without any actual cost. All the tested C compilers are capable of optimizing these even at the lowest optimization setting, e.g:

i32 foo(i32 x) {
	return addi32(x, 4096);
}

Becomes the following at the lowest optimization level.

foo(int):
        lea     eax, [rdi+4096]
        ret

The main design goal of C0 is to map Odin's semantics better for C and to enable future translation to other optimizing compiler frameworks or directly to machine code. The C prelude is strictly only necessary to make the IR compile with a regular C compiler and can be omitted in future cases of more direct translation. C0 is literally just C but simple enough to also be an IR in the same language.

Reference

The following is the whole reference of all instructions in C0.

Instruction(s) Operation
rd{i,u}{8,16,32,64,128}(&src) Read
wr{i,u}{8,16,32,64,128}(&dst, src) Write
add{i,u}{8,16,32,64,128}(lhs, rhs) Addition
sub{i,u}{8,16,32,64,128}(lhs, rhs) Subtraction
mul{i,u}{8,16,32,64,128}(lhs, rhs) Multiplication
quo{i,u}{8,16,32,64,128}(lhs, rhs) Quotient
rem{i,u}{8,16,32,64,128}(lhs, rhs) Remainder
shl{i,u}{8,16,32,64,128}(lhs, rhs) Left shift
shr{i,u}{8,16,32,64,128}(lhs, rhs) Right shift
and{i,u}{8,16,32,64,128}(lhs, rhs) Bitwise and
or{i,u}{8,16,32,64,128}(lhs, rhs) Bitwise or
xor{i,u}{8,16,32,64,128}(lhs, rhs) Bitwise xor
eq{i,u}{8,16,32,64,128}(lhs, rhs) Equal
neq{i,u}{8,16,32,64,128}(lhs, rhs) Not-equal
lt{i,u}{8,16,32,64,128}(lhs, rhs) Less-than
gt{i,u}{8,16,32,64,128}(lhs, rhs) Greater-than
lteq{i,u}{8,16,32,64,128}(lhs, rhs) Less-than-equal
gteq{i,u}{8,16,32,64,128}(lhs, rhs) Greater-than-equal
addf{16,32,64}(lhs, rhs) Addition
subf{16,32,64}(lhs, rhs) Subtraction
mulf{16,32,64}(lhs, rhs) Multiplication
divf{16,32,64}(lhs, rhs) Division
eqf{16,32,64}(lhs, rhs) Equal
neqf{16,32,64}(lhs, rhs) Not-equal
ltf{16,32,64}(lhs, rhs) Less-than
gtf{16,32,64}(lhs, rhs) Greater-than
lteqf{16,32,64}(lhs, rhs) Less-than-equal
gteqf{16,32,64}(lhs, rhs) Greater-than-equal
atom_thread_fence(order) Atomic thread fence
atom_signal_fence(order) Atomic signal fence
atom_wr{i,u}{8,16,32,64}(&dst, src, order) Atomic write
atom_rd{i,u}{8,16,32,64}(&src, order) Atomic read
atom_xchg{i,u}{8,16,32,64}(&dst, v, order) Atomic exchange
atom_cas{i,u}{8,16,32,64}(&dst, e, v, s, f) Atomic compare exchange
atom_add{i,u}{8,16,32,64}(&dst, delta, order) Atomic fetch-add
atom_sub{i,u}{8,16,32,64}(&dst, delta, order) Atomic fetch-sub
atom_and{i,u}{8,16,32,64}(&dst, delta, order) Atomic fetch-and
atom_or{i,u}{8,16,32,64}(&dst, delta, order) Atomic fetch-or
atom_xor{i,u}{8,16,32,64}(&dst, delta, order) Atomic fetch-xor
memcpy(s, d, l) Memory copy
memset(s, b, l) Memory set

There is also a bunch of cvt_a_b instructions which convert {s,u}{8,16,32,64,128} and f{16,32,64} between each other which are not listed here since there's too many permutations of them.

The memcpy and memset instructions are used to initialize variables from literals, but also exists as regular special intrinsics for optimization purposes to help eliminate dead stores and to allow transmute to be implemented in a safe way.

There is no pointer arithmetic.

Syntax

Since C0 is a very minimal subset of the C language it requires little effort to parse so it makes for a good IR as-is. There is no operators because all operations translate to function calls.

Types

C0 defines the following types. Types are really only necessary for variables as all instructions are typed.

Type C type
i8 signed char
u8 unsigned char
i16 signed short
u16 unsigned short
i32 signed int
u32 unsigned int
i64 signed long long
u64 unsigned long long
i128 signed __128i or software
u128 unsigned __i128 or software
f16 half software
f32 float
f64 double
ptr void*

The usual pointer and array versions of these types also exist.

Only 1D array types are supported

Punctuation

The punctuation is minimal.

Punctuation Name
& Address
* Dereference
() Call
, Comma/Sequence
; Semicolon
{} Scope

Literals

The regular int, float, and string literals are supported within C0, they just cannot be used as rvalues since no assignment operation exists. They're only usable as literals to instructions.

Control flow

The following control flow statements exist.

Statement
if
else
for
continue
break
switch
return
goto

Records

The struct and union record types are both used too.

Example

Here's a trivial example of Odin compiled to C0

factorial :: (n: int) -> int {
	if n == 0 do return 1;
	return n * factorial(n - 1);
}
i64 factorial(i64 n) {
	if (eqi64(n, 0)) return 1;
	return muli64(n, factorial(subi64(n, 1)));
}

The prelude would look something like the following.

#define FORCE_INLINE __attribute__((always_inline)) inline

typedef signed long i64;
typedef signed __int128 i128;

FORCE_INLINE int eqi64(i64 a, i64 b) {
	return a == b;
}

FORCE_INLINE i64 muli64(i64 a, i64 b) {
	const i128 lhs = a;
	const i128 rhs = b;
	const i128 sum = lhs * rhs;
	return (i64)(sum & 0xffffffffffffffffull);
}

FORCE_INLINE i64 subi64(i64 a, i64 b) {
	const i128 lhs = a;
	const i128 rhs = b;
	const i128 sum = lhs - rhs;
	return (i64)(sum & 0xffffffffffffffffull);
}

Which when compiled together would generate the following assembly, even on the lowest optimization setting. We have well-defined to wrap signed integer underflow too.

factorial(long):
        mov     eax, 1
        test    rdi, rdi
        jne     .L8
        ret
.L8:
        push    rbx
        mov     rbx, rdi
        lea     rdi, [rdi-1]
        call    factorial(long)
        mul     rbx
        pop     rbx
        ret