If you're familiar with brainfuck, you'll know it doesn't need to represent constants; however, they're really useful for some optimizations. My interpreter converts the brainfuck code into a bytecode which contains integer offsets. My compiler works by converting the bytecode into a disgusting ball of generic types. So I needed constants, and if I couldn't handle constants, the project was probably doomed anyway.
Behold:
interface Const {
int Run();
}
// Single Hex Digits
struct D0 : Const { public int Run() => 0; }
struct D1 : Const { public int Run() => 1; }
struct D2 : Const { public int Run() => 2; }
// D3 - DD omitted
struct DE : Const { public int Run() => 0xE; }
struct DF : Const { public int Run() => 0xF; }
// Two Hex Digits
struct Num<A,B> : Const
where A: struct, Const
where B: struct, Const
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Run() {
return default(A).Run()<<4 | default(B).Run();
}
}
// Negatives
struct Neg<A> : Const
where A: struct, Const
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Run() {
return -default(A).Run();
}
}
With these building blocks, we can represent any integer constant. We can verify it works with Sharplab:
class Example {
int Run() {
return default(Neg<Num<D2,DF>>).Run();
}
}
// compiles to
L0000: mov eax, 0xffffffd1
L0005: ret