Getting Started with Aether
This guide covers installation and basic usage of the Aether programming language.
Installation
Quick Install (recommended)
git clone https://github.com/nicolasmd87/aether.git
cd aether
./install.sh
This builds the compiler, CLI tool, and standard library, then installs everything to ~/.aether. After restarting your terminal (or running source ~/.bashrc, ~/.zshrc, or ~/.bash_profile), the ae command is available globally.
To install to a custom location:
./install.sh /usr/local # System-wide install (needs sudo)
Prerequisites
The installer checks for these automatically and provides platform-specific install commands if something is missing. Here's what you need:
macOS:
xcode-select --install
# Or with Homebrew: brew install gcc make
Linux (Debian/Ubuntu/Pop!_OS/Mint):
sudo apt-get install build-essential
Linux (Fedora):
sudo dnf install gcc make
Linux (RHEL/CentOS/Rocky/AlmaLinux):
sudo yum install gcc make
Linux (Arch/Manjaro):
sudo pacman -S base-devel
Linux (openSUSE):
sudo zypper install gcc make
Linux (Alpine):
apk add build-base
Windows:
The easiest way is to download the pre-built release binary, no MSYS2, no manual toolchain required:
- Download
aether-*-windows-x86_64.zipfrom GitHub Releases - Extract to any folder, e.g.
C:\aether - Add
C:\aether\binto your PATH (System Settings → Environment Variables → Path) - Restart your terminal (so PATH takes effect)
- Open any terminal (PowerShell or CMD):
ae version
ae init hello
cd hello
ae run
GCC is downloaded automatically on the first ae run (~80 MB, one-time). No MSYS2, no separate installer needed.
Windows Defender tip: The first build may trigger a scan. Adding C:\aether to Windows Security exclusions speeds things up.
Building from source / contributors: Install MSYS2, open "MSYS2 MinGW 64-bit", thenpacman -S mingw-w64-x86_64-gcc make gitandmake ae.
Development Build (without installing)
If you prefer to build without installing to your system:
make ae
./build/ae version
This builds the ae CLI tool in the build/ directory. You'll need to use ./build/ae instead of just ae.
Editor Setup
For syntax highlighting and a better development experience:
VS Code / Cursor:
cd editor/vscode
./install.sh
Features included:
- Syntax highlighting with TextMate grammar
- Custom "Aether Erlang" dark theme optimized for Aether code
.aefile icons- Basic language configuration (comments, brackets, etc.)
After installation, open any .ae file and VS Code will automatically apply syntax highlighting. Select the "Aether Erlang" theme via Preferences > Color Theme for the best experience.
Your First Aether Program
Option A: Create a project (recommended)
ae init myproject
cd myproject
ae run
This creates a project with aether.toml, src/main.ae, and tests/.
Option B: Run a single file
Create hello.ae:
main() {
println("Hello, Aether!")
}
ae run hello.ae
Build an executable:
ae build hello.ae -o hello
./hello
Build for WebAssembly:
ae build --target wasm hello.ae
node hello.js
Requires Emscripten (emcc on PATH).
Type-check without compiling:
ae check hello.ae
Add a package (any git host):
ae add github.com/user/repo # latest from GitHub
ae add github.com/user/repo@v1.0.0 # specific version
ae add gitlab.com/user/repo # GitLab
ae add codeberg.org/user/repo # Codeberg
Error Handling
Functions that can fail return (value, error) tuples. Check the error first, handle it, then continue, no else needed:
safe_divide(a: int, b: int) -> {
if b == 0 {
return 0, "division by zero"
}
return a / b, ""
}
main() {
result, err = safe_divide(10, 3)
if err != "" {
println("Error: ${err}")
exit(1)
}
println("Result: ${result}")
// Discard error with _
val, _ = safe_divide(42, 7)
println(val)
}
Function Contracts (requires / ensures)
Aether supports Eiffel-style runtime contracts, pre/postconditions you attach directly to a function declaration. They document intent at the boundary, fire as aether_panic on violation (which prints the failed predicate by name), and elide entirely when the predicate is provably constant-true at compile time.
add(a: int, b: int) -> int
requires a >= 0
requires b >= 0
ensures result >= a
ensures result >= b
{
return a + b
}
safe_div(a: int, b: int) -> int
requires b != 0
{
return a / b
}
requires <expr>runs at function entry, with parameters in scope.ensures <expr>runs before eachreturn, withresultbound to the value about to be returned.- Multiple clauses, freely interleaved, each is checked independently so the panic message names the specific failed predicate.
aetherc --no-contracts(analog of-DNDEBUG) drops every check at codegen for release builds.
See Function contracts for the full semantics, the const-fold elision rules, and v1 limitations.
Interactive REPL
Experiment with Aether interactively:
ae repl
┌───────────────────────────┐
│ Aether <version> REPL │
│ :help for commands │
└───────────────────────────┘
ae> x = 5
ae> y = 3
ae> println(x + y)
8
ae> :show
x = 5
y = 3
ae> :quit
Assignments and constants persist across evaluations. Multi-line blocks (if/while/for) auto-continue until braces close. Use :reset to clear the session, :show to see accumulated code.
Actor-Based Programming
Aether is built around the actor model. Here is a simple counter example:
message Ping {}
actor Counter {
state count = 0
receive {
Ping() -> {
count = count + 1
}
}
}
main() {
c = spawn(Counter())
c ! Ping {}
c ! Ping {}
c ! Ping {}
// Wait for all messages to be processed
wait_for_idle()
println("Count: ${c.count}")
}
Actors are lightweight concurrent entities that communicate through asynchronous messages. Each actor has private state and a mailbox for incoming messages. Messages are defined with the message keyword and sent with the ! operator. The runtime distributes actors across available CPU cores automatically.
Use wait_for_idle() to block until all actors have finished processing their messages. This is essential when you need to read actor state or coordinate completion.
Module System
Import modules using the import statement:
import std.map
main() {
mymap = map.new()
defer map.free(mymap)
map.put(mymap, "greeting", "hello")
println("Map created")
}
Functions are called using namespace-style syntax: namespace.function(). Stdlib functions that can fail return Go-style (value, err) tuples, check err first, then use value:
body, err = http.get("http://example.com")
if err != "" { println("failed: ${err}"); return }
println(body)
| Import | Namespace | Example |
|---|---|---|
import std.string | string | string.new("hello"), n, err = string.to_int("42") |
import std.file | file | content, err = file.read("f"), file.exists("f") |
import std.dir | dir | err = dir.create("d"), dir.exists("d") |
import std.path | path | path.join("a", "b"), path.basename("a/b") |
import std.list | list | list.new(), err = list.add(l, item) |
import std.map | map | map.new(), err = map.put(m, k, v) |
import std.json | json | v, err = json.parse(str), json.create_object() |
import std.math | math | math.abs_int(x), math.sqrt(x) |
import std.http | http | body, err = http.get(url), http.server_create(port) |
import std.tcp | tcp | sock, err = tcp.connect(host, port), tcp.write(sock, data) |
import std.log | log | err = log.init("app.log", 0), log.write(0, msg) |
import std.io | io | content, err = io.read_file("f"), io.getenv("HOME") |
import std.os | os | out, err = os.exec("ls"), os.system("cmd") |
Raw externs are preserved under a _raw suffix (e.g. http.get_raw) for advanced callers who need direct access to the underlying ptr or status code.
Creating Your Own Modules
You can write reusable modules in pure Aether, no C required. Place your module in lib/<name>/module.ae:
lib/mymath/module.ae:
export const PI = 3
export double_it(x) {
return multiply(x, 2)
}
export add(a, b) {
return a + b
}
// Private helper, not accessible from outside
multiply(a, b) {
return a * b
}
src/main.ae:
import mymath
main() {
println(mymath.double_it(5)) // 10
println(mymath.add(3, 4)) // 7
println(mymath.PI) // 3
// mymath.multiply(2, 3) // Error: not exported
}
Use export to control which functions and constants are part of your module's public API. Non-exported symbols are private, they can be used internally by exported functions but are not accessible to importers. If a module has no export declarations, all symbols are public (backwards compatible).
Modules support functions, constants, intra-module calls (functions calling other functions in the same module), and export visibility. See Module System Design for full details.
Standard Library
Aether includes a standard library with the following modules:
| Module | Description |
|---|---|
std.string | String operations |
std.file | File operations |
std.dir | Directory operations |
std.path | Path utilities |
std.list | Dynamic array (ArrayList) |
std.map | Hash map (HashMap) |
std.json | JSON parsing and creation |
std.http | HTTP client and server |
std.tcp | TCP sockets |
std.log | Structured logging |
std.math | Math functions |
std.io | Console I/O, environment variables |
std.os | Shell execution, command output capture |
See stdlib-api.md for the full API reference.
When you read a file or get data from stdlib functions, the result is a regular string, you canprint()it, use it in"${interpolation}", or pass it in messages directly. No conversion needed.
Error Handling and Memory
Stdlib functions that can fail return a Go-style (value, err) tuple. Check the error string first, then use the value. The wrappers auto-free any heap allocations, so you don't need defer free() for stdlib returns:
import std.io
main() {
// io.getenv is infallible; returns null if unset
home = io.getenv("HOME")
if home != 0 {
println(home)
}
// io.read_file is Go-style
content, err = io.read_file("data.txt")
if err != "" {
println("cannot read: ${err}")
return
}
println(content)
}
free() is still a language builtin for manually-allocated memory (e.g. make, list.new, map.new). defer free(...) or defer list.free(...) ensure cleanup on scope exit.
For std.string managed strings (string.new(), string.to_upper(), etc.), use defer string.release(s) for reference counting.
Pattern Matching
Aether has Erlang-inspired pattern matching.
Function Pattern Matching
Define functions with multiple clauses that match on argument values:
// Match on literal values
factorial(0) -> 1
factorial(n) when n > 0 -> n * factorial(n - 1)
// Fibonacci with multiple base cases
fib(0) -> 0
fib(1) -> 1
fib(n) when n > 1 -> fib(n - 1) + fib(n - 2)
// Guards for conditional matching
classify(x) when x < 0 -> "negative"
classify(x) when x == 0 -> "zero"
classify(x) when x > 0 -> "positive"
// Multi-parameter pattern matching
gcd(a, 0) -> a
gcd(a, b) when b > 0 -> gcd(b, a - (a / b) * b)
This style replaces verbose if/else chains with declarative, readable code.
Match Statements
Use match for value dispatch:
match (value) {
0 -> { println("Zero") }
1 -> { println("One") }
_ -> { println("Other") }
}
List Patterns
Match on arrays (requires corresponding _len variable):
nums = [1, 2, 3]
nums_len = 3
match (nums) {
[] -> { println("empty") }
[x] -> { println("one element") }
[h|t] -> {
println("head: ${h}")
}
}
Project Configuration
Projects use aether.toml for configuration. Created automatically by ae init:
[package]
name = "myproject"
version = "0.1.0"
[[bin]]
name = "myproject"
path = "src/main.ae"
[dependencies]
[build]
target = "native"
# link_flags = "-lsqlite3 -lcurl" # Link external C libraries
Linking C Libraries
To link external C libraries, add link_flags to the [build] section:
[build]
link_flags = "-lsqlite3 -lcurl -lssl"
This allows Aether programs to use libraries like SQLite, libcurl, OpenSSL, etc.
Command-Line Arguments
Access command-line arguments using the runtime's argument functions:
extern aether_args_count() -> int
extern aether_args_get(index: int) -> string
main() {
count = aether_args_count()
for (i = 0; i < count; i = i + 1) {
println(aether_args_get(i))
}
}
To pass arguments, build and run the binary directly:
ae build myprogram.ae -o myprogram
./myprogram arg1 arg2
Environment Variables
Read configuration from environment variables using std.os:
import std.os
main() {
home = os.getenv("HOME")
defer free(home)
if home != 0 {
println("Home directory: ${home}")
}
}
The builtin getenv() also works without an import for quick scripts (returns a malloc'd string, use defer free()).
When something doesn't compile
Aether's typer is precise, error[E0301]: Undefined function 'super_token' is the right output for a build pipeline but not always for an operator authoring a config script. ae help <script.ae> translates the typer's terse output into actionable, on-machine suggestions:
ae help my_script.ae # Human-readable findings
ae help my_script.ae --json # Machine-readable (CI integration)
ae help my_script.ae --fix # Apply safe rewrites after diff confirmation
Heuristics cover Levenshtein-matched name suggestions, YAML/HCL-shape detection inside closure-DSL blocks (port: 9990 → port(9990)), missing-import detection against the stdlib catalog, type-mismatch English ("Drop the quotes"), and library-author-shipped *.help.md hints. Hard privacy contract: no network calls, no file reads outside the script + its imports + co-located hints, no execution of the script. See Config-IS-Code Diagnostics and examples/ae-help-demo/ for a worked example.
Layering modules across multiple directories
PATH-style --lib chains let you layer project-local module overrides on top of vendored or shared roots:
# Try a local patch in lib/foo/ first, fall back to vendor/foo/:
ae run main.ae --lib lib --lib vendor
# Same as a separator-string (':' POSIX, ';' Windows):
ae run main.ae --lib "lib:vendor"
# Or via env var:
AETHER_LIB_DIR="lib:vendor" ae run main.ae
# See what the toolchain will actually search, in order:
ae lib-path --lib "lib:vendor"
Left-most entry wins on a name collision; each import walks the chain independently. See examples/packages/lib-path-layering/ for a runnable demo and Module System for the design.
Next Steps
- Read the Tutorial for a guided introduction
- Learn about C Interoperability to use C libraries
- See C Embedding Guide to embed Aether actors in C applications
- Explore Standard Library Documentation
- See Architecture for compiler and runtime internals
- See Runtime Optimizations for performance details
- Use
ae upgradeto get the latest release, orae version listto see and switch between releases
Version Management
Install, upgrade, and switch between Aether releases without reinstalling:
ae upgrade # Install the latest release and switch to it
ae install v0.25.0 # Install a specific release (latest if omitted)
ae use v0.25.0 # Switch to an installed version
ae version # Show current version
ae version list # List all available releases (marks installed/active)
ae install / ae use are the short forms of ae version install / ae version use, which still work. ae upgrade (alias ae update) is a no-op when you are already on the latest release.
Versions are stored in ~/.aether/versions/. The active version is symlinked to ~/.aether/current (Linux/macOS) or copied to ~/.aether/bin/ (Windows).
Troubleshooting
Build Failures
"Aether compiler not found" (Windows)
- Make sure you restarted your terminal after adding
C:\aether\binto PATH - Verify:
where aeshould showC:\aether\bin\ae.exe - Verify:
where aethercshould showC:\aether\bin\aetherc.exe - If both exist but it still fails, set
AETHER_HOME:set AETHER_HOME=C:\aether
"gcc: command not found" (Windows)
- This should not happen with the pre-built binary,
aeauto-downloads GCC (~80 MB) on first run - If you built from source via MSYS2, open the "MSYS2 MinGW 64-bit" shell, not plain PowerShell
- Verify:
gcc --versionshould show MinGW-w64 GCC
"pthread.h: No such file or directory"
- Linux:
sudo apt-get install libpthread-stubs0-dev - Windows: The Aether runtime uses Win32 threads natively, no pthread library needed
Test failures
- Run
make testfor unit tests,make test-aefor integration tests - Check for port conflicts if network tests fail (port 8080)
Quick tips when you hit trouble
- Rebuild from scratch if anything looks stale:
make clean && make. - On Windows, launch
aefrom a directory where your user can write (the first run downloads GCC into~\.aether\). - Inside an actor body,
stateis a reserved keyword, pick a different local-variable name; outside actor bodies it has no special meaning.
WebAssembly and Embedded Targets
Aether supports cross-compilation to platforms without pthreads or POSIX APIs via the PLATFORM Makefile variable:
# Build for WebAssembly (requires Emscripten SDK)
make stdlib PLATFORM=wasm
# Or force cooperative mode on native for testing:
make stdlib EXTRA_CFLAGS="-DAETHER_NO_THREADING"
# Build for embedded ARM (syntax-check only, requires arm-none-eabi-gcc)
make ci-embedded
# Docker-based cross-compilation (no local toolchain needed):
make docker-ci-wasm # Emscripten + Node.js execution
make docker-ci-embedded # ARM Cortex-M4 syntax-check
On threadless platforms, the cooperative scheduler (aether_scheduler_coop.c) replaces the multi-core scheduler. All actors run on a single thread via aether_scheduler_poll(). Multi-actor programs work correctly, messages are processed cooperatively during wait_for_idle().
Stdlib modules that depend on filesystem or networking return errors gracefully (NULL, 0, -1). Console I/O (print, println) always works.
Platform-Specific Notes
macOS:
- May need
xcode-select --installfor command line tools - Homebrew GCC recommended:
brew install gcc - Apple Silicon (M1/M2/M3): Runtime auto-detects P-cores for consistent performance
- Thread affinity is advisory on macOS; occasional benchmark variance is normal
Linux:
- Kernel 4.14+ recommended for full NUMA support
- AddressSanitizer may require
gcc-multilibon some distributions - Full thread affinity support for deterministic performance
Windows:
- Pre-built binaries work in any terminal (PowerShell, CMD, Windows Terminal)
- GCC is auto-downloaded on first
ae runno MSYS2 or manual setup required - The runtime uses Win32 threads natively, no pthreads library required
- Full thread affinity support via
SetThreadAffinityMask - Building from source: Use MSYS2 MinGW 64-bit shell with
make ae