Solod: Go can be a better C
I'm working on a new programming language named Solod (So). It's a strict subset of Go that translates to regular C.
Highlights:
- Go in, C out. You write regular Go code and get readable C11 as output.
- Zero runtime. No garbage collection, no reference counting, no hidden allocations.
- Rich standard library. Use familiar types and functions ported from Go's stdlib.
- Native C interop. Call C from So and So from C — no CGO, no overhead.
- Go tooling works out of the box. Syntax highlighting, LSP, linting and package management.
So supports structs, methods, interfaces, slices, maps, multiple returns, and defer. Everything is stack-allocated by default; heap is opt-in through the standard library. There is limited support for generics, and concurrency is provided by the standard library instead of being built into the language.
So is for Go developers who want systems-level control without learning a new language. And for C programmers who like Go's safety, structure, and tooling.
Hello world • Language tour • Memory management • Concurrency • Compatibility • Design decisions • FAQ • Final thoughts
'Hello world' example
This Go code in a file main.go:
package main
import (
"solod.dev/so/conc"
"solod.dev/so/mem"
"solod.dev/so/sync/atomic"
)
// Account is a thread-safe money account.
type Account struct {
Balance atomic.Int64
}
// Deposit adds an amount to the balance.
func (a *Account) Deposit(amount int64) {
a.Balance.Add(amount)
}
// pay deposits $10 into the shared account.
func pay(arg any) {
acc := arg.(*Account)
acc.Deposit(10)
}
func main() {
var acc Account
// Run 100 payments across 4 worker threads.
opts := conc.PoolOptions{NumThreads: 4}
pool := conc.NewPool(mem.System, opts)
defer pool.Free()
for range 100 {
pool.Go(pay, &acc)
}
pool.Wait()
println("balance is", acc.Balance.Load())
}
Translates to a header file main.h:
#pragma once
#include "so/builtin/builtin.h"
#include "so/conc/conc.h"
#include "so/mem/mem.h"
#include "so/sync/atomic/atomic.h"
// Account is a thread-safe money account.
typedef struct main_Account {
atomic_Int64 Balance;
} main_Account;
// Deposit adds an amount to the balance.
void main_Account_Deposit(void* self, int64_t amount);
Plus an implementation file main.c:
#include "main.h"
// Deposit adds an amount to the balance.
void main_Account_Deposit(void* self, int64_t amount) {
main_Account* a = self;
atomic_Int64_Add(&a->Balance, amount);
}
// pay deposits $10 into the shared account.
static void pay(void* arg) {
main_Account* acc = (main_Account*)arg;
main_Account_Deposit(acc, 10);
}
int main(void) {
main_Account acc = {0};
// Run 100 payments across 4 worker threads.
conc_PoolOptions opts = (conc_PoolOptions){.NumThreads = 4};
conc_Pool* pool = conc_NewPool(mem_System, opts);
for (so_int _i = 0; _i < 100; _i++) {
conc_Pool_Go(pool, pay, &acc);
}
conc_Pool_Wait(pool);
so_println("%s %" PRId64, "balance is", atomic_Int64_Load(&acc.Balance));
conc_Pool_Free(pool);
return 0;
}
Language tour
In terms of features, So is an intersection between Go and C, making it one of the simplest C-like languages out there — on par with Hare.
And since So is a strict subset of Go, you already know it if you know Go. It's pretty handy if you don't want to learn another syntax.
Let's briefly go over the language features and see how they translate to C.
Variables • Strings • Arrays • Slices • Maps • If/else and for • Functions • Multiple returns • Structs • Methods • Interfaces • Enums • Errors • Defer • C interop • Packages
Values and variables
So supports basic Go types and variable declarations:
// so
const n = 100_000
f := 3.14
var r = '本'
var v any = 42
// c
const so_int n = 100000;
double f = 3.14;
so_rune r = U'本';
void* v = &(so_int){42};
byte is translated to so_byte (uint8_t), rune to so_rune (int32_t), and int to so_int (int64_t).
any is not treated as an interface. Instead, it's translated to void*. This makes handling pointers much easier and removes the need for unsafe.Pointer.
nil is translated to NULL (for pointer types).
Strings
Strings are represented as so_String type in C:
// c
typedef struct {
const char* ptr;
so_int len;
} so_String;
All standard string operations are supported, including indexing, slicing, and iterating with a for-range loop.
// so
str := "Hi 世界!"
println("str[1] =", str[1])
for i, r := range str {
println("i =", i, "r =", r)
}
// c
so_String str = so_str("Hi 世界!");
so_println("%s %u", "str[1] =", so_at(so_byte, str, 1));
for (so_int i = 0, _iw = 0; i < so_len(str); i += _iw) {
_iw = 0;
so_rune r = so_utf8_decode(str, i, &_iw);
so_println("%s %" PRId64 " %s %d", "i =", i, "r =", r);
}
Converting a string to a byte slice and back is a zero-copy operation:
// so
s := "1世3"
bs := []byte(s)
s1 := string(bs)
// c
so_String s = so_str("1世3");
so_Slice bs = so_string_bytes(s); // wraps s.ptr
so_String s1 = so_bytes_string(bs); // wraps bs.ptr
Converting a string to a rune slice and back allocates on the stack with alloca:
// so
s := "1世3"
rs := []rune(s)
s1 := string(rs)
// c
so_String s = so_str("1世3");
so_Slice rs = so_string_runes(s); // allocates
so_String s1 = so_runes_string(rs); // allocates
There's a so/strings stdlib package for heap-allocated strings and various string operations.
Arrays
Arrays are represented as plain C arrays (T name[N]):
// so
var a [5]int // zero-initialized
b := [5]int{1, 2, 3, 4, 5} // explicit values
c := [...]int{1, 2, 3, 4, 5} // inferred size
d := [...]int{100, 3: 400, 500} // designated initializers
// c
so_int a[5] = {0};
so_int b[5] = {1, 2, 3, 4, 5};
so_int c[5] = {1, 2, 3, 4, 5};
so_int d[5] = {100, [3] = 400, 500};
len() on an array is emitted as a compile-time constant, and slicing an array produces a so_Slice.
Slices
Slices are represented as so_Slice type in C:
// c
typedef struct {
void* ptr;
so_int len;
so_int cap;
} so_Slice;
All standard slice operations are supported, including indexing, slicing, and iterating with a for-range loop.
// so
s1 := []string{"a", "b", "c", "d", "e"}
s2 := s1[1 : len(s1)-1]
for i, v := range s2 {
println(i, v)
}
// c
so_Slice s1 = (so_Slice){(so_String[5]){
so_str("a"), so_str("b"), so_str("c"),
so_str("d"), so_str("e")}, 5, 5};
so_Slice s2 = so_slice(so_String, s1, 1, so_len(s1) - 1);
for (so_int i = 0; i < so_len(s2); i++) {
so_String v = so_at(so_String, s2, i);
so_println("%" PRId64 " %.*s", i, v.len, v.ptr);
}
As in Go, a slice is a value type. Unlike in Go, a nil slice and an empty slice are the same thing:
// so
var nils []int = nil
var empty []int = []int{}
// c
so_Slice nils = (so_Slice){0};
so_Slice empty = (so_Slice){0};
make() allocates a fixed amount of memory on the stack (sizeof(T)*cap). append() only works up to the initial capacity and panics if it's exceeded. There's no automatic reallocation; use the so/slices stdlib package for heap allocation and dynamic arrays.
Maps
Maps are fixed-size and stack-allocated, backed by "mask-step-index" hashtables. They are pointer-based reference types, represented as so_Map* in C. No delete, no resize.
// c
typedef struct {
void* keys;
void* vals;
so_int len;
so_int cap;
} so_Map;
Only use maps when you have a small, fixed number of items (<1024). For anything else, use heap-allocated maps from the so/maps package.
Most of the standard map operations are supported, including getting/setting values and iterating with a for-range loop:
// so
m := map[string]int{"a": 11, "b": 22}
for k, v := range m {
println(k, v)
}
// c
so_Map* m = &(so_Map){(so_String[2]){
so_str("a"), so_str("b")},
(so_int[2]){11, 22}, 2, 2};
for (so_int _i = 0; _i < (so_int)m->len; _i++) {
so_String k = ((so_String*)m->keys)[_i];
so_int v = ((so_int*)m->vals)[_i];
so_println("%.*s %" PRId64, k.len, k.ptr, v);
}
As in Go, a map is a pointer type. A nil map emits as NULL in C.
If/else and for
If-else and for come in all shapes and sizes, just like in Go: else-if chaining, the three-part for loop, the while-style loop, and range over an integer. All of them map to their C equivalents one to one.
The one exception is an if statement with an init statement — it becomes a scoped block in C:
// so
if num := 9; num < 10 {
println(num, "has 1 digit")
}
// c
{
so_int num = 9;
if (num < 10) {
so_println("%" PRId64 " %s", num, "has 1 digit");
}
}
switch is also supported, both tagged and tagless, and translates to an if/else-if chain. Labels, goto, and labeled break map directly to C.
Functions
Regular functions translate to C naturally:
// so
func sumABC(a, b, c int) int {
return a + b + c
}
// c
static so_int sumABC(so_int a, so_int b, so_int c) {
return a + b + c;
}
Named function types become typedefs:
// so
type SumFn func(int, int, int) int
fn1 := sumABC // infer type
var fn2 SumFn = sumABC // explicit type
s := fn2(7, 8, 9)
// main.h
typedef so_int (*main_SumFn)(so_int, so_int, so_int);
// main.c
main_SumFn fn1 = sumABC;
main_SumFn fn2 = sumABC;
so_int s = fn2(7, 8, 9);
Exported functions (capitalized) become public C symbols prefixed with the package name (package_Func). Unexported functions are static.
Variadic functions use the standard ... syntax and translate to passing a slice:
// so
func sum(nums ...int) int {
total := 0
for _, num := range nums {
total += num
}
return total
}
func main() {
sum(1, 2, 3, 4, 5)
}
// c
static so_int sum(so_Slice nums) {
so_int total = 0;
for (so_int _ = 0; _ < so_len(nums); _++) {
so_int num = so_at(so_int, nums, _);
total += num;
}
return total;
}
int main(void) {
sum((so_Slice){(so_int[5]){1, 2, 3, 4, 5}, 5, 5});
}
Function literals (anonymous functions and closures) are not supported.
Multiple returns
So supports two-value multiple returns in two patterns: (T, error) and (T1, T2). Both cases translate to signature-specific C types:
// so
func divide(a, b int) (int, error) {
return a / b, nil
}
func divmod(a, b int) (int, int) {
return a / b, a % b
}
// c
typedef struct { so_int val; so_Error err; } so_R_int_err;
typedef struct { so_int val; so_int val2; } so_R_int_int;
// c
static so_R_int_err divide(so_int a, so_int b) {
return (so_R_int_err){.val = a / b, .err = NULL};
}
static so_R_int_int divmod(so_int a, so_int b) {
return (so_R_int_int){.val = a / b, .val2 = a % b};
}
Named return values are not supported.
Structs
Structs translate to C naturally:
// so
type person struct {
name string
age int
}
bob := person{"Bob", 20}
alice := person{name: "Alice", age: 30}
fred := person{name: "Fred"}
// c
typedef struct person {
so_String name;
so_int age;
} person;
person bob = (person){so_str("Bob"), 20};
person alice = (person){.name = so_str("Alice"), .age = 30};
person fred = (person){.name = so_str("Fred")};
new() works with types and values:
// so
n := new(int) // *int, zero-initialized
p := new(person) // *person, zero-initialized
n2 := new(42) // *int with value 42
p2 := new(person{name: "Alice"}) // *person with values
// c
so_int* n = &(so_int){0};
person* p = &(person){0};
so_int* n2 = &(so_int){42};
person* p2 = &(person){.name = so_str("Alice")};
Methods
Methods are defined on struct types with pointer or value receivers:
// so
type Rect struct {
width, height int
}
func (r *Rect) Area() int {
return r.width * r.height
}
func (r Rect) resize(x int) Rect {
r.height *= x
r.width *= x
return r
}
Pointer receivers pass void* self in C and cast to the struct pointer. Value receivers pass the struct by value, so modifications operate on a copy:
// c
typedef struct main_Rect {
so_int width;
so_int height;
} main_Rect;
so_int main_Rect_Area(void* self) {
main_Rect* r = (main_Rect*)self;
return r->width * r->height;
}
static main_Rect main_Rect_resize(main_Rect r, so_int x) {
r.height *= x;
r.width *= x;
return r;
}
As in Go, calling a method on a value or a pointer takes the address or dereferences automatically, so r.Area() emits main_Rect_Area(&r).
Interfaces
Interfaces in So are like Go interfaces, but they don't include runtime type information.
Interface declarations list the required methods:
// so
type Shape interface {
Area() int
Perim(n int) int
}
In C, an interface is a struct with a void* self pointer and function pointers for each method (less efficient than using a static method table, but simpler; this might change in the future):
// c
typedef struct main_Shape {
void* self;
so_int (*Area)(void* self);
so_int (*Perim)(void* self, so_int n);
} main_Shape;
Just as in Go, a concrete type implements an interface by providing the necessary methods:
// so
func (r *Rect) Area() int {
// ...
}
func (r *Rect) Perim(n int) int {
// ...
}
// c
so_int main_Rect_Area(void* self) {
// ...
}
so_int main_Rect_Perim(void* self, so_int n) {
// ...
}
Passing a concrete type to functions that accept interfaces:
// so
func calcShape(s Shape) int {
return s.Perim(2) + s.Area()
}
r := Rect{width: 10, height: 5}
calcShape(&r) // implicit conversion
calcShape(Shape(&r)) // explicit conversion
// c
static so_int calcShape(main_Shape s) {
return s.Perim(s.self, 2) + s.Area(s.self);
}
main_Rect r = (main_Rect){.width = 10, .height = 5};
calcShape((main_Shape){.self = &r,
.Area = main_Rect_Area,
.Perim = main_Rect_Perim});
calcShape((main_Shape){.self = &r,
.Area = main_Rect_Area,
.Perim = main_Rect_Perim});
Type assertion works for concrete types (v := iface.(*Type)), but not for interfaces (iface.(Interface)). Type switch is not supported.
Empty interfaces (interface{} and any) are translated to void*.
Enums
So supports typed constant groups as enums, with iota for integer-typed ones. Each constant is emitted as a C const, and iota values are evaluated at compile time:
// so
type Day int
const (
Sunday Day = iota
Monday
Tuesday
)
// main.h
typedef so_int main_Day;
static const main_Day main_Sunday = 0;
static const main_Day main_Monday = 1;
static const main_Day main_Tuesday = 2;
String-typed constant groups work the same way, emitting so_str("...") values.
Errors
The error type is a regular interface with an Error() string method. In C, it is represented as an so_Error interface struct:
// c
typedef struct {
void* self;
so_String (*Error)(void* self);
} so_Error;
Use errors.New to create sentinel errors at the package level:
// so
import "solod.dev/so/errors"
var ErrOutOfTea = errors.New("no more tea available")
// c
#include "so/errors/errors.h"
so_Error main_ErrOutOfTea = errors_New("no more tea available");
Errors are compared using ==. This is an O(1) operation (compares pointers, not strings):
// so
func makeTea(arg int) error {
if arg == 42 {
return ErrOutOfTea
}
return nil
}
err := makeTea(42)
if err == ErrOutOfTea {
println("out of tea")
}
// c
static so_Error makeTea(so_int arg) {
if (arg == 42) {
return main_ErrOutOfTea;
}
return NULL;
}
so_Error err = makeTea(42);
if (err == main_ErrOutOfTea) {
so_println("%s", "out of tea");
}
Dynamic errors (fmt.Errorf) and error wrapping are not supported.
Defer
defer schedules a function or method call to run at the end of the enclosing function (as in Go):
// so
func funcScope() {
xopen(&state)
defer xclose(&state)
if state != 1 {
panic("unexpected state")
}
}
Deferred calls are emitted inline (before returns, panics, and function end) in LIFO order:
// c
static void funcScope(void) {
xopen(&state);
if (state != 1) {
xclose(&state);
so_panic("unexpected state");
}
xclose(&state);
}
C interop
Include a C header file with so:include:
//so:include <stdio.h>
Declare an external C type (excluded from emission) with so:extern:
//so:extern FILE
type os_file struct{}
Declare an external C function:
//so:extern
func fopen(path string, mode string) *os_file { return nil }
When calling extern functions, string and []T arguments are automatically decayed to their C equivalents: string literals become raw C strings ("hello"), string values become char*, and slices become raw pointers. This makes interop cleaner:
// so
f := fopen("/tmp/test.txt", "w")
// c
os_file* f = fopen("/tmp/test.txt", "w");
// not like this:
// fopen(so_str("/tmp/test.txt"), so_str("w"))
The decay behavior can be turned off with the nodecay flag:
//so:extern nodecay
func set_name(acc *Account, name string)
The so/c package includes helpers for converting C pointers back to So string and slice types. The unsafe package is also available and is implemented as compiler built-ins.
Packages
Each Go package is translated into a single .h + .c pair, regardless of how many .go files it contains. Multiple .go files in the same package are merged into one .c file, separated by // -- filename.go -- comments.
Exported symbols (capitalized names) are prefixed with the package name:
// geom/geom.go
package geom
const Pi = 3.14159
func RectArea(width, height float64) float64 {
return width * height
}
Becomes:
// geom.h
extern const double geom_Pi;
double geom_RectArea(double width, double height);
// geom.c
const double geom_Pi = 3.14159;
double geom_RectArea(double width, double height) { ... }
Unexported symbols (lowercase names) keep their original names and are marked static:
// c
static double rectArea(double width, double height);
Exported symbols are declared in the .h file (with extern for variables). Unexported symbols only appear in the .c file.
Importing a So package translates to a C #include, and calling an imported symbol uses the package prefix: geom.RectArea(5, 10) becomes geom_RectArea(5, 10).
That's it for the language tour!
The standard library ports a growing subset of Go's stdlib: strings, bytes, strconv, fmt, io, bufio, os, net, time, math, encoding/json, log/slog, and more. Two areas deserve a closer look because, unlike Go, Solod has no runtime to provide them: memory management and concurrency.
Memory management
There is no garbage collector and no reference counting. Everything is stack-allocated by default, and the language built-ins never touch the heap: make reserves a fixed amount of stack space, append panics past the initial capacity, and new returns a pointer to a stack value.
Heap allocation lives in so/mem, behind a three-method Allocator interface:
type Allocator interface {
Alloc(size int, align int) (any, error)
Realloc(ptr any, oldSize int, newSize int, align int) (any, error)
Free(ptr any, size int, align int)
}
Anything that allocates takes an allocator and states in its documentation who owns the result. You free it yourself, often using defer:
// so
p := mem.Alloc[point](mem.System)
defer mem.Free(mem.System, p)
p.x, p.y = 3, 4
// c
point* p = mem_Alloc(point, (mem_System));
p->x = 3;
p->y = 4;
mem_Free(point, (mem_System), (p));
The library ships four allocators:
Systemis backed by Ccalloc/realloc/free.Arenabump-allocates from a fixed buffer and frees everything at once withReset.Trackerwraps another allocator and counts allocations, so a test can assert that nothing leaked.NoAllocfails every allocation, so you can prove that a code path does not allocate.
Since an arena takes a regular byte slice, you can back the "heap" with stack memory:
var buf [1024]byte
arena := mem.NewArena(buf[:])
nums := slices.Make[int](&arena, 4)
arena.Reset()
The same convention applies throughout the rest of the library. so/slices and so/maps are growable counterparts to built-in slices and maps, and their allocating functions (Make, Append, Free) take the allocator as the first argument:
nums := slices.Make[int](mem.System, 0)
defer slices.Free(mem.System, nums)
nums = slices.Append(mem.System, nums, 1, 2, 3)
Concurrency
There is no go statement, no built-in chan, and no select. Concurrency is OS threads, provided by so/conc.
conc.Go starts a single thread, and conc.Pool runs tasks with a fixed set of workers (that's the pool from the hello-world example above). OS threads are not cheap to start, so a pool is the right tool for short-lived or numerous tasks:
func bump(arg any) any {
c := arg.(*counter)
c.n++
return nil
}
var cnt counter
th := conc.Go(bump, &cnt)
th.Wait()
conc.Chan[T] is a thread-safe FIFO channel, buffered or unbuffered, with optional send and receive timeouts. It carries values by copy:
ch := conc.NewChan[int](mem.System, 2)
defer ch.Free()
ch.Send(1)
ch.Send(2)
ch.Close()
var v int
for ch.Recv(&v) {
println("got:", v)
}
so/sync provides mutexes, condition variables, and once-only initialization, all backed by pthreads. Unlike Go, the zero value is not ready to use: each primitive has Init and Free.
var mu sync.Mutex
mu.Init()
defer mu.Free()
mu.Lock()
count++
mu.Unlock()
so/sync/atomic provides low-level atomic memory primitives, just like Go.
Compatibility
So generates C11 code that relies on several GCC/Clang extensions:
- Binary literals (
0b1010) in generated code. - Statement expressions (
({...})) in macros. __attribute__((constructor))for package-level initialization.__auto_typefor local type inference in generated code.__typeof__for type inference in generic macros.allocaand VLAs formake()and other dynamic stack allocations.
You can use GCC, Clang, or zig cc to compile the transpiled C code. MSVC is not supported.
Supported operating systems: Linux, macOS, and Windows (core language only).
Supported platforms: amd64, arm64, riscv64, i386, wasm32, and freestanding environments.
Design decisions
So is highly opinionated.
Simplicity is key. Fewer features are always better. Every new feature is strongly discouraged by default and should be added only if there are very convincing real-world use cases to support it. This applies to the standard library too — So tries to export as little of Go's stdlib API as possible while still remaining highly useful for real-world use cases.
No heap allocations are allowed in language built-ins (like maps, slices, new, or append). Heap allocations are allowed in the standard library, but they have to be explicit. If a function or type allocates memory, it must take an allocator and clearly state ownership in the documentation.
Fast and easy C interop. Even though So uses Go syntax, it's basically C with its own standard library. Calling C from So, and So from C, should always be simple to write and run efficiently. The So standard library (translated to C) should be easy to add to any C project.
Performance. You can definitely write C code by hand that runs faster than code produced by So. Also, some features in So, like interfaces, are currently implemented in a way that's not the most efficient, mainly to keep things simple. Still, performance matters: the code is benchmarked and optimized to match or beat Go in speed and resource usage.
Readability. There are several languages that claim they can transpile to readable C code. Unfortunately, the C code they generate is usually unreadable or barely readable at best. So isn't perfect in this area either (though it's arguably better than others), but it aims to produce C code that's as readable as possible.
Go compatibility. So code is syntactically valid Go code, with no exceptions. Semantics may differ.
Non-goals:
Hiding C entirely. So is a cleaner way to write C, not a replacement for it. You should be familiar with C to use So effectively.
Go feature parity. Less is more. Iterators aren't coming, and neither are "true" generics.
Frequently asked questions
I have heard these several times, so it's worth answering.
Why not Rust/Zig/Odin/other language?
Because I like C and Go.
Why not TinyGo?
TinyGo is lightweight, but it still has a garbage collector, a runtime, and aims to support all Go features. What I'm after is something even simpler, with no runtime at all, source-level C interop, and eventually, Go's standard library ported to plain C so it can be used in regular C projects.
How does So handle memory?
Everything is stack-allocated by default. There's no garbage collector or reference counting. The standard library provides explicit heap allocation in the so/mem package when you need it — see Memory above.
Is it safe?
So has extra safeguards beyond Go's default type checking:
- It will panic on out-of-bounds array access and nil dereference.
- It won't let you return stack-allocated memory in common situations.
- Programs can detect memory leaks with a tracking allocator.
However, escape analysis doesn't catch every case, and the leak checker won't detect double-free or use-after-free errors by itself.
Most memory-related problems can be caught with AddressSanitizer in modern compilers. I strongly recommend turning on sanitizers with the -sanitize flag while developing. Or set the sanitize flags in CFLAGS yourself.
Is it fast?
Usually on par with Go or faster — see the benchmark link at the end of the post for details. Concurrency stays within ~1.1x of Go's performance for worker pool scenarios, but is much slower for anything that needs to park and wake an OS thread — no magic here.
Can I use So code from C (and vice versa)?
Yes. So compiles to plain C, therefore calling So from C is just calling C from C. Calling C from So is equally straightforward.
Can I compile existing Go packages with So?
Not really. Go uses automatic memory management, while So uses manual memory management. So also supports far fewer features than Go. Neither Go's standard library nor third-party packages will work with So without changes.
How stable is this?
Not for production at the moment. But ready for hobby projects!
Where's the standard library?
See Memory and Concurrency above for the two most important parts. Beyond those, there are packages for strings, files, networking, time, JSON, logging, and more, plus a low-level so/c package to help with C interop. Check out the standard library overview for the full list.
Final thoughts
Solod is still in its early stages, but now is a good time to give it a try — or just keep an eye on it if you like the concept.
Further reading: Installation • Usage • Language tour • Standard library • So by example • Playground • Benchmarks • Source code
★ Subscribe to keep up with new posts.