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.
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.
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
andmemset
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 allowtransmute
to be implemented in a safe way.
There is no pointer arithmetic.
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.
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
The punctuation is minimal.
Punctuation | Name |
---|---|
& |
Address |
* |
Dereference |
() |
Call |
, |
Comma/Sequence |
; |
Semicolon |
{} |
Scope |
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.
The following control flow statements exist.
Statement |
---|
if |
else |
for |
continue |
break |
switch |
return |
goto |
The struct
and union
record types are both used too.
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