Overview
Aela is a commercial-source language for embedded systems.
Aela is a compiled, memory-safe language with a strong, static type system. It started in 2018 as a fork of Rust, but has diverged significantly to service embedded systems use cases. It has a similar syntax and sticks to many of the same goals such as ownership semantics, memory and concurrency safety, only pay for what you use, and zero-cost abstractions.
Programming Language Landscape
This is where Aela fits In terms of Simplicity vs Complexity, and Familiarity vs Uniqueness.
Langauge Goals
All language design decisions revolve around determinism and performance for embedded systems.
Aela also focuses on harmony with C and C++. It offers automatic binding through header inclusion, packed and ordered structs, and inversion of control for system calls like
write(2)
.
Language Goals
All language design decisions revolve around providing absolute determinism, safety, and performance for embedded systems. Anything that compromises these goals through ambiguity, hidden costs, or non-local behavior is eliminated. All excluded features have an ideal alternative (see documentation) or have been simply omitted.
| Excluded Feature | Justification |
|---|---|
| Panics | Panics are not in the type signature; they violate the principle of abstraction and create a hidden, unrecoverable failure path that bypasses the type system. |
| Exceptions | Exceptions create hidden, non-local control flow paths that are impossible to reason about statically and incur runtime overhead. Error handling must be explicit. |
| Global State | Global variables create hidden dependencies and introduce non-determinism, making concurrency unsafe and program behavior impossible to reason about locally. |
| Lifetime Annotations | Explicit lifetime annotations are a significant source of complexity and syntactic noise. The compiler must be smart enough to infer ownership duration without burdening the programmer. |
| Macros | Macros create non-linear, hard-to-debug code flow ("magic"). Code must be explicit and analyzable; what you see is what the compiler gets, ensuring predictability and toolability. |
unsafe
keyword
|
The
unsafe
keyword is an escape hatch that undermines the entire memory safety guarantee of the language. Aela issues a handful of unsafe operations rather than entire blocks.
|
| Attributes | Attributes are a form of meta-programming that injects non-local behavior and hidden complexity. They modify code in non-obvious ways, violating the principle that code should be explicit. |
| Pointer Arithmetic | Direct pointer manipulation is a primary source of memory safety vulnerabilities (e.g., buffer overflows). Safe, bounds-checked slice operations are the only acceptable alternative. |
| Advanced Traits | Complex trait systems lead to baroque, unreadable type signatures and cryptic compiler errors. Simplicity and clarity are prioritized over abstract expressive power. |
| Inline Assembly | Inline assembly is non-portable, opaque to the optimizer, and breaks static analysis. Low-level routines must be provided via a stable C ABI, ensuring a clean contract. |
null
keyword
|
Null pointers are the source of countless runtime crashes. The absence of a value must be explicitly represented and handled in the type system (e.g., via
Option
).
|
| Operator Overloading | Operator overloading makes code ambiguous and hides the cost of operations. Function calls must be explicit to ensure code clarity and predictable performance. |
| Implicit Conversions | Implicit conversions hide potential bugs, such as data truncation or precision loss. All type conversions must be explicit, forcing the programmer to acknowledge the operation. |
Feature Comparison
| Feature | Aela | C | C++ | Rust | Ada |
|---|---|---|---|---|---|
| Compile-Time Memory Safety | √ | √ | √ | ||
| Cold Build Speed | √ | √ | √ | ||
| Reference Counted | √ | √ | |||
| Automatic RAII | √ | √ | |||
| Formal Verification | √ | √ | |||
| Dependent & Refinement Types | √ | ||||
| Language-Native Concurrency | √ | ||||
| Structured Concurrency | √ | √ | |||
| Automatic C/C++ safe binding | √ |
Module and Package System
Aela's module system is designed for organizing code into reusable and maintainable packages. The entire system is controlled by a manifest file named
index.json
.
Packages are precompiled binaries, this makes them fast, but they can also ship with the source code and that won't make them any slower. See the GUI module for a comprehensive example of a multi-platform module.
Package Manifest
Every Aela project is a package, and every package is defined by an
index.json
file at its root. This file tells the compiler everything it needs to know about your project.
Key Fields
- "name": The official name of your package (e.g., "my-app").
- "version": The version number (e.g., "0.1.0").
- "entry": The relative path to the main source file that acts as
- the entry point for compilation (e.g., "src/main.ae").
Example
Managing Dependencies
You can include other Aela packages in your project by listing them in the "dependencies" section of your
index.json
.
- The key is the name you will use to import the module (e.g., "ui").
-
The value is the relative path from your
index.jsonto the - dependency's root directory.
Example
Exports
By default, all functions, structs, and variables defined in a file are private to that file. To make a symbol visible to other modules, you must explicitly mark it with the
export
keyword.
Example
Re-exports
Imports
You use the
import
statement to bring exported symbols from other modules into the current file. Aela supports two forms of imports.
Namespace Import
This is the most common form. It imports all exported symbols from a module under a single namespace alias.
Syntax
Example
Named Imports
This allows you to import specific functions or types directly into the current scope, without needing a namespace qualifier.
Syntax
Example EXAMPLE
Advanced Build Configuration (FFI)
The
index.json
manifest also allows you to control the native build process, which is essential for linking against C, C++, or Objective-C code when using the Foreign Function Interface (FFI).
Key Sections
- "sources": A list of native source files (.c, .mm, etc.) to be
- compiled alongside your Aela code.
- "link": A list of flags to pass to the system linker (e.g., clang).
Notes
- Both "sources" and "link" flags can be specified per-platform
- (e.g., "darwin", "linux", "windows") or as "shared" for all platforms.
- The compiler automatically includes the linker flags from all packages
- listed in your "dependencies" section.
Example
This configuration tells the Aela compiler:
-
On macOS, compile the
src/native/apple.mmObjective-C++ file. - When linking the final executable, add flags to link against the
- Foundation, AppKit, and ObjectiveC system frameworks.
Failures
A primary cause of software defects, security vulnerabilities, and developer anxiety is the "Trust Gap": the difference between what a function's signature claims it does and what its implementation can actually do. Aela is designed to eliminate this gap. Its error and fault handling system is built on a single, non-negotiable principle: a function's signature must be a complete and honest contract, and the compiler must enforce it.
Don't Try-Catch
Traditional exception systems, common in languages like Java, JavaScript, C++, and Python, introduce a form of hidden control flow. A throw or raise statement is a non-local goto that is often invisible in the function's signature.
Consider a typical function in such a language:
function get_user(id: int) -> User
This signature makes a simple promise: "Give me an integer, and I will give you a User." However, the implementation might throw a DatabaseConnectionException, a UserNotFoundException, or a NullPointerException. To understand the function's true behavior, the developer must embark on a research project: reading the documentation (which may be out of date), reading the source code, and reading the source of every function it calls.
This breaks a developer's ability to reason locally about the code.
Don't Panic
Some modern languages, notably Rust, attempt to solve error handling this by creating a two-tiered system:
-
Recoverable Errors (
Result): For expected failures (e.g., file not found). These are part of the type signature. -
Unrecoverable Bugs (
panic!): For programmer errors (e.g., index out of bounds). These are not part of the type signature.
While an improvement, this still creates a hidden side-channel for bugs. More critically, the panic! mechanism creates a deep schism between platforms, especially for embedded systems.
The unwind vs. abort Schism:
On servers, panic! defaults to unwind, a slow and complex process that runs cleanup code (destructors). This adds a significant "tax" to the binary size, which is unacceptable on resource-constrained devices.
On embedded systems, developers are forced to configure panics to abort, which immediately halts the program.
The Broken Promise of abort: When panic = "abort" is used, the language's core safety promise—that resources will be cleaned up on failure (RAII via Drop in Rust)—is broken. Destructors are never called. A MutexGuard will leave a mutex permanently locked. A peripheral that was supposed to be disabled is left in an active state. The very code written to ensure safety on failure becomes useless.
This patched-together model is not a first-principles solution. Aela requires a single, unified system that is safe, deterministic, and efficient on all platforms.
The Fault Model
The Principle of the Honest Contract: Aela avoids ambiguity in failure handling by enforcing that the outcome of a function must be encoded in its signature.
Function Signatures
A function declares its potential to terminate due to an unrecoverable logic bug by using the
|
operator in its return type. This operator separates the single success type from a list of one or more
fault
types.
fn get_at(slice: &[u8], index: int) -> u8 | OutOfBounds;
This signature is an honest contract. It tells any caller: "This function will either return a
u8
, or it will terminate with an
OutOfBounds
fault. There are no other possibilities."
The
fault
and
raise
Keywords
-
`fault`
: A keyword used to declare a type that represents a logic bug or contract violation. This distinguishes it from
structorenum, which represent data and recoverable errors. -
fault OutOfBounds(index: int, len: int); -
`raise`
: A keyword that triggers a fault. It immediately stops the current function's execution and propagates the fault to the caller.
raiseis considered a terminal action. -
raise OutOfBounds(index: index, len: len);
The Standard
match
Statement
The
match
statement is how Aela handles the outcome of functions that may terminate with faults.
Semantic Rules
The safety and special behavior of fault handling are not derived from the syntax, but from a single, powerful semantic rule in the compiler:
If the expression being evaluated by a
matchstatement has a fault path in its signature (i.e., contains a|), then the compiler enforces a "Terminal Arm Rule" on any arm that matches afault-declared type.
This rule is context-dependent. It is triggered by the signature of the function being called, not just the type of the pattern. This is the key insight that resolves all ambiguity.
The "Terminal Arm Rule" is defined as: The block of the arm must end with a terminal statement. Aela does not have implicit returns, so this is a direct check of the final statement in the block.
Terminal statements are:
-
raise; -
std::abort(); -
std::halt(); -
std::reboot();
Note
purefn ketword must not introduce or handle faults; they cannot have a | in their return type and cannot raise.
Note
Faults must not cross FFI; board/FFI shims must convert faults to domain-appropriate codes or terminals.
Note
AP131outlines a proposal to havereturnbe the equivalent ofraise. Allowing both and specifying that they’re identical.
Note
AP132outlines a proposal to permit functions to be generic over an open set of faults (variadic type param), so adapters don’t re-enumerate: ie: fn map(f: fn(T) -> U | E..., x: T) -> U | E...;
Note
AP134outlines a proposal to add a desugaring operator. let b = get_at(xs, 0)?; // expands to match/raise
Complete Examples: The Rule in Practice
These two examples demonstrate how the context-dependent rule creates a safe and unambiguous system.
Example 1: Handling a Live Fault Event
If
reboot;
were omitted, the compiler would issue a clear error:
"Fault-handling arm must end with a terminal statement."
Example 2: Inspecting a Fault as Data
Memory & Mutability
1. Default Allocation Semantics
- All values start on the stack by default.
- Stack allocations are:
- Fast
- Scoped
- Owned directly by the binding.
2. Heap Allocation
To allocate a value on the heap, use the
new
keyword with one of the following modifiers:
Syntax
All heap allocatations are reference counted.
Behavior of
new
Modifiers
| Modifier | Behavior | Description |
|---|---|---|
(none)
|
Heap Allocation (OS based allocator). | Returns a handle/reference to a freshly allocated object, immutable and can not be loaned. |
shared
|
Heap. Shared ownership, single-threaded. | Allowed to be loaned; not thread-safe; not Send/Sync-like. |
shared atomic
|
Heap. Shared ownership, thread-safe. | Reference counts and interior coordination use atomics; Send/Sync if T satisfies the “atomic-safe” requirements you define. |
weak
|
Heap, weak handle to a shared object. | Does not keep the allocation alive; must be paired with at least one strong shared/shared atomic owner somewhere to be useful. |
static
|
Static storage, no OS allocator. | Object is placed/constructed in static memory (BSS/RODATA/flash/SECTION), suitable for bare-metal/MCUs. Lifetime: program-long (or until explicitly torn down by system semantics). |
static atomic
|
Static storage, thread-safe coordination via atomics. | Shared global counters updated in ISR + mainline, Lock-free buffers (ring buffers for UART, DMA, etc.). Configuration/state variables read by multiple cores or ISRs. On a simple single-core MCU with no interrupts, it’s redundant but harmless. |
-
new shared: -
Performs a
copy
of the stack value (
{ ...val }) - Allocates it on the heap
- Wraps it in a reference-counted container (non-atomic)
-
Returns a reference:
&T
-
new shared atomic: - Same as above, but uses atomic reference counting (thread-safe)
3. Mutability Semantics
Mutability is determined only by the binding keyword :
| Keyword | Meaning |
|---|---|
let
|
Immutable binding (read-only) |
var
|
Mutable binding (read-write) |
-
The
mutkeyword is not necesasry here. - Mutability is not encoded in types, structs, or fields.
-
letandvarare both lexically scoped.
Example
Note
Attempting to mutate a
let-bound reference results in a compile-time error!
4. Weak References & Cycle Prevention
To solve the problem of reference cycles (e.g., a parent refers to a child, and the child refers back to the parent), Aela will provide weak references. A weak reference is a non-owning pointer that does not prevent an object from being deallocated.
The system is designed to be minimal and explicit, consisting of three parts:
The weak &T Type
A weak reference has a distinct type to ensure compile-time safety. This allows the compiler to enforce correct usage.
The weak() Downgrade Function
To create a weak reference, you must use the explicit, built-in weak() function. This makes the intent clear and avoids implicit "magic".
let strong_ref: &Foo = new shared { ... };
// Explicitly create a weak reference from a strong one. let weak_ref: weak &Foo = weak(strong_ref);
The if let Upgrade Pattern
Accessing the object behind a weak reference is inherently optional, as the object may have been deallocated. Aela enforces safe access through a conditional if let binding.
// Given a variable 'weak_ref' of type 'weak &Foo'
5. Copying Stack Values
- Heap allocation requires copying the stack value:
- This avoids moving ownership from the stack. Moves are not allowed .
5. Reference Types and Behavior
| Kind | Syntax | RC Type | Thread-Safe | Mutable |
|---|---|---|---|---|
| Stack value |
let x: T = ...
|
None | N/A | No |
| Heap reference |
let x: &T = new shared {...}
|
RC | No | No |
| Heap reference (mutable) |
var x: &T = new shared {...}
|
RC | No | Yes |
| Heap reference (atomic) |
let/var x: &T = new shared atomic {}
|
ARC | Yes | Depends |
6. Compile-Time Analysis
- Safe aliasing of references
-
Proper use of
var(exclusive mutation) - Reference count tracking correctness
- No runtime borrow errors required
The compiler performs static analysis to ensure:
7. No Implicit Moves from Stack
- Stack values cannot be moved to the heap.
-
Heap promotion always requires a
copy
using
{ ...val }. - new shared { ...std::move(x) } is on the roadmap
Calling Conventions
How variables are allocated and passed to functions. The 'new' keyword is the explicit signal for heap allocation and reference counting.
Primitives & Simple Structs (Stack Allocated)
- Rule: Variables declared WITHOUT the 'new' keyword live on the stack.
- In Memory: The variable holds the data directly.
- Function Passing: Passed BY VALUE (a full copy is made).
- Lifetime: Automatic (destroyed when the variable goes out of scope).
- Reference Counted: No.
Boxed Values (Heap Allocated via 'new')
- Rule: Variables initialized WITH the 'new' keyword are allocated on the heap.
- In Memory: The variable holds a POINTER to a "box" on the heap.
- Function Passing: Passed BY REFERENCE (a copy of the pointer is made).
- Lifetime: Managed (Reference Counted).
- Reference Counted: Yes.
Closures (Special Case)
- Rule: A closure that captures variables has its environment allocated on the heap.
-
In Memory: The closure variable is a
{func_ptr, env_ptr}struct. Theenv_ptrpoints to a heap-allocated box containing the captured variables. -
Function Passing: The
{func_ptr, env_ptr}struct itself is small and is passed BY VALUE. - Lifetime: The environment's lifetime is managed by Reference Counting.
Refinement & Dependent Types
TL;DR: Refinement & dependent types are types that embed logical conditions or values themselves, making illegal states unrepresentable by construction.
If you're coming from languages like JavaScript, C++, or Rust, you're used to type systems that check the
shape
of your data. For example, a type checker ensures you don't use a
string
where a
number
is expected.
Aela takes this a step further with refinement types and dependent types . These are features that allow the type system to understand and check the values of your data, not just their shape. It's more rigorous that shape checking, it's a value-aware check that happens entirely at compile time .
Motivation
Most type systems validate
shape
(ie., "this is an
int
"). Aela also validates
value-level facts
: a string is
non-empty
, an integer is
non-zero
, a vector’s
length
matches what your function promises. This turns bugs into
compile‑time errors
and makes code self-documenting.
- Earlier feedback: logical mistakes become type errors you see during compilation.
-
Stronger intent:
types like
NonEmptyStringandNonZerointtell the reader (and the compiler) exactly what you mean. - Confidence: fewer defensive checks sprinkled across your code.
Compile-Time VS. Run-Time Arguments
Aela uses a
single
parameter list for functions, with an optional
compile-time section
separated from the
run-time
section by a semicolon
;
. Parameters are separated by commas. If a parameter has a type annotation, it will be considered a value parameter. If a value has no type annotation, it will be considered a type.
Rules
-
Tokens
before
;are compile-time parameters (type or const/value-level), -
Type parameters:
bare identifiers (ie.,
T,U). -
Const parameters:
Name: Typeform (ie.,N: int). -
Tokens
after
;are run-time parameters (the usualname: Type, with optionalmut/ spread, per language rules). -
If there’s
no
;, the entire list is treated as run-time parameters. -
If the CT part is
empty
, omit
;(preferred style). If the RT part is empty and CT is present, keep a trailing;.
Refinement Types: add a
where
-clause to any base type
A refinement type is a base type with a logical predicate.
Syntax
:
{ id: Type where predicate }
Use refinements when you want value-aware validation while keeping the underlying representation.
Note
The compiler statically checks predicates where it can (literals, constant expressions, and facts learned from prior code/contracts). When a predicate can’t be decided statically, you’ll provide evidence (see “Proving facts to the compiler”).
Dependent Types: when types mention values
A dependent type is a type that depends on values .
A simple, practical pattern is to refine a function’s return by its inputs :
Here the return type depends on the run-time values
a
and
b
. The compiler enforces this contract at compile time wherever it can be proven, and narrows follow-up reasoning.
Another common pattern uses compile-time const parameters to index types:
-
Tis a type parameter . -
MandNare const parameters (compile-time integers). -
The result type’s length
computes
to
M + Nat the type level .
Proving facts to the compiler
There are three common ways to convince the compiler of a refinement:
- Literals and constant expressions — obvious at compile time:
- Local reasoning / guards — use a guard to establish a fact for a scope:
- Type-level contracts — express properties in the type and let the compiler check uses:
Note
In more advanced code, system properties and invariants (see
KW_REQUIRES,KW_ENSURES,KW_INVARIANT) can encode broader guarantees, which the compiler uses as facts within the relevant scope.
End‑to‑End: making division-by-zero impossible
Parameter List: full spec (informal)
Validation
-
The
CT
side only accepts
type/const
parameters (no
mut, no spreads). - The RT side accepts ordinary parameters.
-
A single
top‑level
;within the parentheses splits CT from RT. -
Style: omit an
empty
CT
(; ...)— prefer just( ... ).
Choosing Refinement vs. Dependent
Use refinement types when:
-
You’re constraining a familiar base type (
i32,string, a struct) with a predicate (n != 0,len(s) > 0). - You want to reuse existing APIs with stronger safety.
Use dependent types when:
- The shape of your result depends on inputs (lengths, indices, protocol states).
-
You have natural
compile-time parameters
(ie., a block size
N: int).
They compose well: dependent function types can return refinement types, and vice versa.
Error messages (ergonomics)
- CT/RT mixup : “Compile-time parameter list may only contain type/const parameters.”
-
Missing `;`
: If the first parameter looks like a type/const param (
TorN: int), suggest adding the;. - Unproven refinement : Point to the predicate and suggest guards or helper constructors to provide evidence.
FAQ
Q: Do I have to write CT parameters at call sites? A: Typically no — they’re inferred from RT arguments and expected return types. You can guide inference via annotations.
Q: Can I have multiple `;`?
A: No. There is at most one
top‑level
;
per parameter list.
Q: Are const parameters immutable? A: Yes. They are compile-time values; mutability doesn’t apply.
Q: What about async/pure/thread modifiers?
A: These work unchanged and apply to the function as a whole; the
;
split only affects parameter binding.
Worked examples (with
;
)
1) Identity with a type parameter
2) Map over a vector (type‑level only)
3) Concat with const lengths (conceptual)
4) Safe division with a refinement type
Function Types
Why functions feel like they have “colors” (and how Aela fixes it)
Many languages split the world into synchronous and asynchronous functions. That split tends to spread—call sites, types, libraries—until everything is “[colored][1].” Aela acknowledges that programs have different execution disciplines (purity, async, thread-safety), then solves the composition pain with first-class types , clear call rules , and built‑in intrinsics so you can cross boundaries intentionally and safely.
At a glance:
-
Three disciplines (colors):
pure,thread,async. - Simple call rules enforced by the checker.
- Intrinsic adapters (escape hatches) to cross colors when needed.
-
Contracts
(
requires/ensures) andsystemblocks to guard risky crossings.
The three disciplines
pure
- What it means: No observable side effects; referentially transparent.
- When to use: Deterministic computation, validation, transforms.
-
Gotchas:
Cannot call
asyncor impure functions. May be combined withthreadif the work is offloaded safely.
thread
- What it means: Safe to run off the main executor (e.g., in a worker). No ambient event‑loop assumptions; data must be sendable or shared safely.
- When to use: CPU‑bound work, blocking IO wrapped properly, parallelizable tasks.
-
Combines with:
pure(i.e.,pure thread fn).
async
-
What it means:
May suspend at
awaitpoints, returns a future/awaitable. - When to use: IO‑bound workflows, coordination with timers, event‑driven code.
-
Note:
asyncis notpure. It stands alone.
Call rules (the short version)
-
pure→ may call onlypure. -
thread→ may callthreadorpure; invoking athread fnyields aTaskhandle. -
uncolored
(impure) → may call
thread/puredirectly andasyncvia intrinsic. -
async→ mayawaitotherasyncfunctions orTaskhandles, and also callpure/threadwithout suspension. -
awaitis legal inside bothasync fnandthread fn, because awaiting aTaskjoin or future is permitted in either discipline.
Note
The checker gives precise diagnostics and suggests the right intrinsic when you cross a boundary.
Function types are first‑class
Aela’s type system includes function types with their discipline. These are expressed with refinement/dependent types instead of generics:
Function parameters can carry refinements on their arguments or return types, e.g.:
This makes intrinsics predictable and type‑safe, without requiring a generic system.
Crossing boundaries: standard intrinsics
To make crossing disciplines predictable and ergonomic, Aela ships intrinsics in the standard library:
These are typed adapters that the checker understands.
Signatures & typing rules (no generics required)
Aela doesn’t need parametric generics here—these intrinsics are schematic over types and use refinement/dependent predicates to express constraints. We write them informally as “for any types X, Y…”, and the checker discharges the side conditions.
`to_threaded` *
Checks:
argument and captured values must be sendable or
shared/atomic
. No reliance on an ambient event loop.
`to_async` *
Runtime model: schedules on the blocking/CPU pool; if the pool is saturated, the returned future suspends until capacity is available.
`to_blocking`
* (the common case inside
async
)
This is analogous to Rust Tokio’s `block_in_place`: it yields to the scheduler and executes on the blocking pool.
`block_on` * (drive async to completion from a worker thread)
Policy:
Forbidden on the event‑loop thread; prefer
to_async
/
to_blocking
when in async contexts.
`detach` / `join` *
Diagnostics you’ll see
- “Argument to `to_threaded` captures non‑sendable state Rc(T).”
- “`to_blocking` called from non‑async context.”
- “Pool saturated: `to_async` may suspend until space frees up.”
Examples
CPU work from async (don’t block the loop):
Submit a threaded task and await the handle:
Drive async to completion from a worker thread:
Lift pure compute to workers:
Use blocking work from async:
Use `to_async` to present sync/threaded as async:
Detached background task:
Idioms and examples
-
Keep core transforms
pure, wrap with intrinsics at the edges. -
Use
thread { ... }blocks for structured parallelism; all tasks inside must complete before exit. -
Use
std::selectto race multiple async sources and cancel losers automatically.
FFI without foot‑guns
Declare foreign functions with explicit disciplines and refinements:
Then wrap with
system
policies to guard where they are allowed:
Concurrency & mutability
-
Use
shared/atomictypes for cross‑thread data. -
threadfunctions may operate onsharedsafely. -
purefunctions must not mutate shared state.
Borrow Checker
Core Entities and Notation
-
Place
p: An lvalue, i.e. a path to a memory location. Examples:x,arr[i],s.field. In safe Aela, places are structured paths (no raw pointer deref, pointer arithmetic, or unions). -
Reference
&p/&mut p: A borrow of a place, producing a reference value. -
Loan
L(p, kind): The abstract fact thatpis borrowed, withkind ∈ {shared, unique}. -
Program Point
q: A location in the control-flow graph (CFG). -
Alive(L, q)
: Predicate meaning loan
Lis still valid at pointq(lexically-free semantics). - Operations :
-
reads(p, q): a read of placepatq. -
writes(p, q): a write to placepatq. -
moves(p, q): a move from placepatq. -
Aliases(x, p, q)
:
xholds a reference topatq(logical predicate, tracked via loan origins). -
Escape(r, q)
: Reference
rescapes its region atq(returned, stored, captured, etc.).
inter-procedural Region & Effect Summaries
-
Each function with references is internally elaborated to carry region variables
ρ. -
Example:
fn get_first(&[i32]) -> &i32elaborates tofor<ρ> fn(&ρ [i32]) -> &ρ i32. - Compiler emits a summary : region params, outlives constraints, and an escape set.
- Call sites instantiate these summaries and unify with argument regions. This provides modular checking without WPO.
-
Optional disambiguation syntax for multiple inputs:
fn pick(arr: &T as A, other: &T) -> &A T;ties return to param markerA.
Minimal Syntax Escape Hatch (as A)
Keeps everyday code lifetime-less, but allow explicit disambiguation when inference cannot decide.
-
Syntax:
Parameters may carry labels:
fn foo(x: &T as A) -> &A T. Returns may use&A. Struct fields may tie to labels. -
Labels introduce internal region vars (`ρ_A`)
that summaries use. Unlabeled references get fresh
ρ’s. - Elision rules: one input reference → auto-tie; multiple ambiguous inputs → require a label.
- Advanced usage:
-
Outlives constraints can be added:
fn foo(x: &T as A, y: &U as O) -> &A T where O : A;. -
Methods:
selfimplicitly labeledSelf, but can also be explicitly labeled:self: &Self as S. -
Diagnostics:
Role-based: “return value tied to param
a(label A) butadoes not live long enough.” Quick fixes: “Addas Ato param.”
Lifetimes in Data Structures
-
Reference fields implicitly carry region parameters.
struct Node { data: &T }elaborates tostruct Node<ρ, T> { data: &ρ T }. -
Construction instantiates
ρfrom the argument’s region. Error if struct outlives the referenced data.
Place Overlap (I3)
-
poverlapsp. -
poverlapsp.f. Distinct fields disjoint. -
arr[i]vsarr[j]: disjoint if indices are distinct compile-time constants; else conservative overlap. -
Library intrinsics like
split_at_mutprovide disjointness proofs via trusted contracts or runtime checks.
Escape Conditions (E1)
Reference escapes if it flows into a longer-lived region by:
- Returning from a function.
- Assigning to a longer-lived binding.
- Storing in struct/enum/global.
- Capturing by closure/future that outlives scope.
- Passing to FFI (unless contract says non-retaining).
- Storing in concurrent/shared cell accessible later.
- Erasure into longer-lived object/interface.
Asynchronous Code (async/await)
-
Phase 1:
Forbid loans across
awaitunless the origin outlives the entire future. Practically: locals cannot crossawait; only borrows from captured fields of the async task can. -
Phase 2:
Desugar
asyncto state machines and check across suspension points, enabling safe long-lived borrows.
Closures (FunctionExpression)
- Capture classification:
- Read-only → shared.
- Mutate → unique.
- Move → by-value.
- Trait mapping: shared → Fn; unique → FnMut; move → FnOnce.
- Escaping closures require captured regions to outlive closure region.
Refinement Types
-
Built-in predicates like
initialized(x),not_escaped(x)are decidable and do not require heavy SMT. - User-defined predicates and full logical refinement are measured to avoid compile-time blowups and underspecification.
Diagnostics Without Lifetime Names
-
Role-based regions: “returned reference must not outlive borrow of
arr.” - Highlight borrow creation, return site, and conflict.
- Optional symbolic labels (r1, r2) in error messages for clarity.
Rule-by-Rule Examples
Rule-by-Rule Examples (Concrete)
R0 — Implicit, Lexically-Free Regions
L1 — Loan Creation
L2 — Loan Propagation
A1 — Unique Exclusivity
A2 — Shared Read‑Only
I1 — Write Invalidates
I2 — Move Invalidates
I3 — Overlap/Fields
U1 — Use Requires Alive
U2 — No After‑Free
RB1 — Shared from Unique
RB2 — Unique from Unique (optional v1)
E1 — No Escaping Borrows
M1 — Binding Mutability
M2 — No Shared Mutability Without Atomics
T1 — Temporaries
T2 — Branches/Loops/Match
F1 — FFI Preconditions
F2 — FFI Postconditions
RFT1 — Refinement Well‑Formedness
RFT2 — Discharge at Use Sites
inter‑Procedural Summary (Elision)
Structs with Reference Fields (Implicit Regions)
Async Phase 1 — No Unique Loans Across Await
Closures — Capture Classification
RTOS Harmony
Linux Setup
You most likely need to edit your udev rules
If it doesn’t have your board’s rules in it, list your board to get the vendor and product id.
Replace ID VVVV:PPPP
Reload, re-enumerate
Unplug & Replug, press the reset button & then check:
Zephyr
Install
Install the Zephyr CLI tool
Allows you to flash your Arduino without needing to use sudo every time. It gives your user account permission to access the USB device.
Create a project directory. Create a python environment in the project directory. Initialize it as a new zephyr project. Run the update command to get all the sub-repos.
Go back to your projects root and get the minimal SDK for cross compiling.
Calculate the SHA sum if you want
Extract the SDK and run the setup
Initialize everything by entering your zephyr directory, activate the python env and set the environment (links the SDK you just got)
Compile & Flash Test
Finally, build an Arduino image. I use Blinky here because its super simple and makes sense to start with as a sanity test.
Integrating with Aela
Aela can output object files, so it's very simple to create a Zephyr project with Aela by just adding
CMakeLists.txt
and
prj.conf
files.
Example
prj.conf
Example
CMakeLists.txt
Compiling
Note
The _AE_ARM_OPTS trick copies exactly the -mcpu/-mfpu/-mfloat-abi/-mthumb flags Zephyr uses for this board, so your Aela object links ABI-cleanly without you guessing the right combo. (UNO R4 WiFi is a Cortex-M4; Zephyr’s board page confirms the arch.)
FreeRTOS
Discover Platform Build Flags
Lets assume an Arduino R4 here as an example.
We need to "spy" on the Arduino CLI to get the exact compiler and linker flags required for the R4 WiFi. The Aela build system will pass these directly to its underlyng C compiler. Arduino code files are called "sketches".
- Create a dummy sketch:
- Run a verbose compile:
-
Find and copy the linker command.
Scroll through the output and find the longest command, which will start with something like
.../arm-none-eabi-g++ .... It will be a very long line that links all the object files and libraries together. Copy this entire command into a text editor. We're going to use its flags.
It will look something like this (shortened for clarity):
.../arm-none-eabi-g++ -Os -Wl,--gc-sections -mcpu=cortex-m4 ... -L/path/to/build -Wl,--start-group -lArduinoCore -lrtos -lm -Wl,--end-group ...
We will now transfer the important parts of that command into your
index.json
.
Step 2: Project Structure
Organize your project with your source code inside a
src
directory.
Step 3: The
index.json
Manifest
This is the control center. We'll define a custom platform target called
"arduino_r4"
and place all the flags we discovered into the
link
section.
Create the file
`index.json`
(unless it was created via
aec init
):
Note
You must replace the placeholder paths (
/home/user/...,/tmp/arduino/...) with the full, absolute paths you copied from your verbose compiler output in Step 1.
Step 4: The Source Code
The source code is the same as before, but now it lives in the
src/
directory.
src/main.ae
src/shim.c
Step 5: Build and Flash
You tell the Aela compiler what you're targeting, and it uses the manifest to set the correct build flags.
Build the project for the target triple:
Flash the output. The build process should still result in a .uf2 file in a build directory. You can then flash it with the Arduino CLI as before.
FAQ
Why is Aela Commercial-Source?
Security
Transparency helps, but it’s not enough. Heartbleed, Log4Shell, and the xz backdoor landed in widely used open-source code. The gap isn’t visibility—it’s accountability, resourcing, and execution.
A commercial, closed-source model brings clear responsibility and funded security work:
Formal accountability
Commercial software runs under contracts and SLAs. When a vulnerability appears, a named company is on the hook—legally and financially—to fix it. In decentralized open source, responsibility often sits with volunteers without service commitments.
Dedicated, directed resources
We staff full-time security teams for audits, pen tests, and vulnerability management. Budget and people are assigned to unglamorous but critical maintenance and hardening that many projects lack.
Cohesive vision and focused dev
One organization sets architecture, roadmap, and tradeoffs. Decisions move faster and designs stay consistent. Large open-source efforts juggle many voices, which can slow delivery and blur design.
Choose the assurance model that fits your risk. Community stewardship offers transparency and shared responsibility. Our model adds contractual guarantees, professional assurance, and direct accountability backed by dedicated resources.
Cost
Look past sticker price to total cost of ownership (TCO).
Open source
TCO includes hiring in-house experts who contribute upstream to unblock features and address security issues.
Commercial source
We fold those operational costs into a predictable subscription or license. You get SLAs, legal indemnification, and dedicated support that map cleanly to enterprise procurement and risk frameworks.
In short: invest in internal capability and control, or buy a service-backed solution with predictable costs and clear responsibility.
Overview
Aela is a commercial-source language for embedded systems.
Aela is a compiled, memory-safe language with a strong, static type system. It started in 2018 as a fork of Rust, but has diverged significantly to service embedded systems use cases. It has a similar syntax and sticks to many of the same goals such as ownership semantics, memory and concurrency safety, only pay for what you use, and zero-cost abstractions.
Programming Language Landscape
This is where Aela fits In terms of Simplicity vs Complexity, and Familiarity vs Uniqueness.
Langauge Goals
All language design decisions revolve around determinism and performance for embedded systems.
Aela also focuses on harmony with C and C++. It offers automatic binding through header inclusion, packed and ordered structs, and inversion of control for system calls like
write(2)
.
Language Goals
All language design decisions revolve around providing absolute determinism, safety, and performance for embedded systems. Anything that compromises these goals through ambiguity, hidden costs, or non-local behavior is eliminated. All excluded features have an ideal alternative (see documentation) or have been simply omitted.
| Excluded Feature | Justification |
|---|---|
| Panics | Panics are not in the type signature; they violate the principle of abstraction and create a hidden, unrecoverable failure path that bypasses the type system. |
| Exceptions | Exceptions create hidden, non-local control flow paths that are impossible to reason about statically and incur runtime overhead. Error handling must be explicit. |
| Global State | Global variables create hidden dependencies and introduce non-determinism, making concurrency unsafe and program behavior impossible to reason about locally. |
| Lifetime Annotations | Explicit lifetime annotations are a significant source of complexity and syntactic noise. The compiler must be smart enough to infer ownership duration without burdening the programmer. |
| Macros | Macros create non-linear, hard-to-debug code flow ("magic"). Code must be explicit and analyzable; what you see is what the compiler gets, ensuring predictability and toolability. |
unsafe
keyword
|
The
unsafe
keyword is an escape hatch that undermines the entire memory safety guarantee of the language. Aela issues a handful of unsafe operations rather than entire blocks.
|
| Attributes | Attributes are a form of meta-programming that injects non-local behavior and hidden complexity. They modify code in non-obvious ways, violating the principle that code should be explicit. |
| Pointer Arithmetic | Direct pointer manipulation is a primary source of memory safety vulnerabilities (e.g., buffer overflows). Safe, bounds-checked slice operations are the only acceptable alternative. |
| Advanced Traits | Complex trait systems lead to baroque, unreadable type signatures and cryptic compiler errors. Simplicity and clarity are prioritized over abstract expressive power. |
| Inline Assembly | Inline assembly is non-portable, opaque to the optimizer, and breaks static analysis. Low-level routines must be provided via a stable C ABI, ensuring a clean contract. |
null
keyword
|
Null pointers are the source of countless runtime crashes. The absence of a value must be explicitly represented and handled in the type system (e.g., via
Option
).
|
| Operator Overloading | Operator overloading makes code ambiguous and hides the cost of operations. Function calls must be explicit to ensure code clarity and predictable performance. |
| Implicit Conversions | Implicit conversions hide potential bugs, such as data truncation or precision loss. All type conversions must be explicit, forcing the programmer to acknowledge the operation. |
Feature Comparison
| Feature | Aela | C | C++ | Rust | Ada |
|---|---|---|---|---|---|
| Compile-Time Memory Safety | √ | √ | √ | ||
| Cold Build Speed | √ | √ | √ | ||
| Reference Counted | √ | √ | |||
| Automatic RAII | √ | √ | |||
| Formal Verification | √ | √ | |||
| Dependent & Refinement Types | √ | ||||
| Language-Native Concurrency | √ | ||||
| Structured Concurrency | √ | √ | |||
| Automatic C/C++ safe binding | √ |
Module and Package System
Aela's module system is designed for organizing code into reusable and maintainable packages. The entire system is controlled by a manifest file named
index.json
.
Packages are precompiled binaries, this makes them fast, but they can also ship with the source code and that won't make them any slower. See the GUI module for a comprehensive example of a multi-platform module.
Package Manifest
Every Aela project is a package, and every package is defined by an
index.json
file at its root. This file tells the compiler everything it needs to know about your project.
Key Fields
- "name": The official name of your package (e.g., "my-app").
- "version": The version number (e.g., "0.1.0").
- "entry": The relative path to the main source file that acts as
- the entry point for compilation (e.g., "src/main.ae").
Example
Managing Dependencies
You can include other Aela packages in your project by listing them in the "dependencies" section of your
index.json
.
- The key is the name you will use to import the module (e.g., "ui").
-
The value is the relative path from your
index.jsonto the - dependency's root directory.
Example
Exports
By default, all functions, structs, and variables defined in a file are private to that file. To make a symbol visible to other modules, you must explicitly mark it with the
export
keyword.
Example
Re-exports
Imports
You use the
import
statement to bring exported symbols from other modules into the current file. Aela supports two forms of imports.
Namespace Import
This is the most common form. It imports all exported symbols from a module under a single namespace alias.
Syntax
Example
Named Imports
This allows you to import specific functions or types directly into the current scope, without needing a namespace qualifier.
Syntax
Example EXAMPLE
Advanced Build Configuration (FFI)
The
index.json
manifest also allows you to control the native build process, which is essential for linking against C, C++, or Objective-C code when using the Foreign Function Interface (FFI).
Key Sections
- "sources": A list of native source files (.c, .mm, etc.) to be
- compiled alongside your Aela code.
- "link": A list of flags to pass to the system linker (e.g., clang).
Notes
- Both "sources" and "link" flags can be specified per-platform
- (e.g., "darwin", "linux", "windows") or as "shared" for all platforms.
- The compiler automatically includes the linker flags from all packages
- listed in your "dependencies" section.
Example
This configuration tells the Aela compiler:
-
On macOS, compile the
src/native/apple.mmObjective-C++ file. - When linking the final executable, add flags to link against the
- Foundation, AppKit, and ObjectiveC system frameworks.
Failures
A primary cause of software defects, security vulnerabilities, and developer anxiety is the "Trust Gap": the difference between what a function's signature claims it does and what its implementation can actually do. Aela is designed to eliminate this gap. Its error and fault handling system is built on a single, non-negotiable principle: a function's signature must be a complete and honest contract, and the compiler must enforce it.
Don't Try-Catch
Traditional exception systems, common in languages like Java, JavaScript, C++, and Python, introduce a form of hidden control flow. A throw or raise statement is a non-local goto that is often invisible in the function's signature.
Consider a typical function in such a language:
function get_user(id: int) -> User
This signature makes a simple promise: "Give me an integer, and I will give you a User." However, the implementation might throw a DatabaseConnectionException, a UserNotFoundException, or a NullPointerException. To understand the function's true behavior, the developer must embark on a research project: reading the documentation (which may be out of date), reading the source code, and reading the source of every function it calls.
This breaks a developer's ability to reason locally about the code.
Don't Panic
Some modern languages, notably Rust, attempt to solve error handling this by creating a two-tiered system:
-
Recoverable Errors (
Result): For expected failures (e.g., file not found). These are part of the type signature. -
Unrecoverable Bugs (
panic!): For programmer errors (e.g., index out of bounds). These are not part of the type signature.
While an improvement, this still creates a hidden side-channel for bugs. More critically, the panic! mechanism creates a deep schism between platforms, especially for embedded systems.
The unwind vs. abort Schism:
On servers, panic! defaults to unwind, a slow and complex process that runs cleanup code (destructors). This adds a significant "tax" to the binary size, which is unacceptable on resource-constrained devices.
On embedded systems, developers are forced to configure panics to abort, which immediately halts the program.
The Broken Promise of abort: When panic = "abort" is used, the language's core safety promise—that resources will be cleaned up on failure (RAII via Drop in Rust)—is broken. Destructors are never called. A MutexGuard will leave a mutex permanently locked. A peripheral that was supposed to be disabled is left in an active state. The very code written to ensure safety on failure becomes useless.
This patched-together model is not a first-principles solution. Aela requires a single, unified system that is safe, deterministic, and efficient on all platforms.
The Fault Model
The Principle of the Honest Contract: Aela avoids ambiguity in failure handling by enforcing that the outcome of a function must be encoded in its signature.
Function Signatures
A function declares its potential to terminate due to an unrecoverable logic bug by using the
|
operator in its return type. This operator separates the single success type from a list of one or more
fault
types.
fn get_at(slice: &[u8], index: int) -> u8 | OutOfBounds;
This signature is an honest contract. It tells any caller: "This function will either return a
u8
, or it will terminate with an
OutOfBounds
fault. There are no other possibilities."
The
fault
and
raise
Keywords
-
`fault`
: A keyword used to declare a type that represents a logic bug or contract violation. This distinguishes it from
structorenum, which represent data and recoverable errors. -
fault OutOfBounds(index: int, len: int); -
`raise`
: A keyword that triggers a fault. It immediately stops the current function's execution and propagates the fault to the caller.
raiseis considered a terminal action. -
raise OutOfBounds(index: index, len: len);
The Standard
match
Statement
The
match
statement is how Aela handles the outcome of functions that may terminate with faults.
Semantic Rules
The safety and special behavior of fault handling are not derived from the syntax, but from a single, powerful semantic rule in the compiler:
If the expression being evaluated by a
matchstatement has a fault path in its signature (i.e., contains a|), then the compiler enforces a "Terminal Arm Rule" on any arm that matches afault-declared type.
This rule is context-dependent. It is triggered by the signature of the function being called, not just the type of the pattern. This is the key insight that resolves all ambiguity.
The "Terminal Arm Rule" is defined as: The block of the arm must end with a terminal statement. Aela does not have implicit returns, so this is a direct check of the final statement in the block.
Terminal statements are:
-
raise; -
std::abort(); -
std::halt(); -
std::reboot();
Note
purefn ketword must not introduce or handle faults; they cannot have a | in their return type and cannot raise.
Note
Faults must not cross FFI; board/FFI shims must convert faults to domain-appropriate codes or terminals.
Note
AP131outlines a proposal to havereturnbe the equivalent ofraise. Allowing both and specifying that they’re identical.
Note
AP132outlines a proposal to permit functions to be generic over an open set of faults (variadic type param), so adapters don’t re-enumerate: ie: fn map(f: fn(T) -> U | E..., x: T) -> U | E...;
Note
AP134outlines a proposal to add a desugaring operator. let b = get_at(xs, 0)?; // expands to match/raise
Complete Examples: The Rule in Practice
These two examples demonstrate how the context-dependent rule creates a safe and unambiguous system.
Example 1: Handling a Live Fault Event
If
reboot;
were omitted, the compiler would issue a clear error:
"Fault-handling arm must end with a terminal statement."
Example 2: Inspecting a Fault as Data
Memory & Mutability
1. Default Allocation Semantics
- All values start on the stack by default.
- Stack allocations are:
- Fast
- Scoped
- Owned directly by the binding.
2. Heap Allocation
To allocate a value on the heap, use the
new
keyword with one of the following modifiers:
Syntax
All heap allocatations are reference counted.
Behavior of
new
Modifiers
| Modifier | Behavior | Description |
|---|---|---|
(none)
|
Heap Allocation (OS based allocator). | Returns a handle/reference to a freshly allocated object, immutable and can not be loaned. |
shared
|
Heap. Shared ownership, single-threaded. | Allowed to be loaned; not thread-safe; not Send/Sync-like. |
shared atomic
|
Heap. Shared ownership, thread-safe. | Reference counts and interior coordination use atomics; Send/Sync if T satisfies the “atomic-safe” requirements you define. |
weak
|
Heap, weak handle to a shared object. | Does not keep the allocation alive; must be paired with at least one strong shared/shared atomic owner somewhere to be useful. |
static
|
Static storage, no OS allocator. | Object is placed/constructed in static memory (BSS/RODATA/flash/SECTION), suitable for bare-metal/MCUs. Lifetime: program-long (or until explicitly torn down by system semantics). |
static atomic
|
Static storage, thread-safe coordination via atomics. | Shared global counters updated in ISR + mainline, Lock-free buffers (ring buffers for UART, DMA, etc.). Configuration/state variables read by multiple cores or ISRs. On a simple single-core MCU with no interrupts, it’s redundant but harmless. |
-
new shared: -
Performs a
copy
of the stack value (
{ ...val }) - Allocates it on the heap
- Wraps it in a reference-counted container (non-atomic)
-
Returns a reference:
&T
-
new shared atomic: - Same as above, but uses atomic reference counting (thread-safe)
3. Mutability Semantics
Mutability is determined only by the binding keyword :
| Keyword | Meaning |
|---|---|
let
|
Immutable binding (read-only) |
var
|
Mutable binding (read-write) |
-
The
mutkeyword is not necesasry here. - Mutability is not encoded in types, structs, or fields.
-
letandvarare both lexically scoped.
Example
Note
Attempting to mutate a
let-bound reference results in a compile-time error!
4. Weak References & Cycle Prevention
To solve the problem of reference cycles (e.g., a parent refers to a child, and the child refers back to the parent), Aela will provide weak references. A weak reference is a non-owning pointer that does not prevent an object from being deallocated.
The system is designed to be minimal and explicit, consisting of three parts:
The weak &T Type
A weak reference has a distinct type to ensure compile-time safety. This allows the compiler to enforce correct usage.
The weak() Downgrade Function
To create a weak reference, you must use the explicit, built-in weak() function. This makes the intent clear and avoids implicit "magic".
let strong_ref: &Foo = new shared { ... };
// Explicitly create a weak reference from a strong one. let weak_ref: weak &Foo = weak(strong_ref);
The if let Upgrade Pattern
Accessing the object behind a weak reference is inherently optional, as the object may have been deallocated. Aela enforces safe access through a conditional if let binding.
// Given a variable 'weak_ref' of type 'weak &Foo'
5. Copying Stack Values
- Heap allocation requires copying the stack value:
- This avoids moving ownership from the stack. Moves are not allowed .
5. Reference Types and Behavior
| Kind | Syntax | RC Type | Thread-Safe | Mutable |
|---|---|---|---|---|
| Stack value |
let x: T = ...
|
None | N/A | No |
| Heap reference |
let x: &T = new shared {...}
|
RC | No | No |
| Heap reference (mutable) |
var x: &T = new shared {...}
|
RC | No | Yes |
| Heap reference (atomic) |
let/var x: &T = new shared atomic {}
|
ARC | Yes | Depends |
6. Compile-Time Analysis
- Safe aliasing of references
-
Proper use of
var(exclusive mutation) - Reference count tracking correctness
- No runtime borrow errors required
The compiler performs static analysis to ensure:
7. No Implicit Moves from Stack
- Stack values cannot be moved to the heap.
-
Heap promotion always requires a
copy
using
{ ...val }. - new shared { ...std::move(x) } is on the roadmap
Calling Conventions
How variables are allocated and passed to functions. The 'new' keyword is the explicit signal for heap allocation and reference counting.
Primitives & Simple Structs (Stack Allocated)
- Rule: Variables declared WITHOUT the 'new' keyword live on the stack.
- In Memory: The variable holds the data directly.
- Function Passing: Passed BY VALUE (a full copy is made).
- Lifetime: Automatic (destroyed when the variable goes out of scope).
- Reference Counted: No.
Boxed Values (Heap Allocated via 'new')
- Rule: Variables initialized WITH the 'new' keyword are allocated on the heap.
- In Memory: The variable holds a POINTER to a "box" on the heap.
- Function Passing: Passed BY REFERENCE (a copy of the pointer is made).
- Lifetime: Managed (Reference Counted).
- Reference Counted: Yes.
Closures (Special Case)
- Rule: A closure that captures variables has its environment allocated on the heap.
-
In Memory: The closure variable is a
{func_ptr, env_ptr}struct. Theenv_ptrpoints to a heap-allocated box containing the captured variables. -
Function Passing: The
{func_ptr, env_ptr}struct itself is small and is passed BY VALUE. - Lifetime: The environment's lifetime is managed by Reference Counting.
Refinement & Dependent Types
TL;DR: Refinement & dependent types are types that embed logical conditions or values themselves, making illegal states unrepresentable by construction.
If you're coming from languages like JavaScript, C++, or Rust, you're used to type systems that check the
shape
of your data. For example, a type checker ensures you don't use a
string
where a
number
is expected.
Aela takes this a step further with refinement types and dependent types . These are features that allow the type system to understand and check the values of your data, not just their shape. It's more rigorous that shape checking, it's a value-aware check that happens entirely at compile time .
Motivation
Most type systems validate
shape
(ie., "this is an
int
"). Aela also validates
value-level facts
: a string is
non-empty
, an integer is
non-zero
, a vector’s
length
matches what your function promises. This turns bugs into
compile‑time errors
and makes code self-documenting.
- Earlier feedback: logical mistakes become type errors you see during compilation.
-
Stronger intent:
types like
NonEmptyStringandNonZerointtell the reader (and the compiler) exactly what you mean. - Confidence: fewer defensive checks sprinkled across your code.
Compile-Time VS. Run-Time Arguments
Aela uses a
single
parameter list for functions, with an optional
compile-time section
separated from the
run-time
section by a semicolon
;
. Parameters are separated by commas. If a parameter has a type annotation, it will be considered a value parameter. If a value has no type annotation, it will be considered a type.
Rules
-
Tokens
before
;are compile-time parameters (type or const/value-level), -
Type parameters:
bare identifiers (ie.,
T,U). -
Const parameters:
Name: Typeform (ie.,N: int). -
Tokens
after
;are run-time parameters (the usualname: Type, with optionalmut/ spread, per language rules). -
If there’s
no
;, the entire list is treated as run-time parameters. -
If the CT part is
empty
, omit
;(preferred style). If the RT part is empty and CT is present, keep a trailing;.
Refinement Types: add a
where
-clause to any base type
A refinement type is a base type with a logical predicate.
Syntax
:
{ id: Type where predicate }
Use refinements when you want value-aware validation while keeping the underlying representation.
Note
The compiler statically checks predicates where it can (literals, constant expressions, and facts learned from prior code/contracts). When a predicate can’t be decided statically, you’ll provide evidence (see “Proving facts to the compiler”).
Dependent Types: when types mention values
A dependent type is a type that depends on values .
A simple, practical pattern is to refine a function’s return by its inputs :
Here the return type depends on the run-time values
a
and
b
. The compiler enforces this contract at compile time wherever it can be proven, and narrows follow-up reasoning.
Another common pattern uses compile-time const parameters to index types:
-
Tis a type parameter . -
MandNare const parameters (compile-time integers). -
The result type’s length
computes
to
M + Nat the type level .
Proving facts to the compiler
There are three common ways to convince the compiler of a refinement:
- Literals and constant expressions — obvious at compile time:
- Local reasoning / guards — use a guard to establish a fact for a scope:
- Type-level contracts — express properties in the type and let the compiler check uses:
Note
In more advanced code, system properties and invariants (see
KW_REQUIRES,KW_ENSURES,KW_INVARIANT) can encode broader guarantees, which the compiler uses as facts within the relevant scope.
End‑to‑End: making division-by-zero impossible
Parameter List: full spec (informal)
Validation
-
The
CT
side only accepts
type/const
parameters (no
mut, no spreads). - The RT side accepts ordinary parameters.
-
A single
top‑level
;within the parentheses splits CT from RT. -
Style: omit an
empty
CT
(; ...)— prefer just( ... ).
Choosing Refinement vs. Dependent
Use refinement types when:
-
You’re constraining a familiar base type (
i32,string, a struct) with a predicate (n != 0,len(s) > 0). - You want to reuse existing APIs with stronger safety.
Use dependent types when:
- The shape of your result depends on inputs (lengths, indices, protocol states).
-
You have natural
compile-time parameters
(ie., a block size
N: int).
They compose well: dependent function types can return refinement types, and vice versa.
Error messages (ergonomics)
- CT/RT mixup : “Compile-time parameter list may only contain type/const parameters.”
-
Missing `;`
: If the first parameter looks like a type/const param (
TorN: int), suggest adding the;. - Unproven refinement : Point to the predicate and suggest guards or helper constructors to provide evidence.
FAQ
Q: Do I have to write CT parameters at call sites? A: Typically no — they’re inferred from RT arguments and expected return types. You can guide inference via annotations.
Q: Can I have multiple `;`?
A: No. There is at most one
top‑level
;
per parameter list.
Q: Are const parameters immutable? A: Yes. They are compile-time values; mutability doesn’t apply.
Q: What about async/pure/thread modifiers?
A: These work unchanged and apply to the function as a whole; the
;
split only affects parameter binding.
Worked examples (with
;
)
1) Identity with a type parameter
2) Map over a vector (type‑level only)
3) Concat with const lengths (conceptual)
4) Safe division with a refinement type
Function Types
Why functions feel like they have “colors” (and how Aela fixes it)
Many languages split the world into synchronous and asynchronous functions. That split tends to spread—call sites, types, libraries—until everything is “[colored][1].” Aela acknowledges that programs have different execution disciplines (purity, async, thread-safety), then solves the composition pain with first-class types , clear call rules , and built‑in intrinsics so you can cross boundaries intentionally and safely.
At a glance:
-
Three disciplines (colors):
pure,thread,async. - Simple call rules enforced by the checker.
- Intrinsic adapters (escape hatches) to cross colors when needed.
-
Contracts
(
requires/ensures) andsystemblocks to guard risky crossings.
The three disciplines
pure
- What it means: No observable side effects; referentially transparent.
- When to use: Deterministic computation, validation, transforms.
-
Gotchas:
Cannot call
asyncor impure functions. May be combined withthreadif the work is offloaded safely.
thread
- What it means: Safe to run off the main executor (e.g., in a worker). No ambient event‑loop assumptions; data must be sendable or shared safely.
- When to use: CPU‑bound work, blocking IO wrapped properly, parallelizable tasks.
-
Combines with:
pure(i.e.,pure thread fn).
async
-
What it means:
May suspend at
awaitpoints, returns a future/awaitable. - When to use: IO‑bound workflows, coordination with timers, event‑driven code.
-
Note:
asyncis notpure. It stands alone.
Call rules (the short version)
-
pure→ may call onlypure. -
thread→ may callthreadorpure; invoking athread fnyields aTaskhandle. -
uncolored
(impure) → may call
thread/puredirectly andasyncvia intrinsic. -
async→ mayawaitotherasyncfunctions orTaskhandles, and also callpure/threadwithout suspension. -
awaitis legal inside bothasync fnandthread fn, because awaiting aTaskjoin or future is permitted in either discipline.
Note
The checker gives precise diagnostics and suggests the right intrinsic when you cross a boundary.
Function types are first‑class
Aela’s type system includes function types with their discipline. These are expressed with refinement/dependent types instead of generics:
Function parameters can carry refinements on their arguments or return types, e.g.:
This makes intrinsics predictable and type‑safe, without requiring a generic system.
Crossing boundaries: standard intrinsics
To make crossing disciplines predictable and ergonomic, Aela ships intrinsics in the standard library:
These are typed adapters that the checker understands.
Signatures & typing rules (no generics required)
Aela doesn’t need parametric generics here—these intrinsics are schematic over types and use refinement/dependent predicates to express constraints. We write them informally as “for any types X, Y…”, and the checker discharges the side conditions.
`to_threaded` *
Checks:
argument and captured values must be sendable or
shared/atomic
. No reliance on an ambient event loop.
`to_async` *
Runtime model: schedules on the blocking/CPU pool; if the pool is saturated, the returned future suspends until capacity is available.
`to_blocking`
* (the common case inside
async
)
This is analogous to Rust Tokio’s `block_in_place`: it yields to the scheduler and executes on the blocking pool.
`block_on` * (drive async to completion from a worker thread)
Policy:
Forbidden on the event‑loop thread; prefer
to_async
/
to_blocking
when in async contexts.
`detach` / `join` *
Diagnostics you’ll see
- “Argument to `to_threaded` captures non‑sendable state Rc(T).”
- “`to_blocking` called from non‑async context.”
- “Pool saturated: `to_async` may suspend until space frees up.”
Examples
CPU work from async (don’t block the loop):
Submit a threaded task and await the handle:
Drive async to completion from a worker thread:
Lift pure compute to workers:
Use blocking work from async:
Use `to_async` to present sync/threaded as async:
Detached background task:
Idioms and examples
-
Keep core transforms
pure, wrap with intrinsics at the edges. -
Use
thread { ... }blocks for structured parallelism; all tasks inside must complete before exit. -
Use
std::selectto race multiple async sources and cancel losers automatically.
FFI without foot‑guns
Declare foreign functions with explicit disciplines and refinements:
Then wrap with
system
policies to guard where they are allowed:
Concurrency & mutability
-
Use
shared/atomictypes for cross‑thread data. -
threadfunctions may operate onsharedsafely. -
purefunctions must not mutate shared state.
Borrow Checker
Core Entities and Notation
-
Place
p: An lvalue, i.e. a path to a memory location. Examples:x,arr[i],s.field. In safe Aela, places are structured paths (no raw pointer deref, pointer arithmetic, or unions). -
Reference
&p/&mut p: A borrow of a place, producing a reference value. -
Loan
L(p, kind): The abstract fact thatpis borrowed, withkind ∈ {shared, unique}. -
Program Point
q: A location in the control-flow graph (CFG). -
Alive(L, q)
: Predicate meaning loan
Lis still valid at pointq(lexically-free semantics). - Operations :
-
reads(p, q): a read of placepatq. -
writes(p, q): a write to placepatq. -
moves(p, q): a move from placepatq. -
Aliases(x, p, q)
:
xholds a reference topatq(logical predicate, tracked via loan origins). -
Escape(r, q)
: Reference
rescapes its region atq(returned, stored, captured, etc.).
inter-procedural Region & Effect Summaries
-
Each function with references is internally elaborated to carry region variables
ρ. -
Example:
fn get_first(&[i32]) -> &i32elaborates tofor<ρ> fn(&ρ [i32]) -> &ρ i32. - Compiler emits a summary : region params, outlives constraints, and an escape set.
- Call sites instantiate these summaries and unify with argument regions. This provides modular checking without WPO.
-
Optional disambiguation syntax for multiple inputs:
fn pick(arr: &T as A, other: &T) -> &A T;ties return to param markerA.
Minimal Syntax Escape Hatch (as A)
Keeps everyday code lifetime-less, but allow explicit disambiguation when inference cannot decide.
-
Syntax:
Parameters may carry labels:
fn foo(x: &T as A) -> &A T. Returns may use&A. Struct fields may tie to labels. -
Labels introduce internal region vars (`ρ_A`)
that summaries use. Unlabeled references get fresh
ρ’s. - Elision rules: one input reference → auto-tie; multiple ambiguous inputs → require a label.
- Advanced usage:
-
Outlives constraints can be added:
fn foo(x: &T as A, y: &U as O) -> &A T where O : A;. -
Methods:
selfimplicitly labeledSelf, but can also be explicitly labeled:self: &Self as S. -
Diagnostics:
Role-based: “return value tied to param
a(label A) butadoes not live long enough.” Quick fixes: “Addas Ato param.”
Lifetimes in Data Structures
-
Reference fields implicitly carry region parameters.
struct Node { data: &T }elaborates tostruct Node<ρ, T> { data: &ρ T }. -
Construction instantiates
ρfrom the argument’s region. Error if struct outlives the referenced data.
Place Overlap (I3)
-
poverlapsp. -
poverlapsp.f. Distinct fields disjoint. -
arr[i]vsarr[j]: disjoint if indices are distinct compile-time constants; else conservative overlap. -
Library intrinsics like
split_at_mutprovide disjointness proofs via trusted contracts or runtime checks.
Escape Conditions (E1)
Reference escapes if it flows into a longer-lived region by:
- Returning from a function.
- Assigning to a longer-lived binding.
- Storing in struct/enum/global.
- Capturing by closure/future that outlives scope.
- Passing to FFI (unless contract says non-retaining).
- Storing in concurrent/shared cell accessible later.
- Erasure into longer-lived object/interface.
Asynchronous Code (async/await)
-
Phase 1:
Forbid loans across
awaitunless the origin outlives the entire future. Practically: locals cannot crossawait; only borrows from captured fields of the async task can. -
Phase 2:
Desugar
asyncto state machines and check across suspension points, enabling safe long-lived borrows.
Closures (FunctionExpression)
- Capture classification:
- Read-only → shared.
- Mutate → unique.
- Move → by-value.
- Trait mapping: shared → Fn; unique → FnMut; move → FnOnce.
- Escaping closures require captured regions to outlive closure region.
Refinement Types
-
Built-in predicates like
initialized(x),not_escaped(x)are decidable and do not require heavy SMT. - User-defined predicates and full logical refinement are measured to avoid compile-time blowups and underspecification.
Diagnostics Without Lifetime Names
-
Role-based regions: “returned reference must not outlive borrow of
arr.” - Highlight borrow creation, return site, and conflict.
- Optional symbolic labels (r1, r2) in error messages for clarity.
Rule-by-Rule Examples
Rule-by-Rule Examples (Concrete)
R0 — Implicit, Lexically-Free Regions
L1 — Loan Creation
L2 — Loan Propagation
A1 — Unique Exclusivity
A2 — Shared Read‑Only
I1 — Write Invalidates
I2 — Move Invalidates
I3 — Overlap/Fields
U1 — Use Requires Alive
U2 — No After‑Free
RB1 — Shared from Unique
RB2 — Unique from Unique (optional v1)
E1 — No Escaping Borrows
M1 — Binding Mutability
M2 — No Shared Mutability Without Atomics
T1 — Temporaries
T2 — Branches/Loops/Match
F1 — FFI Preconditions
F2 — FFI Postconditions
RFT1 — Refinement Well‑Formedness
RFT2 — Discharge at Use Sites
inter‑Procedural Summary (Elision)
Structs with Reference Fields (Implicit Regions)
Async Phase 1 — No Unique Loans Across Await
Closures — Capture Classification
RTOS Harmony
Linux Setup
You most likely need to edit your udev rules
If it doesn’t have your board’s rules in it, list your board to get the vendor and product id.
Replace ID VVVV:PPPP
Reload, re-enumerate
Unplug & Replug, press the reset button & then check:
Zephyr
Install
Install the Zephyr CLI tool
Allows you to flash your Arduino without needing to use sudo every time. It gives your user account permission to access the USB device.
Create a project directory. Create a python environment in the project directory. Initialize it as a new zephyr project. Run the update command to get all the sub-repos.
Go back to your projects root and get the minimal SDK for cross compiling.
Calculate the SHA sum if you want
Extract the SDK and run the setup
Initialize everything by entering your zephyr directory, activate the python env and set the environment (links the SDK you just got)
Compile & Flash Test
Finally, build an Arduino image. I use Blinky here because its super simple and makes sense to start with as a sanity test.
Integrating with Aela
Aela can output object files, so it's very simple to create a Zephyr project with Aela by just adding
CMakeLists.txt
and
prj.conf
files.
Example
prj.conf
Example
CMakeLists.txt
Compiling
Note
The _AE_ARM_OPTS trick copies exactly the -mcpu/-mfpu/-mfloat-abi/-mthumb flags Zephyr uses for this board, so your Aela object links ABI-cleanly without you guessing the right combo. (UNO R4 WiFi is a Cortex-M4; Zephyr’s board page confirms the arch.)
FreeRTOS
Discover Platform Build Flags
Lets assume an Arduino R4 here as an example.
We need to "spy" on the Arduino CLI to get the exact compiler and linker flags required for the R4 WiFi. The Aela build system will pass these directly to its underlyng C compiler. Arduino code files are called "sketches".
- Create a dummy sketch:
- Run a verbose compile:
-
Find and copy the linker command.
Scroll through the output and find the longest command, which will start with something like
.../arm-none-eabi-g++ .... It will be a very long line that links all the object files and libraries together. Copy this entire command into a text editor. We're going to use its flags.
It will look something like this (shortened for clarity):
.../arm-none-eabi-g++ -Os -Wl,--gc-sections -mcpu=cortex-m4 ... -L/path/to/build -Wl,--start-group -lArduinoCore -lrtos -lm -Wl,--end-group ...
We will now transfer the important parts of that command into your
index.json
.
Step 2: Project Structure
Organize your project with your source code inside a
src
directory.
Step 3: The
index.json
Manifest
This is the control center. We'll define a custom platform target called
"arduino_r4"
and place all the flags we discovered into the
link
section.
Create the file
`index.json`
(unless it was created via
aec init
):
Note
You must replace the placeholder paths (
/home/user/...,/tmp/arduino/...) with the full, absolute paths you copied from your verbose compiler output in Step 1.
Step 4: The Source Code
The source code is the same as before, but now it lives in the
src/
directory.
src/main.ae
src/shim.c
Step 5: Build and Flash
You tell the Aela compiler what you're targeting, and it uses the manifest to set the correct build flags.
Build the project for the target triple:
Flash the output. The build process should still result in a .uf2 file in a build directory. You can then flash it with the Arduino CLI as before.
FAQ
Why is Aela Commercial-Source?
Security
Transparency helps, but it’s not enough. Heartbleed, Log4Shell, and the xz backdoor landed in widely used open-source code. The gap isn’t visibility—it’s accountability, resourcing, and execution.
A commercial, closed-source model brings clear responsibility and funded security work:
Formal accountability
Commercial software runs under contracts and SLAs. When a vulnerability appears, a named company is on the hook—legally and financially—to fix it. In decentralized open source, responsibility often sits with volunteers without service commitments.
Dedicated, directed resources
We staff full-time security teams for audits, pen tests, and vulnerability management. Budget and people are assigned to unglamorous but critical maintenance and hardening that many projects lack.
Cohesive vision and focused dev
One organization sets architecture, roadmap, and tradeoffs. Decisions move faster and designs stay consistent. Large open-source efforts juggle many voices, which can slow delivery and blur design.
Choose the assurance model that fits your risk. Community stewardship offers transparency and shared responsibility. Our model adds contractual guarantees, professional assurance, and direct accountability backed by dedicated resources.
Cost
Look past sticker price to total cost of ownership (TCO).
Open source
TCO includes hiring in-house experts who contribute upstream to unblock features and address security issues.
Commercial source
We fold those operational costs into a predictable subscription or license. You get SLAs, legal indemnification, and dedicated support that map cleanly to enterprise procurement and risk frameworks.
In short: invest in internal capability and control, or buy a service-backed solution with predictable costs and clear responsibility.
Overview
Aela is a commercial-source language for embedded systems.
Aela is a compiled, memory-safe language with a strong, static type system. It started in 2018 as a fork of Rust, but has diverged significantly to service embedded systems use cases. It has a similar syntax and sticks to many of the same goals such as ownership semantics, memory and concurrency safety, only pay for what you use, and zero-cost abstractions.
Programming Language Landscape
This is where Aela fits In terms of Simplicity vs Complexity, and Familiarity vs Uniqueness.
Langauge Goals
All language design decisions revolve around determinism and performance for embedded systems.
Aela also focuses on harmony with C and C++. It offers automatic binding through header inclusion, packed and ordered structs, and inversion of control for system calls like
write(2)
.
Language Goals
All language design decisions revolve around providing absolute determinism, safety, and performance for embedded systems. Anything that compromises these goals through ambiguity, hidden costs, or non-local behavior is eliminated. All excluded features have an ideal alternative (see documentation) or have been simply omitted.
| Excluded Feature | Justification |
|---|---|
| Panics | Panics are not in the type signature; they violate the principle of abstraction and create a hidden, unrecoverable failure path that bypasses the type system. |
| Exceptions | Exceptions create hidden, non-local control flow paths that are impossible to reason about statically and incur runtime overhead. Error handling must be explicit. |
| Global State | Global variables create hidden dependencies and introduce non-determinism, making concurrency unsafe and program behavior impossible to reason about locally. |
| Lifetime Annotations | Explicit lifetime annotations are a significant source of complexity and syntactic noise. The compiler must be smart enough to infer ownership duration without burdening the programmer. |
| Macros | Macros create non-linear, hard-to-debug code flow ("magic"). Code must be explicit and analyzable; what you see is what the compiler gets, ensuring predictability and toolability. |
unsafe
keyword
|
The
unsafe
keyword is an escape hatch that undermines the entire memory safety guarantee of the language. Aela issues a handful of unsafe operations rather than entire blocks.
|
| Attributes | Attributes are a form of meta-programming that injects non-local behavior and hidden complexity. They modify code in non-obvious ways, violating the principle that code should be explicit. |
| Pointer Arithmetic | Direct pointer manipulation is a primary source of memory safety vulnerabilities (e.g., buffer overflows). Safe, bounds-checked slice operations are the only acceptable alternative. |
| Advanced Traits | Complex trait systems lead to baroque, unreadable type signatures and cryptic compiler errors. Simplicity and clarity are prioritized over abstract expressive power. |
| Inline Assembly | Inline assembly is non-portable, opaque to the optimizer, and breaks static analysis. Low-level routines must be provided via a stable C ABI, ensuring a clean contract. |
null
keyword
|
Null pointers are the source of countless runtime crashes. The absence of a value must be explicitly represented and handled in the type system (e.g., via
Option
).
|
| Operator Overloading | Operator overloading makes code ambiguous and hides the cost of operations. Function calls must be explicit to ensure code clarity and predictable performance. |
| Implicit Conversions | Implicit conversions hide potential bugs, such as data truncation or precision loss. All type conversions must be explicit, forcing the programmer to acknowledge the operation. |
Feature Comparison
| Feature | Aela | C | C++ | Rust | Ada |
|---|---|---|---|---|---|
| Compile-Time Memory Safety | √ | √ | √ | ||
| Cold Build Speed | √ | √ | √ | ||
| Reference Counted | √ | √ | |||
| Automatic RAII | √ | √ | |||
| Formal Verification | √ | √ | |||
| Dependent & Refinement Types | √ | ||||
| Language-Native Concurrency | √ | ||||
| Structured Concurrency | √ | √ | |||
| Automatic C/C++ safe binding | √ |
Module and Package System
Aela's module system is designed for organizing code into reusable and maintainable packages. The entire system is controlled by a manifest file named
index.json
.
Packages are precompiled binaries, this makes them fast, but they can also ship with the source code and that won't make them any slower. See the GUI module for a comprehensive example of a multi-platform module.
Package Manifest
Every Aela project is a package, and every package is defined by an
index.json
file at its root. This file tells the compiler everything it needs to know about your project.
Key Fields
- "name": The official name of your package (e.g., "my-app").
- "version": The version number (e.g., "0.1.0").
- "entry": The relative path to the main source file that acts as
- the entry point for compilation (e.g., "src/main.ae").
Example
Managing Dependencies
You can include other Aela packages in your project by listing them in the "dependencies" section of your
index.json
.
- The key is the name you will use to import the module (e.g., "ui").
-
The value is the relative path from your
index.jsonto the - dependency's root directory.
Example
Exports
By default, all functions, structs, and variables defined in a file are private to that file. To make a symbol visible to other modules, you must explicitly mark it with the
export
keyword.
Example
Re-exports
Imports
You use the
import
statement to bring exported symbols from other modules into the current file. Aela supports two forms of imports.
Namespace Import
This is the most common form. It imports all exported symbols from a module under a single namespace alias.
Syntax
Example
Named Imports
This allows you to import specific functions or types directly into the current scope, without needing a namespace qualifier.
Syntax
Example EXAMPLE
Advanced Build Configuration (FFI)
The
index.json
manifest also allows you to control the native build process, which is essential for linking against C, C++, or Objective-C code when using the Foreign Function Interface (FFI).
Key Sections
- "sources": A list of native source files (.c, .mm, etc.) to be
- compiled alongside your Aela code.
- "link": A list of flags to pass to the system linker (e.g., clang).
Notes
- Both "sources" and "link" flags can be specified per-platform
- (e.g., "darwin", "linux", "windows") or as "shared" for all platforms.
- The compiler automatically includes the linker flags from all packages
- listed in your "dependencies" section.
Example
This configuration tells the Aela compiler:
-
On macOS, compile the
src/native/apple.mmObjective-C++ file. - When linking the final executable, add flags to link against the
- Foundation, AppKit, and ObjectiveC system frameworks.
Failures
A primary cause of software defects, security vulnerabilities, and developer anxiety is the "Trust Gap": the difference between what a function's signature claims it does and what its implementation can actually do. Aela is designed to eliminate this gap. Its error and fault handling system is built on a single, non-negotiable principle: a function's signature must be a complete and honest contract, and the compiler must enforce it.
Don't Try-Catch
Traditional exception systems, common in languages like Java, JavaScript, C++, and Python, introduce a form of hidden control flow. A throw or raise statement is a non-local goto that is often invisible in the function's signature.
Consider a typical function in such a language:
function get_user(id: int) -> User
This signature makes a simple promise: "Give me an integer, and I will give you a User." However, the implementation might throw a DatabaseConnectionException, a UserNotFoundException, or a NullPointerException. To understand the function's true behavior, the developer must embark on a research project: reading the documentation (which may be out of date), reading the source code, and reading the source of every function it calls.
This breaks a developer's ability to reason locally about the code.
Don't Panic
Some modern languages, notably Rust, attempt to solve error handling this by creating a two-tiered system:
-
Recoverable Errors (
Result): For expected failures (e.g., file not found). These are part of the type signature. -
Unrecoverable Bugs (
panic!): For programmer errors (e.g., index out of bounds). These are not part of the type signature.
While an improvement, this still creates a hidden side-channel for bugs. More critically, the panic! mechanism creates a deep schism between platforms, especially for embedded systems.
The unwind vs. abort Schism:
On servers, panic! defaults to unwind, a slow and complex process that runs cleanup code (destructors). This adds a significant "tax" to the binary size, which is unacceptable on resource-constrained devices.
On embedded systems, developers are forced to configure panics to abort, which immediately halts the program.
The Broken Promise of abort: When panic = "abort" is used, the language's core safety promise—that resources will be cleaned up on failure (RAII via Drop in Rust)—is broken. Destructors are never called. A MutexGuard will leave a mutex permanently locked. A peripheral that was supposed to be disabled is left in an active state. The very code written to ensure safety on failure becomes useless.
This patched-together model is not a first-principles solution. Aela requires a single, unified system that is safe, deterministic, and efficient on all platforms.
The Fault Model
The Principle of the Honest Contract: Aela avoids ambiguity in failure handling by enforcing that the outcome of a function must be encoded in its signature.
Function Signatures
A function declares its potential to terminate due to an unrecoverable logic bug by using the
|
operator in its return type. This operator separates the single success type from a list of one or more
fault
types.
fn get_at(slice: &[u8], index: int) -> u8 | OutOfBounds;
This signature is an honest contract. It tells any caller: "This function will either return a
u8
, or it will terminate with an
OutOfBounds
fault. There are no other possibilities."
The
fault
and
raise
Keywords
-
`fault`
: A keyword used to declare a type that represents a logic bug or contract violation. This distinguishes it from
structorenum, which represent data and recoverable errors. -
fault OutOfBounds(index: int, len: int); -
`raise`
: A keyword that triggers a fault. It immediately stops the current function's execution and propagates the fault to the caller.
raiseis considered a terminal action. -
raise OutOfBounds(index: index, len: len);
The Standard
match
Statement
The
match
statement is how Aela handles the outcome of functions that may terminate with faults.
Semantic Rules
The safety and special behavior of fault handling are not derived from the syntax, but from a single, powerful semantic rule in the compiler:
If the expression being evaluated by a
matchstatement has a fault path in its signature (i.e., contains a|), then the compiler enforces a "Terminal Arm Rule" on any arm that matches afault-declared type.
This rule is context-dependent. It is triggered by the signature of the function being called, not just the type of the pattern. This is the key insight that resolves all ambiguity.
The "Terminal Arm Rule" is defined as: The block of the arm must end with a terminal statement. Aela does not have implicit returns, so this is a direct check of the final statement in the block.
Terminal statements are:
-
raise; -
std::abort(); -
std::halt(); -
std::reboot();
Note
purefn ketword must not introduce or handle faults; they cannot have a | in their return type and cannot raise.
Note
Faults must not cross FFI; board/FFI shims must convert faults to domain-appropriate codes or terminals.
Note
AP131outlines a proposal to havereturnbe the equivalent ofraise. Allowing both and specifying that they’re identical.
Note
AP132outlines a proposal to permit functions to be generic over an open set of faults (variadic type param), so adapters don’t re-enumerate: ie: fn map(f: fn(T) -> U | E..., x: T) -> U | E...;
Note
AP134outlines a proposal to add a desugaring operator. let b = get_at(xs, 0)?; // expands to match/raise
Complete Examples: The Rule in Practice
These two examples demonstrate how the context-dependent rule creates a safe and unambiguous system.
Example 1: Handling a Live Fault Event
If
reboot;
were omitted, the compiler would issue a clear error:
"Fault-handling arm must end with a terminal statement."
Example 2: Inspecting a Fault as Data
Memory & Mutability
1. Default Allocation Semantics
- All values start on the stack by default.
- Stack allocations are:
- Fast
- Scoped
- Owned directly by the binding.
2. Heap Allocation
To allocate a value on the heap, use the
new
keyword with one of the following modifiers:
Syntax
All heap allocatations are reference counted.
Behavior of
new
Modifiers
| Modifier | Behavior | Description |
|---|---|---|
(none)
|
Heap Allocation (OS based allocator). | Returns a handle/reference to a freshly allocated object, immutable and can not be loaned. |
shared
|
Heap. Shared ownership, single-threaded. | Allowed to be loaned; not thread-safe; not Send/Sync-like. |
shared atomic
|
Heap. Shared ownership, thread-safe. | Reference counts and interior coordination use atomics; Send/Sync if T satisfies the “atomic-safe” requirements you define. |
weak
|
Heap, weak handle to a shared object. | Does not keep the allocation alive; must be paired with at least one strong shared/shared atomic owner somewhere to be useful. |
static
|
Static storage, no OS allocator. | Object is placed/constructed in static memory (BSS/RODATA/flash/SECTION), suitable for bare-metal/MCUs. Lifetime: program-long (or until explicitly torn down by system semantics). |
static atomic
|
Static storage, thread-safe coordination via atomics. | Shared global counters updated in ISR + mainline, Lock-free buffers (ring buffers for UART, DMA, etc.). Configuration/state variables read by multiple cores or ISRs. On a simple single-core MCU with no interrupts, it’s redundant but harmless. |
-
new shared: -
Performs a
copy
of the stack value (
{ ...val }) - Allocates it on the heap
- Wraps it in a reference-counted container (non-atomic)
-
Returns a reference:
&T
-
new shared atomic: - Same as above, but uses atomic reference counting (thread-safe)
3. Mutability Semantics
Mutability is determined only by the binding keyword :
| Keyword | Meaning |
|---|---|
let
|
Immutable binding (read-only) |
var
|
Mutable binding (read-write) |
-
The
mutkeyword is not necesasry here. - Mutability is not encoded in types, structs, or fields.
-
letandvarare both lexically scoped.
Example
Note
Attempting to mutate a
let-bound reference results in a compile-time error!
4. Weak References & Cycle Prevention
To solve the problem of reference cycles (e.g., a parent refers to a child, and the child refers back to the parent), Aela will provide weak references. A weak reference is a non-owning pointer that does not prevent an object from being deallocated.
The system is designed to be minimal and explicit, consisting of three parts:
The weak &T Type
A weak reference has a distinct type to ensure compile-time safety. This allows the compiler to enforce correct usage.
The weak() Downgrade Function
To create a weak reference, you must use the explicit, built-in weak() function. This makes the intent clear and avoids implicit "magic".
let strong_ref: &Foo = new shared { ... };
// Explicitly create a weak reference from a strong one. let weak_ref: weak &Foo = weak(strong_ref);
The if let Upgrade Pattern
Accessing the object behind a weak reference is inherently optional, as the object may have been deallocated. Aela enforces safe access through a conditional if let binding.
// Given a variable 'weak_ref' of type 'weak &Foo'
5. Copying Stack Values
- Heap allocation requires copying the stack value:
- This avoids moving ownership from the stack. Moves are not allowed .
5. Reference Types and Behavior
| Kind | Syntax | RC Type | Thread-Safe | Mutable |
|---|---|---|---|---|
| Stack value |
let x: T = ...
|
None | N/A | No |
| Heap reference |
let x: &T = new shared {...}
|
RC | No | No |
| Heap reference (mutable) |
var x: &T = new shared {...}
|
RC | No | Yes |
| Heap reference (atomic) |
let/var x: &T = new shared atomic {}
|
ARC | Yes | Depends |
6. Compile-Time Analysis
- Safe aliasing of references
-
Proper use of
var(exclusive mutation) - Reference count tracking correctness
- No runtime borrow errors required
The compiler performs static analysis to ensure:
7. No Implicit Moves from Stack
- Stack values cannot be moved to the heap.
-
Heap promotion always requires a
copy
using
{ ...val }. - new shared { ...std::move(x) } is on the roadmap
Calling Conventions
How variables are allocated and passed to functions. The 'new' keyword is the explicit signal for heap allocation and reference counting.
Primitives & Simple Structs (Stack Allocated)
- Rule: Variables declared WITHOUT the 'new' keyword live on the stack.
- In Memory: The variable holds the data directly.
- Function Passing: Passed BY VALUE (a full copy is made).
- Lifetime: Automatic (destroyed when the variable goes out of scope).
- Reference Counted: No.
Boxed Values (Heap Allocated via 'new')
- Rule: Variables initialized WITH the 'new' keyword are allocated on the heap.
- In Memory: The variable holds a POINTER to a "box" on the heap.
- Function Passing: Passed BY REFERENCE (a copy of the pointer is made).
- Lifetime: Managed (Reference Counted).
- Reference Counted: Yes.
Closures (Special Case)
- Rule: A closure that captures variables has its environment allocated on the heap.
-
In Memory: The closure variable is a
{func_ptr, env_ptr}struct. Theenv_ptrpoints to a heap-allocated box containing the captured variables. -
Function Passing: The
{func_ptr, env_ptr}struct itself is small and is passed BY VALUE. - Lifetime: The environment's lifetime is managed by Reference Counting.
Refinement & Dependent Types
TL;DR: Refinement & dependent types are types that embed logical conditions or values themselves, making illegal states unrepresentable by construction.
If you're coming from languages like JavaScript, C++, or Rust, you're used to type systems that check the
shape
of your data. For example, a type checker ensures you don't use a
string
where a
number
is expected.
Aela takes this a step further with refinement types and dependent types . These are features that allow the type system to understand and check the values of your data, not just their shape. It's more rigorous that shape checking, it's a value-aware check that happens entirely at compile time .
Motivation
Most type systems validate
shape
(ie., "this is an
int
"). Aela also validates
value-level facts
: a string is
non-empty
, an integer is
non-zero
, a vector’s
length
matches what your function promises. This turns bugs into
compile‑time errors
and makes code self-documenting.
- Earlier feedback: logical mistakes become type errors you see during compilation.
-
Stronger intent:
types like
NonEmptyStringandNonZerointtell the reader (and the compiler) exactly what you mean. - Confidence: fewer defensive checks sprinkled across your code.
Compile-Time VS. Run-Time Arguments
Aela uses a
single
parameter list for functions, with an optional
compile-time section
separated from the
run-time
section by a semicolon
;
. Parameters are separated by commas. If a parameter has a type annotation, it will be considered a value parameter. If a value has no type annotation, it will be considered a type.
Rules
-
Tokens
before
;are compile-time parameters (type or const/value-level), -
Type parameters:
bare identifiers (ie.,
T,U). -
Const parameters:
Name: Typeform (ie.,N: int). -
Tokens
after
;are run-time parameters (the usualname: Type, with optionalmut/ spread, per language rules). -
If there’s
no
;, the entire list is treated as run-time parameters. -
If the CT part is
empty
, omit
;(preferred style). If the RT part is empty and CT is present, keep a trailing;.
Refinement Types: add a
where
-clause to any base type
A refinement type is a base type with a logical predicate.
Syntax
:
{ id: Type where predicate }
Use refinements when you want value-aware validation while keeping the underlying representation.
Note
The compiler statically checks predicates where it can (literals, constant expressions, and facts learned from prior code/contracts). When a predicate can’t be decided statically, you’ll provide evidence (see “Proving facts to the compiler”).
Dependent Types: when types mention values
A dependent type is a type that depends on values .
A simple, practical pattern is to refine a function’s return by its inputs :
Here the return type depends on the run-time values
a
and
b
. The compiler enforces this contract at compile time wherever it can be proven, and narrows follow-up reasoning.
Another common pattern uses compile-time const parameters to index types:
-
Tis a type parameter . -
MandNare const parameters (compile-time integers). -
The result type’s length
computes
to
M + Nat the type level .
Proving facts to the compiler
There are three common ways to convince the compiler of a refinement:
- Literals and constant expressions — obvious at compile time:
- Local reasoning / guards — use a guard to establish a fact for a scope:
- Type-level contracts — express properties in the type and let the compiler check uses:
Note
In more advanced code, system properties and invariants (see
KW_REQUIRES,KW_ENSURES,KW_INVARIANT) can encode broader guarantees, which the compiler uses as facts within the relevant scope.
End‑to‑End: making division-by-zero impossible
Parameter List: full spec (informal)
Validation
-
The
CT
side only accepts
type/const
parameters (no
mut, no spreads). - The RT side accepts ordinary parameters.
-
A single
top‑level
;within the parentheses splits CT from RT. -
Style: omit an
empty
CT
(; ...)— prefer just( ... ).
Choosing Refinement vs. Dependent
Use refinement types when:
-
You’re constraining a familiar base type (
i32,string, a struct) with a predicate (n != 0,len(s) > 0). - You want to reuse existing APIs with stronger safety.
Use dependent types when:
- The shape of your result depends on inputs (lengths, indices, protocol states).
-
You have natural
compile-time parameters
(ie., a block size
N: int).
They compose well: dependent function types can return refinement types, and vice versa.
Error messages (ergonomics)
- CT/RT mixup : “Compile-time parameter list may only contain type/const parameters.”
-
Missing `;`
: If the first parameter looks like a type/const param (
TorN: int), suggest adding the;. - Unproven refinement : Point to the predicate and suggest guards or helper constructors to provide evidence.
FAQ
Q: Do I have to write CT parameters at call sites? A: Typically no — they’re inferred from RT arguments and expected return types. You can guide inference via annotations.
Q: Can I have multiple `;`?
A: No. There is at most one
top‑level
;
per parameter list.
Q: Are const parameters immutable? A: Yes. They are compile-time values; mutability doesn’t apply.
Q: What about async/pure/thread modifiers?
A: These work unchanged and apply to the function as a whole; the
;
split only affects parameter binding.
Worked examples (with
;
)
1) Identity with a type parameter
2) Map over a vector (type‑level only)
3) Concat with const lengths (conceptual)
4) Safe division with a refinement type
Function Types
Why functions feel like they have “colors” (and how Aela fixes it)
Many languages split the world into synchronous and asynchronous functions. That split tends to spread—call sites, types, libraries—until everything is “[colored][1].” Aela acknowledges that programs have different execution disciplines (purity, async, thread-safety), then solves the composition pain with first-class types , clear call rules , and built‑in intrinsics so you can cross boundaries intentionally and safely.
At a glance:
-
Three disciplines (colors):
pure,thread,async. - Simple call rules enforced by the checker.
- Intrinsic adapters (escape hatches) to cross colors when needed.
-
Contracts
(
requires/ensures) andsystemblocks to guard risky crossings.
The three disciplines
pure
- What it means: No observable side effects; referentially transparent.
- When to use: Deterministic computation, validation, transforms.
-
Gotchas:
Cannot call
asyncor impure functions. May be combined withthreadif the work is offloaded safely.
thread
- What it means: Safe to run off the main executor (e.g., in a worker). No ambient event‑loop assumptions; data must be sendable or shared safely.
- When to use: CPU‑bound work, blocking IO wrapped properly, parallelizable tasks.
-
Combines with:
pure(i.e.,pure thread fn).
async
-
What it means:
May suspend at
awaitpoints, returns a future/awaitable. - When to use: IO‑bound workflows, coordination with timers, event‑driven code.
-
Note:
asyncis notpure. It stands alone.
Call rules (the short version)
-
pure→ may call onlypure. -
thread→ may callthreadorpure; invoking athread fnyields aTaskhandle. -
uncolored
(impure) → may call
thread/puredirectly andasyncvia intrinsic. -
async→ mayawaitotherasyncfunctions orTaskhandles, and also callpure/threadwithout suspension. -
awaitis legal inside bothasync fnandthread fn, because awaiting aTaskjoin or future is permitted in either discipline.
Note
The checker gives precise diagnostics and suggests the right intrinsic when you cross a boundary.
Function types are first‑class
Aela’s type system includes function types with their discipline. These are expressed with refinement/dependent types instead of generics:
Function parameters can carry refinements on their arguments or return types, e.g.:
This makes intrinsics predictable and type‑safe, without requiring a generic system.
Crossing boundaries: standard intrinsics
To make crossing disciplines predictable and ergonomic, Aela ships intrinsics in the standard library:
These are typed adapters that the checker understands.
Signatures & typing rules (no generics required)
Aela doesn’t need parametric generics here—these intrinsics are schematic over types and use refinement/dependent predicates to express constraints. We write them informally as “for any types X, Y…”, and the checker discharges the side conditions.
`to_threaded` *
Checks:
argument and captured values must be sendable or
shared/atomic
. No reliance on an ambient event loop.
`to_async` *
Runtime model: schedules on the blocking/CPU pool; if the pool is saturated, the returned future suspends until capacity is available.
`to_blocking`
* (the common case inside
async
)
This is analogous to Rust Tokio’s `block_in_place`: it yields to the scheduler and executes on the blocking pool.
`block_on` * (drive async to completion from a worker thread)
Policy:
Forbidden on the event‑loop thread; prefer
to_async
/
to_blocking
when in async contexts.
`detach` / `join` *
Diagnostics you’ll see
- “Argument to `to_threaded` captures non‑sendable state Rc(T).”
- “`to_blocking` called from non‑async context.”
- “Pool saturated: `to_async` may suspend until space frees up.”
Examples
CPU work from async (don’t block the loop):
Submit a threaded task and await the handle:
Drive async to completion from a worker thread:
Lift pure compute to workers:
Use blocking work from async:
Use `to_async` to present sync/threaded as async:
Detached background task:
Idioms and examples
-
Keep core transforms
pure, wrap with intrinsics at the edges. -
Use
thread { ... }blocks for structured parallelism; all tasks inside must complete before exit. -
Use
std::selectto race multiple async sources and cancel losers automatically.
FFI without foot‑guns
Declare foreign functions with explicit disciplines and refinements:
Then wrap with
system
policies to guard where they are allowed:
Concurrency & mutability
-
Use
shared/atomictypes for cross‑thread data. -
threadfunctions may operate onsharedsafely. -
purefunctions must not mutate shared state.
Borrow Checker
Core Entities and Notation
-
Place
p: An lvalue, i.e. a path to a memory location. Examples:x,arr[i],s.field. In safe Aela, places are structured paths (no raw pointer deref, pointer arithmetic, or unions). -
Reference
&p/&mut p: A borrow of a place, producing a reference value. -
Loan
L(p, kind): The abstract fact thatpis borrowed, withkind ∈ {shared, unique}. -
Program Point
q: A location in the control-flow graph (CFG). -
Alive(L, q)
: Predicate meaning loan
Lis still valid at pointq(lexically-free semantics). - Operations :
-
reads(p, q): a read of placepatq. -
writes(p, q): a write to placepatq. -
moves(p, q): a move from placepatq. -
Aliases(x, p, q)
:
xholds a reference topatq(logical predicate, tracked via loan origins). -
Escape(r, q)
: Reference
rescapes its region atq(returned, stored, captured, etc.).
inter-procedural Region & Effect Summaries
-
Each function with references is internally elaborated to carry region variables
ρ. -
Example:
fn get_first(&[i32]) -> &i32elaborates tofor<ρ> fn(&ρ [i32]) -> &ρ i32. - Compiler emits a summary : region params, outlives constraints, and an escape set.
- Call sites instantiate these summaries and unify with argument regions. This provides modular checking without WPO.
-
Optional disambiguation syntax for multiple inputs:
fn pick(arr: &T as A, other: &T) -> &A T;ties return to param markerA.
Minimal Syntax Escape Hatch (as A)
Keeps everyday code lifetime-less, but allow explicit disambiguation when inference cannot decide.
-
Syntax:
Parameters may carry labels:
fn foo(x: &T as A) -> &A T. Returns may use&A. Struct fields may tie to labels. -
Labels introduce internal region vars (`ρ_A`)
that summaries use. Unlabeled references get fresh
ρ’s. - Elision rules: one input reference → auto-tie; multiple ambiguous inputs → require a label.
- Advanced usage:
-
Outlives constraints can be added:
fn foo(x: &T as A, y: &U as O) -> &A T where O : A;. -
Methods:
selfimplicitly labeledSelf, but can also be explicitly labeled:self: &Self as S. -
Diagnostics:
Role-based: “return value tied to param
a(label A) butadoes not live long enough.” Quick fixes: “Addas Ato param.”
Lifetimes in Data Structures
-
Reference fields implicitly carry region parameters.
struct Node { data: &T }elaborates tostruct Node<ρ, T> { data: &ρ T }. -
Construction instantiates
ρfrom the argument’s region. Error if struct outlives the referenced data.
Place Overlap (I3)
-
poverlapsp. -
poverlapsp.f. Distinct fields disjoint. -
arr[i]vsarr[j]: disjoint if indices are distinct compile-time constants; else conservative overlap. -
Library intrinsics like
split_at_mutprovide disjointness proofs via trusted contracts or runtime checks.
Escape Conditions (E1)
Reference escapes if it flows into a longer-lived region by:
- Returning from a function.
- Assigning to a longer-lived binding.
- Storing in struct/enum/global.
- Capturing by closure/future that outlives scope.
- Passing to FFI (unless contract says non-retaining).
- Storing in concurrent/shared cell accessible later.
- Erasure into longer-lived object/interface.
Asynchronous Code (async/await)
-
Phase 1:
Forbid loans across
awaitunless the origin outlives the entire future. Practically: locals cannot crossawait; only borrows from captured fields of the async task can. -
Phase 2:
Desugar
asyncto state machines and check across suspension points, enabling safe long-lived borrows.
Closures (FunctionExpression)
- Capture classification:
- Read-only → shared.
- Mutate → unique.
- Move → by-value.
- Trait mapping: shared → Fn; unique → FnMut; move → FnOnce.
- Escaping closures require captured regions to outlive closure region.
Refinement Types
-
Built-in predicates like
initialized(x),not_escaped(x)are decidable and do not require heavy SMT. - User-defined predicates and full logical refinement are measured to avoid compile-time blowups and underspecification.
Diagnostics Without Lifetime Names
-
Role-based regions: “returned reference must not outlive borrow of
arr.” - Highlight borrow creation, return site, and conflict.
- Optional symbolic labels (r1, r2) in error messages for clarity.
Rule-by-Rule Examples
Rule-by-Rule Examples (Concrete)
R0 — Implicit, Lexically-Free Regions
L1 — Loan Creation
L2 — Loan Propagation
A1 — Unique Exclusivity
A2 — Shared Read‑Only
I1 — Write Invalidates
I2 — Move Invalidates
I3 — Overlap/Fields
U1 — Use Requires Alive
U2 — No After‑Free
RB1 — Shared from Unique
RB2 — Unique from Unique (optional v1)
E1 — No Escaping Borrows
M1 — Binding Mutability
M2 — No Shared Mutability Without Atomics
T1 — Temporaries
T2 — Branches/Loops/Match
F1 — FFI Preconditions
F2 — FFI Postconditions
RFT1 — Refinement Well‑Formedness
RFT2 — Discharge at Use Sites
inter‑Procedural Summary (Elision)
Structs with Reference Fields (Implicit Regions)
Async Phase 1 — No Unique Loans Across Await
Closures — Capture Classification
RTOS Harmony
Linux Setup
You most likely need to edit your udev rules
If it doesn’t have your board’s rules in it, list your board to get the vendor and product id.
Replace ID VVVV:PPPP
Reload, re-enumerate
Unplug & Replug, press the reset button & then check:
Zephyr
Install
Install the Zephyr CLI tool
Allows you to flash your Arduino without needing to use sudo every time. It gives your user account permission to access the USB device.
Create a project directory. Create a python environment in the project directory. Initialize it as a new zephyr project. Run the update command to get all the sub-repos.
Go back to your projects root and get the minimal SDK for cross compiling.
Calculate the SHA sum if you want
Extract the SDK and run the setup
Initialize everything by entering your zephyr directory, activate the python env and set the environment (links the SDK you just got)
Compile & Flash Test
Finally, build an Arduino image. I use Blinky here because its super simple and makes sense to start with as a sanity test.
Integrating with Aela
Aela can output object files, so it's very simple to create a Zephyr project with Aela by just adding
CMakeLists.txt
and
prj.conf
files.
Example
prj.conf
Example
CMakeLists.txt
Compiling
Note
The _AE_ARM_OPTS trick copies exactly the -mcpu/-mfpu/-mfloat-abi/-mthumb flags Zephyr uses for this board, so your Aela object links ABI-cleanly without you guessing the right combo. (UNO R4 WiFi is a Cortex-M4; Zephyr’s board page confirms the arch.)
FreeRTOS
Discover Platform Build Flags
Lets assume an Arduino R4 here as an example.
We need to "spy" on the Arduino CLI to get the exact compiler and linker flags required for the R4 WiFi. The Aela build system will pass these directly to its underlyng C compiler. Arduino code files are called "sketches".
- Create a dummy sketch:
- Run a verbose compile:
-
Find and copy the linker command.
Scroll through the output and find the longest command, which will start with something like
.../arm-none-eabi-g++ .... It will be a very long line that links all the object files and libraries together. Copy this entire command into a text editor. We're going to use its flags.
It will look something like this (shortened for clarity):
.../arm-none-eabi-g++ -Os -Wl,--gc-sections -mcpu=cortex-m4 ... -L/path/to/build -Wl,--start-group -lArduinoCore -lrtos -lm -Wl,--end-group ...
We will now transfer the important parts of that command into your
index.json
.
Step 2: Project Structure
Organize your project with your source code inside a
src
directory.
Step 3: The
index.json
Manifest
This is the control center. We'll define a custom platform target called
"arduino_r4"
and place all the flags we discovered into the
link
section.
Create the file
`index.json`
(unless it was created via
aec init
):
Note
You must replace the placeholder paths (
/home/user/...,/tmp/arduino/...) with the full, absolute paths you copied from your verbose compiler output in Step 1.
Step 4: The Source Code
The source code is the same as before, but now it lives in the
src/
directory.
src/main.ae
src/shim.c
Step 5: Build and Flash
You tell the Aela compiler what you're targeting, and it uses the manifest to set the correct build flags.
Build the project for the target triple:
Flash the output. The build process should still result in a .uf2 file in a build directory. You can then flash it with the Arduino CLI as before.
FAQ
Why is Aela Commercial-Source?
Security
Transparency helps, but it’s not enough. Heartbleed, Log4Shell, and the xz backdoor landed in widely used open-source code. The gap isn’t visibility—it’s accountability, resourcing, and execution.
A commercial, closed-source model brings clear responsibility and funded security work:
Formal accountability
Commercial software runs under contracts and SLAs. When a vulnerability appears, a named company is on the hook—legally and financially—to fix it. In decentralized open source, responsibility often sits with volunteers without service commitments.
Dedicated, directed resources
We staff full-time security teams for audits, pen tests, and vulnerability management. Budget and people are assigned to unglamorous but critical maintenance and hardening that many projects lack.
Cohesive vision and focused dev
One organization sets architecture, roadmap, and tradeoffs. Decisions move faster and designs stay consistent. Large open-source efforts juggle many voices, which can slow delivery and blur design.
Choose the assurance model that fits your risk. Community stewardship offers transparency and shared responsibility. Our model adds contractual guarantees, professional assurance, and direct accountability backed by dedicated resources.
Cost
Look past sticker price to total cost of ownership (TCO).
Open source
TCO includes hiring in-house experts who contribute upstream to unblock features and address security issues.
Commercial source
We fold those operational costs into a predictable subscription or license. You get SLAs, legal indemnification, and dedicated support that map cleanly to enterprise procurement and risk frameworks.
In short: invest in internal capability and control, or buy a service-backed solution with predictable costs and clear responsibility.
Overview
Aela is a commercial-source language for embedded systems.
Aela is a compiled, memory-safe language with a strong, static type system. It started in 2018 as a fork of Rust, but has diverged significantly to service embedded systems use cases. It has a similar syntax and sticks to many of the same goals such as ownership semantics, memory and concurrency safety, only pay for what you use, and zero-cost abstractions.
Programming Language Landscape
This is where Aela fits In terms of Simplicity vs Complexity, and Familiarity vs Uniqueness.
Langauge Goals
All language design decisions revolve around determinism and performance for embedded systems.
Aela also focuses on harmony with C and C++. It offers automatic binding through header inclusion, packed and ordered structs, and inversion of control for system calls like
write(2)
.
Language Goals
All language design decisions revolve around providing absolute determinism, safety, and performance for embedded systems. Anything that compromises these goals through ambiguity, hidden costs, or non-local behavior is eliminated. All excluded features have an ideal alternative (see documentation) or have been simply omitted.
| Excluded Feature | Justification |
|---|---|
| Panics | Panics are not in the type signature; they violate the principle of abstraction and create a hidden, unrecoverable failure path that bypasses the type system. |
| Exceptions | Exceptions create hidden, non-local control flow paths that are impossible to reason about statically and incur runtime overhead. Error handling must be explicit. |
| Global State | Global variables create hidden dependencies and introduce non-determinism, making concurrency unsafe and program behavior impossible to reason about locally. |
| Lifetime Annotations | Explicit lifetime annotations are a significant source of complexity and syntactic noise. The compiler must be smart enough to infer ownership duration without burdening the programmer. |
| Macros | Macros create non-linear, hard-to-debug code flow ("magic"). Code must be explicit and analyzable; what you see is what the compiler gets, ensuring predictability and toolability. |
unsafe
keyword
|
The
unsafe
keyword is an escape hatch that undermines the entire memory safety guarantee of the language. Aela issues a handful of unsafe operations rather than entire blocks.
|
| Attributes | Attributes are a form of meta-programming that injects non-local behavior and hidden complexity. They modify code in non-obvious ways, violating the principle that code should be explicit. |
| Pointer Arithmetic | Direct pointer manipulation is a primary source of memory safety vulnerabilities (e.g., buffer overflows). Safe, bounds-checked slice operations are the only acceptable alternative. |
| Advanced Traits | Complex trait systems lead to baroque, unreadable type signatures and cryptic compiler errors. Simplicity and clarity are prioritized over abstract expressive power. |
| Inline Assembly | Inline assembly is non-portable, opaque to the optimizer, and breaks static analysis. Low-level routines must be provided via a stable C ABI, ensuring a clean contract. |
null
keyword
|
Null pointers are the source of countless runtime crashes. The absence of a value must be explicitly represented and handled in the type system (e.g., via
Option
).
|
| Operator Overloading | Operator overloading makes code ambiguous and hides the cost of operations. Function calls must be explicit to ensure code clarity and predictable performance. |
| Implicit Conversions | Implicit conversions hide potential bugs, such as data truncation or precision loss. All type conversions must be explicit, forcing the programmer to acknowledge the operation. |
Feature Comparison
| Feature | Aela | C | C++ | Rust | Ada |
|---|---|---|---|---|---|
| Compile-Time Memory Safety | √ | √ | √ | ||
| Cold Build Speed | √ | √ | √ | ||
| Reference Counted | √ | √ | |||
| Automatic RAII | √ | √ | |||
| Formal Verification | √ | √ | |||
| Dependent & Refinement Types | √ | ||||
| Language-Native Concurrency | √ | ||||
| Structured Concurrency | √ | √ | |||
| Automatic C/C++ safe binding | √ |
Module and Package System
Aela's module system is designed for organizing code into reusable and maintainable packages. The entire system is controlled by a manifest file named
index.json
.
Packages are precompiled binaries, this makes them fast, but they can also ship with the source code and that won't make them any slower. See the GUI module for a comprehensive example of a multi-platform module.
Package Manifest
Every Aela project is a package, and every package is defined by an
index.json
file at its root. This file tells the compiler everything it needs to know about your project.
Key Fields
- "name": The official name of your package (e.g., "my-app").
- "version": The version number (e.g., "0.1.0").
- "entry": The relative path to the main source file that acts as
- the entry point for compilation (e.g., "src/main.ae").
Example
Managing Dependencies
You can include other Aela packages in your project by listing them in the "dependencies" section of your
index.json
.
- The key is the name you will use to import the module (e.g., "ui").
-
The value is the relative path from your
index.jsonto the - dependency's root directory.
Example
Exports
By default, all functions, structs, and variables defined in a file are private to that file. To make a symbol visible to other modules, you must explicitly mark it with the
export
keyword.
Example
Re-exports
Imports
You use the
import
statement to bring exported symbols from other modules into the current file. Aela supports two forms of imports.
Namespace Import
This is the most common form. It imports all exported symbols from a module under a single namespace alias.
Syntax
Example
Named Imports
This allows you to import specific functions or types directly into the current scope, without needing a namespace qualifier.
Syntax
Example EXAMPLE
Advanced Build Configuration (FFI)
The
index.json
manifest also allows you to control the native build process, which is essential for linking against C, C++, or Objective-C code when using the Foreign Function Interface (FFI).
Key Sections
- "sources": A list of native source files (.c, .mm, etc.) to be
- compiled alongside your Aela code.
- "link": A list of flags to pass to the system linker (e.g., clang).
Notes
- Both "sources" and "link" flags can be specified per-platform
- (e.g., "darwin", "linux", "windows") or as "shared" for all platforms.
- The compiler automatically includes the linker flags from all packages
- listed in your "dependencies" section.
Example
This configuration tells the Aela compiler:
-
On macOS, compile the
src/native/apple.mmObjective-C++ file. - When linking the final executable, add flags to link against the
- Foundation, AppKit, and ObjectiveC system frameworks.
Failures
A primary cause of software defects, security vulnerabilities, and developer anxiety is the "Trust Gap": the difference between what a function's signature claims it does and what its implementation can actually do. Aela is designed to eliminate this gap. Its error and fault handling system is built on a single, non-negotiable principle: a function's signature must be a complete and honest contract, and the compiler must enforce it.
Don't Try-Catch
Traditional exception systems, common in languages like Java, JavaScript, C++, and Python, introduce a form of hidden control flow. A throw or raise statement is a non-local goto that is often invisible in the function's signature.
Consider a typical function in such a language:
function get_user(id: int) -> User
This signature makes a simple promise: "Give me an integer, and I will give you a User." However, the implementation might throw a DatabaseConnectionException, a UserNotFoundException, or a NullPointerException. To understand the function's true behavior, the developer must embark on a research project: reading the documentation (which may be out of date), reading the source code, and reading the source of every function it calls.
This breaks a developer's ability to reason locally about the code.
Don't Panic
Some modern languages, notably Rust, attempt to solve error handling this by creating a two-tiered system:
-
Recoverable Errors (
Result): For expected failures (e.g., file not found). These are part of the type signature. -
Unrecoverable Bugs (
panic!): For programmer errors (e.g., index out of bounds). These are not part of the type signature.
While an improvement, this still creates a hidden side-channel for bugs. More critically, the panic! mechanism creates a deep schism between platforms, especially for embedded systems.
The unwind vs. abort Schism:
On servers, panic! defaults to unwind, a slow and complex process that runs cleanup code (destructors). This adds a significant "tax" to the binary size, which is unacceptable on resource-constrained devices.
On embedded systems, developers are forced to configure panics to abort, which immediately halts the program.
The Broken Promise of abort: When panic = "abort" is used, the language's core safety promise—that resources will be cleaned up on failure (RAII via Drop in Rust)—is broken. Destructors are never called. A MutexGuard will leave a mutex permanently locked. A peripheral that was supposed to be disabled is left in an active state. The very code written to ensure safety on failure becomes useless.
This patched-together model is not a first-principles solution. Aela requires a single, unified system that is safe, deterministic, and efficient on all platforms.
The Fault Model
The Principle of the Honest Contract: Aela avoids ambiguity in failure handling by enforcing that the outcome of a function must be encoded in its signature.
Function Signatures
A function declares its potential to terminate due to an unrecoverable logic bug by using the
|
operator in its return type. This operator separates the single success type from a list of one or more
fault
types.
fn get_at(slice: &[u8], index: int) -> u8 | OutOfBounds;
This signature is an honest contract. It tells any caller: "This function will either return a
u8
, or it will terminate with an
OutOfBounds
fault. There are no other possibilities."
The
fault
and
raise
Keywords
-
`fault`
: A keyword used to declare a type that represents a logic bug or contract violation. This distinguishes it from
structorenum, which represent data and recoverable errors. -
fault OutOfBounds(index: int, len: int); -
`raise`
: A keyword that triggers a fault. It immediately stops the current function's execution and propagates the fault to the caller.
raiseis considered a terminal action. -
raise OutOfBounds(index: index, len: len);
The Standard
match
Statement
The
match
statement is how Aela handles the outcome of functions that may terminate with faults.
Semantic Rules
The safety and special behavior of fault handling are not derived from the syntax, but from a single, powerful semantic rule in the compiler:
If the expression being evaluated by a
matchstatement has a fault path in its signature (i.e., contains a|), then the compiler enforces a "Terminal Arm Rule" on any arm that matches afault-declared type.
This rule is context-dependent. It is triggered by the signature of the function being called, not just the type of the pattern. This is the key insight that resolves all ambiguity.
The "Terminal Arm Rule" is defined as: The block of the arm must end with a terminal statement. Aela does not have implicit returns, so this is a direct check of the final statement in the block.
Terminal statements are:
-
raise; -
std::abort(); -
std::halt(); -
std::reboot();
Note
purefn ketword must not introduce or handle faults; they cannot have a | in their return type and cannot raise.
Note
Faults must not cross FFI; board/FFI shims must convert faults to domain-appropriate codes or terminals.
Note
AP131outlines a proposal to havereturnbe the equivalent ofraise. Allowing both and specifying that they’re identical.
Note
AP132outlines a proposal to permit functions to be generic over an open set of faults (variadic type param), so adapters don’t re-enumerate: ie: fn map(f: fn(T) -> U | E..., x: T) -> U | E...;
Note
AP134outlines a proposal to add a desugaring operator. let b = get_at(xs, 0)?; // expands to match/raise
Complete Examples: The Rule in Practice
These two examples demonstrate how the context-dependent rule creates a safe and unambiguous system.
Example 1: Handling a Live Fault Event
If
reboot;
were omitted, the compiler would issue a clear error:
"Fault-handling arm must end with a terminal statement."
Example 2: Inspecting a Fault as Data
Memory & Mutability
1. Default Allocation Semantics
- All values start on the stack by default.
- Stack allocations are:
- Fast
- Scoped
- Owned directly by the binding.
2. Heap Allocation
To allocate a value on the heap, use the
new
keyword with one of the following modifiers:
Syntax
All heap allocatations are reference counted.
Behavior of
new
Modifiers
| Modifier | Behavior | Description |
|---|---|---|
(none)
|
Heap Allocation (OS based allocator). | Returns a handle/reference to a freshly allocated object, immutable and can not be loaned. |
shared
|
Heap. Shared ownership, single-threaded. | Allowed to be loaned; not thread-safe; not Send/Sync-like. |
shared atomic
|
Heap. Shared ownership, thread-safe. | Reference counts and interior coordination use atomics; Send/Sync if T satisfies the “atomic-safe” requirements you define. |
weak
|
Heap, weak handle to a shared object. | Does not keep the allocation alive; must be paired with at least one strong shared/shared atomic owner somewhere to be useful. |
static
|
Static storage, no OS allocator. | Object is placed/constructed in static memory (BSS/RODATA/flash/SECTION), suitable for bare-metal/MCUs. Lifetime: program-long (or until explicitly torn down by system semantics). |
static atomic
|
Static storage, thread-safe coordination via atomics. | Shared global counters updated in ISR + mainline, Lock-free buffers (ring buffers for UART, DMA, etc.). Configuration/state variables read by multiple cores or ISRs. On a simple single-core MCU with no interrupts, it’s redundant but harmless. |
-
new shared: -
Performs a
copy
of the stack value (
{ ...val }) - Allocates it on the heap
- Wraps it in a reference-counted container (non-atomic)
-
Returns a reference:
&T
-
new shared atomic: - Same as above, but uses atomic reference counting (thread-safe)
3. Mutability Semantics
Mutability is determined only by the binding keyword :
| Keyword | Meaning |
|---|---|
let
|
Immutable binding (read-only) |
var
|
Mutable binding (read-write) |
-
The
mutkeyword is not necesasry here. - Mutability is not encoded in types, structs, or fields.
-
letandvarare both lexically scoped.
Example
Note
Attempting to mutate a
let-bound reference results in a compile-time error!
4. Weak References & Cycle Prevention
To solve the problem of reference cycles (e.g., a parent refers to a child, and the child refers back to the parent), Aela will provide weak references. A weak reference is a non-owning pointer that does not prevent an object from being deallocated.
The system is designed to be minimal and explicit, consisting of three parts:
The weak &T Type
A weak reference has a distinct type to ensure compile-time safety. This allows the compiler to enforce correct usage.
The weak() Downgrade Function
To create a weak reference, you must use the explicit, built-in weak() function. This makes the intent clear and avoids implicit "magic".
let strong_ref: &Foo = new shared { ... };
// Explicitly create a weak reference from a strong one. let weak_ref: weak &Foo = weak(strong_ref);
The if let Upgrade Pattern
Accessing the object behind a weak reference is inherently optional, as the object may have been deallocated. Aela enforces safe access through a conditional if let binding.
// Given a variable 'weak_ref' of type 'weak &Foo'
5. Copying Stack Values
- Heap allocation requires copying the stack value:
- This avoids moving ownership from the stack. Moves are not allowed .
5. Reference Types and Behavior
| Kind | Syntax | RC Type | Thread-Safe | Mutable |
|---|---|---|---|---|
| Stack value |
let x: T = ...
|
None | N/A | No |
| Heap reference |
let x: &T = new shared {...}
|
RC | No | No |
| Heap reference (mutable) |
var x: &T = new shared {...}
|
RC | No | Yes |
| Heap reference (atomic) |
let/var x: &T = new shared atomic {}
|
ARC | Yes | Depends |
6. Compile-Time Analysis
- Safe aliasing of references
-
Proper use of
var(exclusive mutation) - Reference count tracking correctness
- No runtime borrow errors required
The compiler performs static analysis to ensure:
7. No Implicit Moves from Stack
- Stack values cannot be moved to the heap.
-
Heap promotion always requires a
copy
using
{ ...val }. - new shared { ...std::move(x) } is on the roadmap
Calling Conventions
How variables are allocated and passed to functions. The 'new' keyword is the explicit signal for heap allocation and reference counting.
Primitives & Simple Structs (Stack Allocated)
- Rule: Variables declared WITHOUT the 'new' keyword live on the stack.
- In Memory: The variable holds the data directly.
- Function Passing: Passed BY VALUE (a full copy is made).
- Lifetime: Automatic (destroyed when the variable goes out of scope).
- Reference Counted: No.
Boxed Values (Heap Allocated via 'new')
- Rule: Variables initialized WITH the 'new' keyword are allocated on the heap.
- In Memory: The variable holds a POINTER to a "box" on the heap.
- Function Passing: Passed BY REFERENCE (a copy of the pointer is made).
- Lifetime: Managed (Reference Counted).
- Reference Counted: Yes.
Closures (Special Case)
- Rule: A closure that captures variables has its environment allocated on the heap.
-
In Memory: The closure variable is a
{func_ptr, env_ptr}struct. Theenv_ptrpoints to a heap-allocated box containing the captured variables. -
Function Passing: The
{func_ptr, env_ptr}struct itself is small and is passed BY VALUE. - Lifetime: The environment's lifetime is managed by Reference Counting.
Refinement & Dependent Types
TL;DR: Refinement & dependent types are types that embed logical conditions or values themselves, making illegal states unrepresentable by construction.
If you're coming from languages like JavaScript, C++, or Rust, you're used to type systems that check the
shape
of your data. For example, a type checker ensures you don't use a
string
where a
number
is expected.
Aela takes this a step further with refinement types and dependent types . These are features that allow the type system to understand and check the values of your data, not just their shape. It's more rigorous that shape checking, it's a value-aware check that happens entirely at compile time .
Motivation
Most type systems validate
shape
(ie., "this is an
int
"). Aela also validates
value-level facts
: a string is
non-empty
, an integer is
non-zero
, a vector’s
length
matches what your function promises. This turns bugs into
compile‑time errors
and makes code self-documenting.
- Earlier feedback: logical mistakes become type errors you see during compilation.
-
Stronger intent:
types like
NonEmptyStringandNonZerointtell the reader (and the compiler) exactly what you mean. - Confidence: fewer defensive checks sprinkled across your code.
Compile-Time VS. Run-Time Arguments
Aela uses a
single
parameter list for functions, with an optional
compile-time section
separated from the
run-time
section by a semicolon
;
. Parameters are separated by commas. If a parameter has a type annotation, it will be considered a value parameter. If a value has no type annotation, it will be considered a type.
Rules
-
Tokens
before
;are compile-time parameters (type or const/value-level), -
Type parameters:
bare identifiers (ie.,
T,U). -
Const parameters:
Name: Typeform (ie.,N: int). -
Tokens
after
;are run-time parameters (the usualname: Type, with optionalmut/ spread, per language rules). -
If there’s
no
;, the entire list is treated as run-time parameters. -
If the CT part is
empty
, omit
;(preferred style). If the RT part is empty and CT is present, keep a trailing;.
Refinement Types: add a
where
-clause to any base type
A refinement type is a base type with a logical predicate.
Syntax
:
{ id: Type where predicate }
Use refinements when you want value-aware validation while keeping the underlying representation.
Note
The compiler statically checks predicates where it can (literals, constant expressions, and facts learned from prior code/contracts). When a predicate can’t be decided statically, you’ll provide evidence (see “Proving facts to the compiler”).
Dependent Types: when types mention values
A dependent type is a type that depends on values .
A simple, practical pattern is to refine a function’s return by its inputs :
Here the return type depends on the run-time values
a
and
b
. The compiler enforces this contract at compile time wherever it can be proven, and narrows follow-up reasoning.
Another common pattern uses compile-time const parameters to index types:
-
Tis a type parameter . -
MandNare const parameters (compile-time integers). -
The result type’s length
computes
to
M + Nat the type level .
Proving facts to the compiler
There are three common ways to convince the compiler of a refinement:
- Literals and constant expressions — obvious at compile time:
- Local reasoning / guards — use a guard to establish a fact for a scope:
- Type-level contracts — express properties in the type and let the compiler check uses:
Note
In more advanced code, system properties and invariants (see
KW_REQUIRES,KW_ENSURES,KW_INVARIANT) can encode broader guarantees, which the compiler uses as facts within the relevant scope.
End‑to‑End: making division-by-zero impossible
Parameter List: full spec (informal)
Validation
-
The
CT
side only accepts
type/const
parameters (no
mut, no spreads). - The RT side accepts ordinary parameters.
-
A single
top‑level
;within the parentheses splits CT from RT. -
Style: omit an
empty
CT
(; ...)— prefer just( ... ).
Choosing Refinement vs. Dependent
Use refinement types when:
-
You’re constraining a familiar base type (
i32,string, a struct) with a predicate (n != 0,len(s) > 0). - You want to reuse existing APIs with stronger safety.
Use dependent types when:
- The shape of your result depends on inputs (lengths, indices, protocol states).
-
You have natural
compile-time parameters
(ie., a block size
N: int).
They compose well: dependent function types can return refinement types, and vice versa.
Error messages (ergonomics)
- CT/RT mixup : “Compile-time parameter list may only contain type/const parameters.”
-
Missing `;`
: If the first parameter looks like a type/const param (
TorN: int), suggest adding the;. - Unproven refinement : Point to the predicate and suggest guards or helper constructors to provide evidence.
FAQ
Q: Do I have to write CT parameters at call sites? A: Typically no — they’re inferred from RT arguments and expected return types. You can guide inference via annotations.
Q: Can I have multiple `;`?
A: No. There is at most one
top‑level
;
per parameter list.
Q: Are const parameters immutable? A: Yes. They are compile-time values; mutability doesn’t apply.
Q: What about async/pure/thread modifiers?
A: These work unchanged and apply to the function as a whole; the
;
split only affects parameter binding.
Worked examples (with
;
)
1) Identity with a type parameter
2) Map over a vector (type‑level only)
3) Concat with const lengths (conceptual)
4) Safe division with a refinement type
Function Types
Why functions feel like they have “colors” (and how Aela fixes it)
Many languages split the world into synchronous and asynchronous functions. That split tends to spread—call sites, types, libraries—until everything is “[colored][1].” Aela acknowledges that programs have different execution disciplines (purity, async, thread-safety), then solves the composition pain with first-class types , clear call rules , and built‑in intrinsics so you can cross boundaries intentionally and safely.
At a glance:
-
Three disciplines (colors):
pure,thread,async. - Simple call rules enforced by the checker.
- Intrinsic adapters (escape hatches) to cross colors when needed.
-
Contracts
(
requires/ensures) andsystemblocks to guard risky crossings.
The three disciplines
pure
- What it means: No observable side effects; referentially transparent.
- When to use: Deterministic computation, validation, transforms.
-
Gotchas:
Cannot call
asyncor impure functions. May be combined withthreadif the work is offloaded safely.
thread
- What it means: Safe to run off the main executor (e.g., in a worker). No ambient event‑loop assumptions; data must be sendable or shared safely.
- When to use: CPU‑bound work, blocking IO wrapped properly, parallelizable tasks.
-
Combines with:
pure(i.e.,pure thread fn).
async
-
What it means:
May suspend at
awaitpoints, returns a future/awaitable. - When to use: IO‑bound workflows, coordination with timers, event‑driven code.
-
Note:
asyncis notpure. It stands alone.
Call rules (the short version)
-
pure→ may call onlypure. -
thread→ may callthreadorpure; invoking athread fnyields aTaskhandle. -
uncolored
(impure) → may call
thread/puredirectly andasyncvia intrinsic. -
async→ mayawaitotherasyncfunctions orTaskhandles, and also callpure/threadwithout suspension. -
awaitis legal inside bothasync fnandthread fn, because awaiting aTaskjoin or future is permitted in either discipline.
Note
The checker gives precise diagnostics and suggests the right intrinsic when you cross a boundary.
Function types are first‑class
Aela’s type system includes function types with their discipline. These are expressed with refinement/dependent types instead of generics:
Function parameters can carry refinements on their arguments or return types, e.g.:
This makes intrinsics predictable and type‑safe, without requiring a generic system.
Crossing boundaries: standard intrinsics
To make crossing disciplines predictable and ergonomic, Aela ships intrinsics in the standard library:
These are typed adapters that the checker understands.
Signatures & typing rules (no generics required)
Aela doesn’t need parametric generics here—these intrinsics are schematic over types and use refinement/dependent predicates to express constraints. We write them informally as “for any types X, Y…”, and the checker discharges the side conditions.
`to_threaded` *
Checks:
argument and captured values must be sendable or
shared/atomic
. No reliance on an ambient event loop.
`to_async` *
Runtime model: schedules on the blocking/CPU pool; if the pool is saturated, the returned future suspends until capacity is available.
`to_blocking`
* (the common case inside
async
)
This is analogous to Rust Tokio’s `block_in_place`: it yields to the scheduler and executes on the blocking pool.
`block_on` * (drive async to completion from a worker thread)
Policy:
Forbidden on the event‑loop thread; prefer
to_async
/
to_blocking
when in async contexts.
`detach` / `join` *
Diagnostics you’ll see
- “Argument to `to_threaded` captures non‑sendable state Rc(T).”
- “`to_blocking` called from non‑async context.”
- “Pool saturated: `to_async` may suspend until space frees up.”
Examples
CPU work from async (don’t block the loop):
Submit a threaded task and await the handle:
Drive async to completion from a worker thread:
Lift pure compute to workers:
Use blocking work from async:
Use `to_async` to present sync/threaded as async:
Detached background task:
Idioms and examples
-
Keep core transforms
pure, wrap with intrinsics at the edges. -
Use
thread { ... }blocks for structured parallelism; all tasks inside must complete before exit. -
Use
std::selectto race multiple async sources and cancel losers automatically.
FFI without foot‑guns
Declare foreign functions with explicit disciplines and refinements:
Then wrap with
system
policies to guard where they are allowed:
Concurrency & mutability
-
Use
shared/atomictypes for cross‑thread data. -
threadfunctions may operate onsharedsafely. -
purefunctions must not mutate shared state.
Borrow Checker
Core Entities and Notation
-
Place
p: An lvalue, i.e. a path to a memory location. Examples:x,arr[i],s.field. In safe Aela, places are structured paths (no raw pointer deref, pointer arithmetic, or unions). -
Reference
&p/&mut p: A borrow of a place, producing a reference value. -
Loan
L(p, kind): The abstract fact thatpis borrowed, withkind ∈ {shared, unique}. -
Program Point
q: A location in the control-flow graph (CFG). -
Alive(L, q)
: Predicate meaning loan
Lis still valid at pointq(lexically-free semantics). - Operations :
-
reads(p, q): a read of placepatq. -
writes(p, q): a write to placepatq. -
moves(p, q): a move from placepatq. -
Aliases(x, p, q)
:
xholds a reference topatq(logical predicate, tracked via loan origins). -
Escape(r, q)
: Reference
rescapes its region atq(returned, stored, captured, etc.).
inter-procedural Region & Effect Summaries
-
Each function with references is internally elaborated to carry region variables
ρ. -
Example:
fn get_first(&[i32]) -> &i32elaborates tofor<ρ> fn(&ρ [i32]) -> &ρ i32. - Compiler emits a summary : region params, outlives constraints, and an escape set.
- Call sites instantiate these summaries and unify with argument regions. This provides modular checking without WPO.
-
Optional disambiguation syntax for multiple inputs:
fn pick(arr: &T as A, other: &T) -> &A T;ties return to param markerA.
Minimal Syntax Escape Hatch (as A)
Keeps everyday code lifetime-less, but allow explicit disambiguation when inference cannot decide.
-
Syntax:
Parameters may carry labels:
fn foo(x: &T as A) -> &A T. Returns may use&A. Struct fields may tie to labels. -
Labels introduce internal region vars (`ρ_A`)
that summaries use. Unlabeled references get fresh
ρ’s. - Elision rules: one input reference → auto-tie; multiple ambiguous inputs → require a label.
- Advanced usage:
-
Outlives constraints can be added:
fn foo(x: &T as A, y: &U as O) -> &A T where O : A;. -
Methods:
selfimplicitly labeledSelf, but can also be explicitly labeled:self: &Self as S. -
Diagnostics:
Role-based: “return value tied to param
a(label A) butadoes not live long enough.” Quick fixes: “Addas Ato param.”
Lifetimes in Data Structures
-
Reference fields implicitly carry region parameters.
struct Node { data: &T }elaborates tostruct Node<ρ, T> { data: &ρ T }. -
Construction instantiates
ρfrom the argument’s region. Error if struct outlives the referenced data.
Place Overlap (I3)
-
poverlapsp. -
poverlapsp.f. Distinct fields disjoint. -
arr[i]vsarr[j]: disjoint if indices are distinct compile-time constants; else conservative overlap. -
Library intrinsics like
split_at_mutprovide disjointness proofs via trusted contracts or runtime checks.
Escape Conditions (E1)
Reference escapes if it flows into a longer-lived region by:
- Returning from a function.
- Assigning to a longer-lived binding.
- Storing in struct/enum/global.
- Capturing by closure/future that outlives scope.
- Passing to FFI (unless contract says non-retaining).
- Storing in concurrent/shared cell accessible later.
- Erasure into longer-lived object/interface.
Asynchronous Code (async/await)
-
Phase 1:
Forbid loans across
awaitunless the origin outlives the entire future. Practically: locals cannot crossawait; only borrows from captured fields of the async task can. -
Phase 2:
Desugar
asyncto state machines and check across suspension points, enabling safe long-lived borrows.
Closures (FunctionExpression)
- Capture classification:
- Read-only → shared.
- Mutate → unique.
- Move → by-value.
- Trait mapping: shared → Fn; unique → FnMut; move → FnOnce.
- Escaping closures require captured regions to outlive closure region.
Refinement Types
-
Built-in predicates like
initialized(x),not_escaped(x)are decidable and do not require heavy SMT. - User-defined predicates and full logical refinement are measured to avoid compile-time blowups and underspecification.
Diagnostics Without Lifetime Names
-
Role-based regions: “returned reference must not outlive borrow of
arr.” - Highlight borrow creation, return site, and conflict.
- Optional symbolic labels (r1, r2) in error messages for clarity.
Rule-by-Rule Examples
Rule-by-Rule Examples (Concrete)
R0 — Implicit, Lexically-Free Regions
L1 — Loan Creation
L2 — Loan Propagation
A1 — Unique Exclusivity
A2 — Shared Read‑Only
I1 — Write Invalidates
I2 — Move Invalidates
I3 — Overlap/Fields
U1 — Use Requires Alive
U2 — No After‑Free
RB1 — Shared from Unique
RB2 — Unique from Unique (optional v1)
E1 — No Escaping Borrows
M1 — Binding Mutability
M2 — No Shared Mutability Without Atomics
T1 — Temporaries
T2 — Branches/Loops/Match
F1 — FFI Preconditions
F2 — FFI Postconditions
RFT1 — Refinement Well‑Formedness
RFT2 — Discharge at Use Sites
inter‑Procedural Summary (Elision)
Structs with Reference Fields (Implicit Regions)
Async Phase 1 — No Unique Loans Across Await
Closures — Capture Classification
RTOS Harmony
Linux Setup
You most likely need to edit your udev rules
If it doesn’t have your board’s rules in it, list your board to get the vendor and product id.
Replace ID VVVV:PPPP
Reload, re-enumerate
Unplug & Replug, press the reset button & then check:
Zephyr
Install
Install the Zephyr CLI tool
Allows you to flash your Arduino without needing to use sudo every time. It gives your user account permission to access the USB device.
Create a project directory. Create a python environment in the project directory. Initialize it as a new zephyr project. Run the update command to get all the sub-repos.
Go back to your projects root and get the minimal SDK for cross compiling.
Calculate the SHA sum if you want
Extract the SDK and run the setup
Initialize everything by entering your zephyr directory, activate the python env and set the environment (links the SDK you just got)
Compile & Flash Test
Finally, build an Arduino image. I use Blinky here because its super simple and makes sense to start with as a sanity test.
Integrating with Aela
Aela can output object files, so it's very simple to create a Zephyr project with Aela by just adding
CMakeLists.txt
and
prj.conf
files.
Example
prj.conf
Example
CMakeLists.txt
Compiling
Note
The _AE_ARM_OPTS trick copies exactly the -mcpu/-mfpu/-mfloat-abi/-mthumb flags Zephyr uses for this board, so your Aela object links ABI-cleanly without you guessing the right combo. (UNO R4 WiFi is a Cortex-M4; Zephyr’s board page confirms the arch.)
FreeRTOS
Discover Platform Build Flags
Lets assume an Arduino R4 here as an example.
We need to "spy" on the Arduino CLI to get the exact compiler and linker flags required for the R4 WiFi. The Aela build system will pass these directly to its underlyng C compiler. Arduino code files are called "sketches".
- Create a dummy sketch:
- Run a verbose compile:
-
Find and copy the linker command.
Scroll through the output and find the longest command, which will start with something like
.../arm-none-eabi-g++ .... It will be a very long line that links all the object files and libraries together. Copy this entire command into a text editor. We're going to use its flags.
It will look something like this (shortened for clarity):
.../arm-none-eabi-g++ -Os -Wl,--gc-sections -mcpu=cortex-m4 ... -L/path/to/build -Wl,--start-group -lArduinoCore -lrtos -lm -Wl,--end-group ...
We will now transfer the important parts of that command into your
index.json
.
Step 2: Project Structure
Organize your project with your source code inside a
src
directory.
Step 3: The
index.json
Manifest
This is the control center. We'll define a custom platform target called
"arduino_r4"
and place all the flags we discovered into the
link
section.
Create the file
`index.json`
(unless it was created via
aec init
):
Note
You must replace the placeholder paths (
/home/user/...,/tmp/arduino/...) with the full, absolute paths you copied from your verbose compiler output in Step 1.
Step 4: The Source Code
The source code is the same as before, but now it lives in the
src/
directory.
src/main.ae
src/shim.c
Step 5: Build and Flash
You tell the Aela compiler what you're targeting, and it uses the manifest to set the correct build flags.
Build the project for the target triple:
Flash the output. The build process should still result in a .uf2 file in a build directory. You can then flash it with the Arduino CLI as before.
FAQ
Why is Aela Commercial-Source?
Security
Transparency helps, but it’s not enough. Heartbleed, Log4Shell, and the xz backdoor landed in widely used open-source code. The gap isn’t visibility—it’s accountability, resourcing, and execution.
A commercial, closed-source model brings clear responsibility and funded security work:
Formal accountability
Commercial software runs under contracts and SLAs. When a vulnerability appears, a named company is on the hook—legally and financially—to fix it. In decentralized open source, responsibility often sits with volunteers without service commitments.
Dedicated, directed resources
We staff full-time security teams for audits, pen tests, and vulnerability management. Budget and people are assigned to unglamorous but critical maintenance and hardening that many projects lack.
Cohesive vision and focused dev
One organization sets architecture, roadmap, and tradeoffs. Decisions move faster and designs stay consistent. Large open-source efforts juggle many voices, which can slow delivery and blur design.
Choose the assurance model that fits your risk. Community stewardship offers transparency and shared responsibility. Our model adds contractual guarantees, professional assurance, and direct accountability backed by dedicated resources.
Cost
Look past sticker price to total cost of ownership (TCO).
Open source
TCO includes hiring in-house experts who contribute upstream to unblock features and address security issues.
Commercial source
We fold those operational costs into a predictable subscription or license. You get SLAs, legal indemnification, and dedicated support that map cleanly to enterprise procurement and risk frameworks.
In short: invest in internal capability and control, or buy a service-backed solution with predictable costs and clear responsibility.