Dev log #13
Welcome to the next devlog!
This one is both a dev log and a Hardware Sniffers entry to tell you a bit more about the MISC platform.
A few years ago I started designing a programming language that I thought I wanted for systems programming. I still did and even now do most work in C99 (specifically C99), but the more I do, I increasingly realize that I have many things that I would change to make myself feel like I'm telling the CPU exactly what I want to do without any ambiguity. Last year and early this year I finalized the design of the first revision of this language, which is called DANGC. It is inspired (clearly, based on the name) by C, but it takes a lot from Ada, which blew my mind the first time I saw it (especially SPARKS).
The main concept is that there are no types. I am tired of pretending that that are these various forms of data that we're exchanging between the CPU and memory. It's worse in C, because you could use int and have it mean different things on different platforms.
Saying 8 one_byte_wide_num; is far more expressive than int i_dont_know_how_wide_this_actually_is_guess_it_depends;. If the digit prior to the variable name is confusing, then remember that it's just the width in bits. You interpret those bits however you want. An 8-bit value could be interpreted as an integer, but also as a float. The standard library will provide some utilities to give users common things like IEEE 754 floating-points, but the standard library will itself be a DANGC program. The language doesn't have types.
From the head of DANGC's documentation file:
Neo: #define CHECKED_SK_CMP_FUNC(type, p) ((int (*)(const void *, const void *)) ((1 ? p : (int (*)(const type * const *, const type * const *))0)))
DANGC: Do not try and bend the type. That's impossible. Instead, only try to realize the truth.
Neo: What truth?
DANGC: There is no type.
Neo: There is no type?
DANGC: Then you'll see that it is not the type that bends, it is only yourself.
Neo: sort(64 *sk, 32 n) rets 1 where out == 1'0x0 => sorted(sk, n) == 1'0x1;
It has a built-in profiler and context management, where the main TU has a construct called world (which exists in .bss) and all other TUs have one called subsystem. All work happens in work units called "spins." There is also a built-in concurrency manager called "executor coordinator." An "executor unit" is something like a thread but more generic since MISC doesn't call these "threads." "Execution units" are individual units of work to be run within executor units. Inline assembly can be used for maximum human-readable control.
Here is an example:
example.dngs, a.k.a. a specification file (like a header file):
!INTERROGATE(CYCLES)
dim(32 color, 8 shift [8'0, 8'8)) rets 32 where out\[8'31:8'24] == color[8'31:8'24], out[8'23:8'16] <= color[8'23:8'16], out[8'15:8'8] <= color[8'15:8'8], out[8'7:8'0] <= color[8'7:8'0];
_start(64 *args) rets 64;
spin(64 dt, 64 abst, 64 idxt) rets 1;
example.dngb, a.k.a. a behavior file:
"example.dngs"
init dim(color, shift)
8 r = color[8'23:8'16] >> shift;
8 g = color[8'15:8'8] >> shift;
8 b = color[8'7:8'0] >> shift;
out = {color[8'31:8'24], r, g, b};
ret;
term dim
init _start(args)
call spin(64'0, 64'0, 64'0);
out = 64'0;
ret;
term _start
init spin(dt, abst, idxt)
32 pixel = call dim(32'0xFFCAFE42, 8'1);
64 cycles = dim.CYCLES;
out = 1'1;
ret;
term spin
A bit more involved example:
mlp.dngs:
#define FRAC_BITS 8'8
#define UNIT 64'256
#define RATE_SHIFT 8'2
#define INPUTS 64'2
#define HIDDEN 64'2
#define OUTPUT_WEIGHTS 64'4
#define HIDDEN_BIASES 64'6
#define OUTPUT_BIAS 64'8
#define PARAMS 64'9
#define TRAIN_STEPS 64'32
#define ERROR_Q8_TOLERANCE 64'2
#define LOSS_Q16_TOLERANCE (ERROR_Q8_TOLERANCE * ERROR_Q8_TOLERANCE)
#define SIGN_BIT 8'63
world[4] = prediction;
world[5] = loss_q16;
world[6] = train_step;
world[7] = forward_cycles;
world[8] = backward_cycles;
!INTERROGATE(CYCLES)
forward(64 *weights, 64 *hidden, 64 x0 [64'0, UNIT], 64 x1 [64'0, UNIT]) rets 64 where hidden[64'0][SIGN_BIT] == 1'0, hidden[64'1][SIGN_BIT] == 1'0;
!INTERROGATE(CYCLES)
backward(64 *weights, 64 *hidden, 64 x0 [64'0, UNIT], 64 x1 [64'0, UNIT], 64 prediction, 64 target [64'0, UNIT]) rets 64 where out == (prediction - target) * (prediction - target);
_start(64 *args) rets 64;
spin(64 dt, 64 abst, 64 idxt) rets 1 where out == 1'1, world.train_step == TRAIN_STEPS, world.loss_q16 <= LOSS_Q16_TOLERANCE;
mlp.dngb:
"mlp.dngs"
init forward(weights, hidden, x0, x1)
64 idx = 64'0;
64 base = 64'0;
64 activation = 64'0;
init layer:
base = idx * INPUTS;
activation = ((weights[base] * x0 + weights[base + 64'1] * x1) >>> FRAC_BITS) + weights[HIDDEN_BIASES + idx];
hidden[idx] = activation[SIGN_BIT] == 1'1 ? 64'0 : activation;
idx+;
jmp layer iff (idx < HIDDEN);
term layer
out = ((weights[OUTPUT_WEIGHTS] * hidden[64'0] + weights[OUTPUT_WEIGHTS + 64'1] * hidden[64'1]) >>> FRAC_BITS) + weights[OUTPUT_BIAS];
ret;
term forward
init backward(weights, hidden, x0, x1, prediction, target)
64 err = prediction - target;
64 grad = err + err;
64 delta = 64'0;
64 idx = 64'0;
64 base = 64'0;
out = err * err;
init layer:
base = idx * INPUTS;
delta = (grad * weights[OUTPUT_WEIGHTS + idx]) >>> FRAC_BITS;
delta = hidden[idx] == 64'0 ? 64'0 : delta;
weights[OUTPUT_WEIGHTS + idx] -= ((grad * hidden[idx]) >>> FRAC_BITS) >>> RATE_SHIFT;
weights[base] -= ((delta * x0) >>> FRAC_BITS) >>> RATE_SHIFT;
weights[base + 64'1] -= ((delta * x1) >>> FRAC_BITS) >>> RATE_SHIFT;
weights[HIDDEN_BIASES + idx] -= delta >>> RATE_SHIFT;
idx+;
jmp layer iff (idx < HIDDEN);
term layer
weights[OUTPUT_BIAS] -= grad >>> RATE_SHIFT;
ret;
term backward
init _start(args)
call spin(64'0, 64'0, 64'0);
out = 64'0;
ret;
term _start
init spin(dt, abst, idxt)
64 weights[PARAMS] = {64'64, 64'128, 64'128, 64'64, 64'128, 64'128, 64'0, 64'0, 64'0};
64 hidden[HIDDEN] = {64'0, 64'0};
64 x0 = 64'128;
64 x1 = UNIT;
64 target = 64'192;
64 err = 64'0;
64 initial_loss_q16 = 64'0;
world.prediction = call forward(weights, hidden, x0, x1);
err = world.prediction - target;
initial_loss_q16 = err * err;
world.loss_q16 = initial_loss_q16;
world.forward_cycles = forward.CYCLES;
world.backward_cycles = 64'0;
world.train_step = 64'0;
init training:
world.loss_q16 = call backward(weights, hidden, x0, x1, world.prediction, target);
world.backward_cycles = world.backward_cycles + backward.CYCLES;
world.prediction = call forward(weights, hidden, x0, x1);
world.forward_cycles = world.forward_cycles + forward.CYCLES;
world.train_step = world.train_step + 64'1;
jmp training iff (world.train_step < TRAIN_STEPS);
term training
err = world.prediction - target;
world.loss_q16 = err * err;
out = world.loss_q16 < initial_loss_q16 && world.loss_q16 <= LOSS_Q16_TOLERANCE;
ret;
term spin
Compiling using DANGC's toolchain would print:
align: 0/4 func entries off 64B cacheline; 19/22 local labels off 16B
call-graph:
forward -> (leaf)
backward -> (leaf)
_start -> spin
spin -> backward, forward
and then produce an ELF binary. It doesn't support MISC's EMIRU format yet:
./dangc-mlp-example: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
As you can see, this is on Linux. DANGLING can't run the toolchain at this point but I want to port it as soon as I can.
There will be more DANGC example programs available in Crosstalk throughout Early Access.
This post is a part of the Hardware Sniffers "trivia" series about MISC.
If you're interested in the game, then consider wishlisting:
Thank you!
*DanglingPtr
