Comment by Joker_vD
1 year ago
Well, I am glad to inform you that I am writing a language (working name "Troglodyte") that will have exactly the desired ("do what I wrote, damn it!") semantics with regards to memory allocations, pointer casting etc. specifically for cavemen like me who sometimes want something more high-level than assembler but still low-level enough to be able to accidentally drop a sharp rock on my foot.
It will have machine-native integers, arrays of such integers, arrays of bytes, functions that take/return machine-wide integers, and that's pretty much it for the data types. All arithmetic is two-complement, signed or unsigned as you desire, and pointers are just numbers as well. Quality-of-life features include being able to straight-up include arbitrary binaries as function definitions:
func [[raw]] builtin_assembler_is_too_much_work(a, b) "\x48\x89\xF8\x48\x99\x48\xF7\xEE\xC3";
I'm very sympathetic to wanting a language that gives you direct access to the same control assembly does while letting you delegate register allocation / isel / calling convention and so forth to a compiler where it doesn't matter so much.
LLVM IR is interesting in that respect. Some libraries are written in it directly (all compiler runtimes afaik but there are probably others). The ergonomics on it are rather poor but writing exactly the control flow graph you want does work. It doesn't give control over register allocation or scheduling but does give control over the CFG and a decent approximation to instruction selection.
Some years ago I wrote compute kernels in terms of vector types and compiler intrinsics, including some that could target DSP style compute-and-branch operations. Adjusting the code and the compiler simultaneously worked really well - matched and thus replaced the handwritten assembly for the simpler cases. I miss that ISA / toolchain combination.
I'd be curious to hear what troglodyte can handle that C with compiler extensions and the type aliasing optimisations discarded cannot. Specifically the "language" I really should get around to writing for x64 and/or amdgpu would be "C" with:
1. an intrinsic for each instruction, with a function type. Probably as a header file with a lot of declarations in it.
2. mark variables as allocated to specific registers, or maybe in a set of registers. Syntax.
3. some notation for bespoke calling conventions. Syntax.
4. some notation for ordering instructions (i.e. scheduling them)
5. a lattice of address spaces (global outlives stack etc, a bit gpu specific)
The intrinsic per instruction definitely works. That gives a pretty way to write AVX code or similar. Marking variables as allocated to specific registers also works (gcc does this to an extent with the asm annotation). Point 3 and onwards is where I get hazy on how to make it work sensibly. Custom calling conventions work really well in a compiler backend, and in C they need to be expressed in the function type, but I'm still obsessing how best to do that.