One of the first ideas I have for ten is the ability to control the execution of each function through meta arguments.
In Zig for example, every function that does an allocation requires an
Allocator
. I think this is a very important concept, because it lets you
control how memory will be allocated in your program, and there are environments
where this is a necessity (Embedded, Wasm, ...).
But when I first encountered the Zig allocator pattern, I had been doing embedded development for some time, and I thought "this is not enough, I want to be able to pass more down, for example a HAL on how to do I2C".
The idea is to be able to do led_on()
on Linux or embedded target, and on
Linux it writes 1
to a file and on the embedded target it writes to I2C.
To control this, I thought of meta arguments. Meta arguments are passed to all functions down the stack.
So if you do this for example:
fn a() {
return b();
}
fn b() {
return @alloc(10);
}
fn main() {
a[alloc=my_allocator]();
}
It is like an implicit context (like in OpenGL) but with a clearly defined language semantic. Also, it requires a mechanism to be made thread safe, I don't have the full solution to this yet, but I have some ideas.
Currently, I have a fairly good idea on the memory related meta arguments.
alloc
which defines the allocator to be usedmemory
which defines the memory management strategy, at present I thought ofarc
(automatic reference counting, which would be the default) andleak
which would simply leak memory (for arena allocators). But it could also begc
or anything else.
The meta arguments are available to the functions themselves, but also to the
compiler. For example, the memory
meta argument would be used by the compiler
to introduce retain
and release
calls.
In addition to the memory, as I explained earlier with my led_on()
example, I
want to be able to control all IO (effects).
Some are pretty standard, like time
, but I want to support user defined ones,
like the I2C
example above, the embedded SDK would define custom ones with an
interface.
Something like:
const micro_ten = import("micro_ten"); // Some generic interface definition for
// embedded
const stm32 = import("stm32_hal"); // The actual stm32 implementation
const linux = import("micro_ten.linux_hal"); // Some emulation layer for testing
// We access the hal meta argument inside the micro_ten namespace, which is a
// variable
fn led_on() {
micro_ten@hal.i2c.write(0xff, 0xff);
}
// we run on stm32
fn main() {
led_on[micro_ten=stm32]();
}
// we run on linux
fn main() {
led_on[micro_ten=linux]();
}
I am not fully sure about the syntax, because it puts the function arguments far from the function name, so I might do:
// we run on linux
fn main() {
[micro_ten=linux] {
led_on();
}
}
Also, I am still thinking on how to properly namespace it, but I think you would just have to require the library like this:
// This uses the micro_ten library as
// declared in the build file, can be anything (git repo...)
const micro_ten = import("micro_ten");
const micro_ten2 = import("micro_ten2");
// we run on linux
fn main() {
[micro_ten=linux, micro_ten2=linux] {
led_on();
}
}
Now, in the led_on()
function itself, you also require the library:
// We actually require the micro_ten2 library, even if we call it micro_ten
const micro_ten = import("micro_ten2");
fn led_on() {
micro_ten@hal.i2c.write(0xff, 0xff);
}
Inside the micro_ten
library, you would have the hal trait definition:
// syntax idea
export const hal = {
i2c: trait = {
fn write(a: u8, b: u8);
}
}