aeaether

The Aether programming language

Actors that compile to C.

Sending a message never takes a lock. Every pair of actors gets its own queue, and the scheduler moves the ones that talk most onto the same core.

No VM, no garbage collector. The output is C, built by the compiler you already have.

$ curl -fsSL https://aether-lang.dev/install.sh | sh

Needs only a C compiler. Or grab a prebuilt binary.

counter.ae
message Inc { n: int }

actor Counter {
    state count = 0
    receive {
        Inc(n) -> {
            count = count + n
            println("count = ${count}")
        }
    }
}

main() {
    c = spawn(Counter())
    c ! Inc { n: 41 }
    c ! Inc { n: 1 }
}
$ ae run counter.ae count = 41 count = 42

Inside the scheduler.

Actors are not threads. The runtime pins one scheduler thread per core and moves actors between them to keep every message on the fastest path.

Per-pair queues

Each sender and receiver pair gets its own single-producer, single-consumer queue, so two senders never contend for the same slot.

Cache-line padded

Head and tail sit on separate 64-byte lines, so the sending core and the receiving core never invalidate each other's cache.

Work stealing

An idle core takes whole actors from a busy one, not individual messages, which keeps each actor's state on one core.

Migration

Actors that message each other often get moved onto the same core, where the handoff becomes a direct write.

The compiler knows the scheduler is there. A loop that fans out messages is batched and sorted by target core, and a message carrying a single scalar rides in a register instead of the heap.

Lock striping, without the locks.

One actor owning hot state serializes every access. That is the single-mutex trap wearing a different hat. Spawn several shards instead, route each key to one of them, and operations on different keys land in different mailboxes on different cores.

? asks and waits for the answer, reply sends it back. Both are language syntax, not a library on top.

sharded-map.ae
message Get { key: string }
message GetReply { value: int }

actor Shard {
    state store = map_new()

    receive {
        Get(key) -> {
            result = map_get(store, key)
            reply GetReply { value: result }
        }
    }
}

main() {
    shards = [spawn(Shard()), spawn(Shard()),
              spawn(Shard()), spawn(Shard())]

    // FNV-1a over the key, modulo the shard count
    idx = shard_for(key, 4)
    got = shards[idx] ? Get { key: key }
}
sourcecounter.ae
aetherc
emitsreadable C
gcc / clang / zig
buildsnative binaryLinux · macOS · Windows
FreeBSD · wasm32 · embedded
x86-64 · Arm64 · 32-bit Arm

Readable C, not a black box.

Aether lowers to C, which GCC, Clang or zig compiles to a native binary. The same source always produces byte-identical C, so builds are reproducible.

Enums, optionals, distinct types and contracts are zero-cost. They disappear into plain C, with no vtables and no virtual dispatch.

hello.ae
main() {
    println("hello, aether")
}
hello.c  ·  generated
#include <stdio.h>
/* ... aether runtime preamble ... */

int main(int argc, char** argv) {
    aether_args_init(argc, argv);
    {
        #line 2 "hello.ae"
        puts("hello, aether");
    }
    return 0;
}

Errors without exceptions

Fallible functions return T!, a (value, err) pair. Handle it inline with or, or pass it up with !. No exceptions, no hidden control flow.

divide.ae
safe_divide(a: int, b: int) -> int! {
    if b == 0 { return 0, "division by zero" }
    return a / b
}

q = safe_divide(10, 0) or -1   // -> -1

Contracts the compiler checks

Attach requires and ensures to a function. A predicate the compiler can prove disappears; one it can prove false fails the build, even when the values come from the call site.

contract.ae
divide(a: int, b: int) -> int
    requires b != 0
{
    return a / b
}

divide(10, 0)   // rejected at compile time

The reverse proxy ships with the language.

Three weighted upstreams, health probes every five seconds, and a breaker that opens after five consecutive failures. The same file adds an LRU response cache, per-upstream rate limiting, retries with jittered backoff, and a Prometheus endpoint.

No framework, no sidecar. All of it is std.http.proxy, and the example is in the repo.

http-reverse-proxy-pool-dsl.ae
pool = proxy.pool("weighted_rr", 30, 0, 0) {
    upstream("http://localhost:9001", 3)
    upstream("http://localhost:9002", 2)
    upstream("http://localhost:9003", 1)

    // probe /health every 5s, two good marks it up
    health("/health", 200, 5000, 2000, 2, 2)

    // five failures in a row opens the breaker for 30s
    breaker(5, 30000, 1)
    rate_limit(200, 50)
}

Config that is code.

A trailing block runs in the caller's scope, so a library's setup surface reads like a config file. It is still Aether underneath, with conditionals, loops and environment lookups, and no YAML dialect to learn.

This is the "DSL with scope" pattern, part of the grammar rather than a macro layer on top.

deploy.ae
// a hypothetical app's config file: serve, host,
// port and repo are its DSL, built on Aether
serve {
    host("127.0.0.1")
    port(9990)
    if os_getenv("STAGE") == "prod" {
        host("0.0.0.0")
    }
    repo("alpha", "/srv/alpha")
    repo("beta",  "/srv/beta")
}

What ships with it.

All of this comes with the compiler. There is nothing to add before you start.

One toolchain
A single ae command: init, run, build, test, fmt, and check, plus ae add github.com/user/repo to pull packages from any git host.
Standard library
A production HTTP stack in std.http: routing, TLS, HTTP/2, WebSocket, SSE, and a full reverse proxy with load balancing, health checks, and caching. Plus JSON, a full cryptography suite, regex, UUID, msgpack and CBOR, and compression.
Imports
Imports work like Java's, not C's: import a.b.c is a hierarchical package path resolved to a nested module tree, and calls stay namespaced (c.fn()), no flat global symbol soup. Opt into bare names with import a.b.c (name) or (*), the import static / star-import equivalents.
C interop
Call any C library by declaring an extern. Tuple returns bind struct-returning C APIs with zero glue. Or embed Aether in Python, Java, and Ruby with --emit=lib.
Cross-compile
One flag: ae build --target=aarch64-linux, using zig as the cross toolchain, so no cross-gcc and no sysroot. Cross builds leave out the OpenSSL, zlib, nghttp2 and PCRE2 features. WebAssembly runs the same actor code on a cooperative scheduler.
Capabilities
What a program may touch, the filesystem, the network, other processes, is a language concern here, not a deployment afterthought. hide and seal deny names at compile time, a preloaded shim checks the same grants at the libc boundary and inherits them across exec, and on FreeBSD the kernel enforces them through Capsicum. Every check, allowed or denied, lands in an audit ring you can read from inside the program.
Memory
RAII, emitted by the compiler rather than written as destructors. No garbage collector and no borrow checker: defer frees at scope exit, and heap-string ownership is tracked for you. A caught panic frees exactly what its defers would have, because the runtime journals armed allocations and drains them before it unwinds.

Start writing Aether.

$ curl -fsSL https://aether-lang.dev/install.sh | sh

$ ae init hello && cd hello && ae run