Aether is a compiled, object-oriented 4GL designed specifically for operating system development. It bridges the gap between high-level expressiveness and bare-metal control. Every design decision serves three goals: zero-cost abstractions that compile to readable NASM, deterministic automatic memory management baked into the binary, and seamless integration with freestanding x86_64 environments.
- Compiled only, no interpreter. The compiler emits NASM assembly, assembles with NASM, and links with LD. No runtime, no VM, no JIT.
- Automatic memory is the default. Bump allocators, region-based allocation, and automatic destructor chains are emitted by the compiler. The programmer never calls
freeordelete. - Classes are optional. The language is fully usable without OOP. Functions, structs, enums, and procedural code are first-class citizens.
- References over pointers. The compiler prefers
refsemantics (borrowed, non-nullable, bounds-checked where possible). Raw pointers exist for hardware access and inline assembly. - Inline NASM assembly. Any function can contain raw NASM blocks. The compiler passes them through verbatim with label resolution.
- Deterministic exceptions.
try/catchwith full stack unwinding, scope cleanup (destructors + defer), and no runtime type information. Compiled to explicit jump tables and cleanup code. - Freestanding by default. No libc dependency. The compiler generates code that runs on bare metal, in kernel space, or hosted on the target OS.
- Multi-target output. The same source can compile to Mach-O (macOS host), ELF (Linux host), flat binary (boot sector), kernel ELF, or relocatable module.
- Universal binary support. The compiler can emit multi-architecture binaries containing x86_64, ARM64, and RISC-V code in a single file, with a thin dispatcher at the entry point.
Aether syntax is brace-delimited, newline-sensitive for statement separation, and visually clean. It draws inspiration from Go (simplicity), Rust (safety concepts), and Python (readability), but is not a clone of any of them.
// Hello World
func main() {
print("Hello, Aether!\n")
}
- Braces
{}for blocks. Functions, if/else, loops, try/catch, classes, structs, enums all use braces. - Newlines separate statements. Semicolons are optional and can be used to put multiple statements on one line.
//for line comments,/* */for block comments.- Identifiers: letters, digits, underscores. Must start with a letter or underscore.
- Case-sensitive.
fooandFooare different. _is the blank identifier. Discards values.
| Type | Size | Description |
|---|---|---|
u8 |
1 byte | Unsigned 8-bit integer |
u16 |
2 bytes | Unsigned 16-bit integer |
u32 |
4 bytes | Unsigned 32-bit integer |
u64 |
8 bytes | Unsigned 64-bit integer |
i8 |
1 byte | Signed 8-bit integer |
i16 |
2 bytes | Signed 16-bit integer |
i32 |
4 bytes | Signed 32-bit integer |
i64 |
8 bytes | Signed 64-bit integer |
f32 |
4 bytes | IEEE 754 single-precision float |
f64 |
8 bytes | IEEE 754 double-precision float |
bool |
1 byte | true or false |
char |
1 byte | ASCII character |
str |
16 bytes | String view: {ptr: u64, len: u64} |
void |
0 bytes | No value (for functions) |
typeid |
8 bytes | Runtime type identifier (opaque) |
| Type | Description |
|---|---|
[N]T |
Fixed-size array of N elements of type T |
[]T |
Dynamic slice: {ptr: u64, len: u64} |
ref T |
Immutable reference to T (non-nullable, borrowed) |
mut ref T |
Mutable reference to T |
*T |
Raw pointer to T (nullable, unchecked) |
?T |
Optional type: {has_value: bool, value: T} |
struct |
Named product type with fields |
enum |
Tagged union with optional payloads |
class |
OOP type with methods, fields, auto-destructor |
trait |
Interface definition (compile-time dispatch) |
dyn T |
Dynamic trait object (runtime dispatch via vtable) |
Standard C-style operators with these additions:
| Operator | Description |
|---|---|
+ |
Numeric add OR string concatenation (when either operand is a string) |
ref |
Take reference |
mut |
Take mutable reference |
owned |
Take ownership (move semantics) |
as |
Type cast |
is |
Type check (for enums with payloads) |
.. |
Range (inclusive start, exclusive end) |
..= |
Range (inclusive both ends) |
?? |
Nil-coalescing (unwrap optional or default) |
?. |
Optional chaining |
! |
Force-unwrap optional (panics on nil) |
Strings support inline interpolation with {expr} syntax:
let name = "Aether"
let msg = "Hello, {name}! The answer is {6 * 7}.\n"
// msg = "Hello, Aether! The answer is 42.\n"
The compiler desugars interpolation into concatenation calls at compile time. Any expression that can be converted to a string (numbers, bools, types with a format method) is valid inside {}.
Aether has no garbage collector and no manual free. Memory management is entirely compile-time determined and baked into the binary. The compiler analyzes lifetimes and inserts allocation, deallocation, and destructor calls automatically.
| Strategy | Description | Used For |
|---|---|---|
| Bump allocation | Linear scan through a pre-allocated arena. O(1) alloc, no free. | Temporary objects, per-function scratch |
| Region allocation | Arena with rollback. Allocations in a region are freed together. | Per-request, per-frame, per-transaction |
| Stack allocation | All variables live on the stack by default. No heap overhead. | Local variables, small structs |
| Heap allocation | Explicit alloc keyword. Paired with automatic destructor. |
Long-lived objects, dynamic data |
When a class instance or heap-allocated value goes out of scope, the compiler:
- Calls the class destructor (if defined)
- Calls destructors for all member fields (recursively)
- Frees the memory (for heap allocations)
This is emitted as explicit code in the compiled binary — no runtime, no GC, no reference counting.
class Buffer {
data: [100]u8
len: u32
destructor() {
print("Buffer destroyed\n")
}
}
func example() {
let buf = Buffer{} // stack-allocated
// ... use buf ...
} // compiler emits: buf.destructor() automatically
defer schedules cleanup code to run at scope exit, regardless of how the scope is exited (normal return, exception, break). Defers run in reverse order of declaration (LIFO).
func read_file(path: str) -> ?str {
let f = open(path)
defer close(f) // runs when scope exits
let data = read_all(f)
return data
} // close(f) called automatically even on early return
Regions provide scoped arena allocation. All allocations within a region are freed when the region ends.
region frame {
let obj = alloc MyObject{} // allocated in region
// ... use obj ...
} // all region allocations freed here
Aether uses a simplified ownership model:
- Owned values (
ownedkeyword): one owner at a time. Ownership can be transferred (moved). - References (
ref): borrowed, non-owning views. The compiler ensures the reference doesn't outlive the referent. - Mutable references (
mut ref): exclusive, mutable borrow. Only one mutable reference to a value at a time.
This is enforced at compile time without a borrow checker as complex as Rust's — Aether uses a simpler, more permissive model that catches use-after-free and double-free but allows more patterns.
Aether has deterministic exception handling with full stack unwinding. No exception objects, no RTTI, no heap allocation for exceptions.
func risky() throws {
if condition {
throw "something went wrong"
}
}
func main() {
try {
risky()
} catch {
print("caught an error!\n")
}
}
The compiler emits:
- Before try body: Save the stack frame and set up a jump buffer (sigsetjmp on host, custom IDT-based for kernel)
- Try body: Execute normally
- On throw: Walk the cleanup table (innermost scope first), call destructors and defers, then jump to the catch handler
- After try body: Clear the jump buffer
- Catch handler: Execute recovery code
Functions that can throw are marked with throws. Callers must either handle the error with try/catch or propagate with throws.
func inner() throws {
throw "fail"
}
func outer() throws {
inner() // propagates automatically
}
func main() {
try {
outer()
} catch {
print("outer failed\n")
}
}
On host targets, segfaults in try blocks are caught via sigsetjmp/siglongjmp. The compiler emits a C helper that sets up signal handlers for SIGSEGV and SIGBUS, then restores the jump buffer on catch.
Classes are the primary OOP construct. They support fields, methods, constructors, destructors, and inheritance.
class Animal {
name: str
age: u32
constructor(name: str) {
this.name = name
this.age = 0
}
destructor() {
print("{this.name} says goodbye\n")
}
func speak() {
print("{this.name} makes a sound\n")
}
}
Single inheritance with virtual dispatch via vtable.
class Dog : Animal {
breed: str
constructor(name: str, breed: str) : super(name) {
this.breed = breed
}
override func speak() {
print("{this.name} barks!\n")
}
}
Class instances are destroyed automatically when they go out of scope:
- Stack-allocated: destructor called at end of scope
- Heap-allocated: destructor called, then memory freed
- Array of classes: each element destroyed in reverse order
- Inheritance chain: destructors called from most-derived to base
| Feature | Struct | Class |
|---|---|---|
| Memory | Stack by default | Stack or heap |
| Copy semantics | Value copy (memcpy) | Reference (pointer) |
| Destructor | Not supported | Automatic |
| Inheritance | Not supported | Supported |
| Methods | Supported | Supported |
| Virtual dispatch | Static only | Static + virtual |
| When to use | Small data, no cleanup needed | Complex objects with lifecycle |
Traits define interfaces that types can implement. Dispatch is static (monomorphized) by default, with dyn for dynamic dispatch.
trait Drawable {
func draw()
}
struct Circle {
x: i32, y: i32, radius: u32
}
impl Drawable for Circle {
func draw() {
print("Circle at ({x}, {y}) r={radius}\n")
}
}
func render(item: dyn Drawable) {
item.draw() // dynamic dispatch via vtable
}
Any function can contain raw NASM assembly blocks using the asm keyword:
func write_port(port: u16, value: u8) {
asm {
mov dx, [port]
mov al, [value]
out dx, al
}
}
Variables can be mapped to specific registers:
func cpuid() -> (u32, u32, u32, u32) {
let eax: u32, ebx: u32, ecx: u32, edx: u32
asm {
mov eax, 1
cpuid
mov [eax], eax
mov [ebx], ebx
mov [ecx], ecx
mov [edx], edx
}
return (eax, ebx, ecx, edx)
}
NASM labels inside asm blocks are resolved by the assembler. Aether labels can be referenced from asm blocks using the @ prefix:
func example() {
asm {
jmp @skip
; ... dead code ...
@skip:
nop
}
}
For boot sectors and low-level code, raw bytes can be emitted:
asm {
db 0x55, 0xAA ; boot signature
times 510-($-$$) db 0
}
A subset of the language can be evaluated at compile time:
const MAX_SIZE = 4096
const TABLE_SIZE = MAX_SIZE / 16
const TABLE: [TABLE_SIZE]u32 = compute_table() // computed at compile time
func compute_table() -> [TABLE_SIZE]u32 {
let result: [TABLE_SIZE]u32
for i in 0..TABLE_SIZE {
result[i] = i * i
}
return result
}
@target // returns current target: "host", "kernel", "boot", "module", "binary"
@arch // returns current architecture: "x86_64", "arm64", "riscv64"
@endian // returns "little" or "big"
@sizeof(T) // returns size of type T at compile time
@alignof(T) // returns alignment of type T
# if @target == "kernel"
// kernel-specific code
# elif @target == "host"
// host-specific code
# else
// fallback
# end
| Target | Output | Entry | Use Case |
|---|---|---|---|
host |
Mach-O (macOS) or ELF (Linux) | _aether_entry |
Testing on host OS |
kernel |
ELF64 | _start |
Aether OS kernel |
boot |
Flat binary (512B) | 0x7C00 |
Boot sector |
binary |
ELF64 at 0x2000000 | _start |
Aether OS /bin/ executables |
module |
ELF64 relocatable | mod_init |
Aether OS kernel modules |
universal |
Multi-arch binary | Dispatcher | Cross-platform executables |
Target-specific code can be annotated:
@target("kernel")
func kernel_init() {
// only compiled for kernel target
}
@target("host")
func host_init() {
// only compiled for host target
}
@entry(0x7C00) // Set entry point address
@layout(512) // Flat binary with max size
@org(0x2000000) // Origin address for code
@section(".text") // Place following code in specific section
| Function | Description |
|---|---|
print(str) |
Print string to stdout/serial |
print_i64(i64) |
Print signed integer |
print_u64(u64) |
Print unsigned integer |
print_hex(u64) |
Print hex |
assert(bool) |
Runtime assertion |
panic(str) |
Fatal error with message |
alloc(T) |
Heap-allocate a value of type T |
alloc_array(T, n) |
Heap-allocate array of n elements |
len(slice) |
Length of slice or array |
copy(dst, src) |
Memory copy |
zero(ptr, len) |
Zero memory |
sizeof(T) |
Size of type (compile-time) |
| Function | Description |
|---|---|
syscall(n, ...) |
Call Aether OS syscall at 0x5000 |
writePortByte(port, value) |
Write byte to I/O port |
readPortByte(port) -> u8 |
Read byte from I/O port |
disableInterrupts() |
Disable interrupts |
enableInterrupts() |
Enable interrupts |
haltCpu() |
Halt CPU |
Source (.ae)
→ Tokenizer (character stream → tokens)
→ Lexer (tokens → token stream with whitespace handling)
→ Parser (token stream → AST)
→ Semantic Analyzer (type checking, name resolution, lifetime analysis)
→ Optimizer (constant folding, dead code elimination, inlining)
→ Code Generator (AST → NASM assembly)
→ Assembler (NASM → object file)
→ Linker (object file → executable)
- AST: Full syntax tree with source locations
- ASM IR: Internal representation of assembly instructions (for multi-arch backends)
- NASM: Text output passed to NASM assembler
| Backend | Architecture | Status |
|---|---|---|
| x86_64 NASM | x86_64 | Primary |
| ARM64 ASM IR | ARM64 | In progress |
| RISC-V ASM IR | RISC-V | In progress |
Every function gets a per-function bump arena for temporary allocations. The compiler emits code to initialize the arena on function entry and reset it on exit. This makes short-lived allocations (string concatenation, temporary buffers) essentially free.
Strings are {ptr: u64, len: u64} views. The + operator concatenates strings when either operand is a string. String interpolation is desugared at compile time. No null-termination required.
match value {
0 => print("zero\n")
1..10 => print("small\n")
10..100 => print("medium\n")
_ => print("large\n")
}
let name = user?.profile?.name ?? "Anonymous"
if let val = optional_value {
print("got {val}\n")
}
func divide(a: i32, b: i32) -> i32
require b != 0
ensure result * b == a
{
return a / b
}
Contracts are checked at runtime in debug builds, stripped in release builds.
let add = func(a: i32, b: i32) -> i32 { return a + b }
let result = add(3, 4) // 7
Closures can capture variables from the enclosing scope. The compiler allocates a closure struct on the stack and passes it as a hidden argument.
struct Vector3 {
x: f32, y: f32, z: f32
}
impl Vector3 {
func op_add(other: Vector3) -> Vector3 {
return Vector3{x + other.x, y + other.y, z + other.z}
}
}
let a = Vector3{1.0, 2.0, 3.0}
let b = Vector3{4.0, 5.0, 6.0}
let c = a + b // calls op_add
func max[T](a: T, b: T) -> T {
if a > b { return a }
return b
}
let m = max[i32](3, 7) // explicit
let n = max(3.0, 7.0) // inferred
Generics are monomorphized at compile time — each concrete instantiation generates separate code.
Functions marked comptime are executed at compile time. Their results are embedded as constants in the binary.
comptime func generate_lookup_table() -> [256]u32 {
let table: [256]u32
for i in 0..256 {
table[i] = i * i
}
return table
}
const LOOKUP = generate_lookup_table()
// math.ae
pub func square(x: i32) -> i32 {
return x * x
}
// main.ae
import "math.ae"
func main() {
print_i64(square(7)) // 49
}
The compiler can produce a single binary containing code for multiple architectures. The entry point is a thin dispatcher that detects the current architecture and jumps to the correct code section. This enables distributing a single binary that runs on x86_64, ARM64, and RISC-V without recompilation.
- Compile Aether source to NASM assembly
- Assemble with NASM to object files
- Link with LD to executables
- Support all targets: host, kernel, boot, binary, module, universal
- Automatic bump allocator per function
- Automatic destructor chains for classes
- Region-based allocation
-
deferstatement -
try/catch/throwwith stack unwinding - Inline NASM assembly blocks
- Classes with inheritance, constructors, destructors
- Structs and enums
- Traits with static and dynamic dispatch
- Generics with monomorphization
- String interpolation
- Pattern matching
- Optional types with
?T,??,?.,! - If-let
- Closures
- Operator overloading
- Compile-time evaluation
- Conditional compilation
- Module/import system
- Contracts (require/ensure)
- Universal binary output
- Freestanding output (no libc dependency)
- Segfault handling in try blocks (host target)
- Compile-time reflection (
@target,@arch, etc.) - Cross-module inlining
- Basic optimizer (constant folding, dead code elimination)
- Debug information output
- Error messages with source locations
- Multi-file compilation
- Linker script generation
- IDE integration (LSP)
- Formatter
- Documentation generator
- Package manager
- Test framework
- Profiling support
- Address sanitizer (compile-time instrumentation)
The Aether language is the primary development tool for Aether OS. The compiler must produce code that conforms to the OS ABI:
- Kernel: ELF64, entry
_start, linked at 0x1000000, no libc, no red zone, no SSE - Binaries: ELF64, entry
_start, loaded at 0x2000000, syscall page at 0x5000 - Modules: ELF64 relocatable, entry
mod_init, module registry at 0x4000 - Boot: Flat binary, entry 0x7C00, max 512 bytes (or configurable)
The compiler must also support host-native compilation for testing, producing Mach-O (macOS) or ELF (Linux) executables that link against the segfault helper for exception handling.
- No garbage collector
- No interpreter or REPL (beyond compile-and-run)
- No JIT compilation
- No runtime type information (RTTI)
- No reflection at runtime
- No dynamic code loading (modules are loaded by the OS, not the language)
- No FFI to C (freestanding target has no C runtime)
- No async/await (cooperative multitasking is handled by the OS)
- No garbage-collected memory model