Get Started

Aela is a software platform for creating formally verifiable, memory safe, and highly reliable applications. What's included in the compiler:

  • Works in any editor
  • Provides a built in linter, formatter, and LSP.
  • A local, offline-first agent that understands the compiler and your codebase and can talk to your other AI services.
  • Supports JIT module (that's hot reloading for compiled programs, aka: edit and continue)

Install the compiler.

Example
0 sudo sh - c 'curl -fsSL https://stablestate.ai/$CUSTOMER_ID | bash'

In a new directory, create a new project using the following command.

Example
0 aec init

This will create some default files.

Example
0 .
1 ├── index . json
2 └── src
3 └── main . ae

Edit the index.json file to name your project.

Example
0 {
1 "name" : "aela - tests" ,
2 "version" : "0 . 1 . 0" ,
3 "entry" : "main . ae" ,
4 "output" : "build / main" ,
5 "include" : [ "src /**/ * . ae" ] ,
6 "exclude" : [ "tests / * * " ] ,
7 "dependencies" : [
8 {
9 "name" : "ui" ,
10 "url" : " . . / lib / ui"
11 }
12 ]
13 }

Next you’ll edit the main.ae file

int { io::print("Hello, Aela!"); return 0; }" lang="rust" title="Example" id="8580699c4d9ff">
Example
0 // Aela Hello World
1
2 import io from "io" ;
3
4 fn main ( args : string [ ] ) - > int {
5 io : : print ( "Hello , Aela ! " ) ;
6 return 0 ;
7 }

To build your project, run the following command.

Example
0 aec build

You’ll see a new directory with the compiled program that you can run.

- new files
0 .
1 ├── build
2 │ └── main
3 ├── index . json
4 └── src
5 └── main . ae

Compiler Modes

aec build

This is your traditional, one-shot Ahead-Of-Time (AOT) compiler command.

What Compiles your entire project from source into a final, optimized executable binary.
How It invokes the compiler engine, which runs all formal verifications, performs release-level optimizations (which can be slow), and links everything together. It's a non-interactive process designed for final output.
Why Running in a Continuous Integration (CI) pipeline or when you're ready to create a production version of your application.

aec run

This is a convenience command for development.

What Builds and immediately executes your program.
How It's a simple wrapper that first performs an aec build (likely with fewer optimizations to be faster than a release build) and then runs the resulting binary.
Why Quickly testing a command-line application's behavior without needing a full watch session.

aec watch

This command exposes the engine's interactive mode directly to the command line.

What Starts the persistent, incremental engine to monitor files and provide continuous feedback in the terminal.
How It activates the same underlying engine that the LSP uses. On every change, it re-verifies your code and can be configured to take an action, such as re-running your test suite (aec watch --exec test) or restarting your application via the JIT.
Why For developers who prefer working in the terminal, it enables a very fast Test-Driven Development (TDD) loop without the overhead of an IDE.

aec package

This is a higher-level workflow and distribution tool.

What Bundles your project into a distributable format.
How It would first run aec build --release to create the optimized executable. Then, it would gather other assets—like documentation, licenses, and configuration files—and package them into a compressed archive (like a .tar.gz) for publishing to a registry or for distribution.
Why Publishing a new version of your library or application for others to use.

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 wont 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

- /path/to/my-app/index.json
0 {
1 "name" : "my - app" ,
2 "version" : "0 . 1 . 0" ,
3 "entry" : "src / main . ae"
4 }

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.json to the
  • dependency's root directory.

Example

- index.json for a project that uses a UI library
0 {
1 "name" : "my - gui - app" ,
2 "version" : "1 . 0 . 0" ,
3 "entry" : "src / app . ae" ,
4 "dependencies" : {
5 "ui" : " . . / libs / aela - ui" ,
6 "database" : " . . / libs / aela - db"
7 }
8 }

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

- ../libs/aela-ui/src/window.ae
0 // This struct can be imported by other modules.
1 export struct WindowOptions {
2 title : string ;
3 width : int ;
4 height : int ;
5 }
6
7 // This function is private to the window.ae module.
8 fn internal_helper ( ) - > void {
9 // ...
10 }

Re-exports

- re-export pattern
0 import { Thing1 , Thing2 } from "things . ae" ;
1
2 export Thing1 ;
3 export Thing2 ;

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

";" lang="" title="Example" id="ab5bd6fe58171">
Example
0 import from " " ;

Example

void { // Access symbols using the `::` namespace operator. let opts: &ui::WindowOptions = new ui::WindowOptions(); helpers::do_something(); }" lang="example" title="- src/app.ae" id="49a0501874e3d">
- src/app.ae
0 // Import the "ui" package, which was defined in index.json.
1 import ui from "ui" ;
2
3 // Import a local utility file using a relative path.
4 import helpers from " . / helpers . ae" ;
5
6 fn main ( ) - > void {
7 // Access symbols using the `::` namespace operator.
8 let opts : & ui : : WindowOptions = new ui : : WindowOptions ( ) ;
9 helpers : : do_something ( ) ;
10 }

Named Imports

This allows you to import specific functions or types directly into the current scope, without needing a namespace qualifier.

Syntax

";" lang="" title="Example" id="2068a58526a41">
Example
0 import { , : } from " " ;

Example EXAMPLE

void { // `Window` can be used directly. let win: &Window = new Window(); // `WindowOptions` is available under the alias `Options`. let opts: &Options = new Options(); }" lang="example" title="- src/app.ae" id="5ed0ea6e30563">
- src/app.ae
0 import { Window , WindowOptions : Options } from "ui" ;
1
2 fn main ( ) - > void {
3 // `Window` can be used directly.
4 let win : & Window = new Window ( ) ;
5
6 // `WindowOptions` is available under the alias `Options`.
7 let opts : & Options = new Options ( ) ;
8 }

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

- index.json for an app with native UI code on macOS
0 {
1 "name" : "aela - native - app" ,
2 "version" : "0 . 1 . 0" ,
3 "entry" : "src / main . ae" ,
4 "sources" : {
5 "darwin" : [ "src / native / apple . mm" ]
6 } ,
7 "link" : {
8 "platform" : {
9 "darwin" : [
10 " - framework" , "Foundation" ,
11 " - framework" , "AppKit" ,
12 " - framework" , "ObjectiveC"
13 ]
14 }
15 }
16 }

This configuration tells the Aela compiler:

  1. On macOS, compile the src/native/apple.mm Objective-C++ file.
  2. When linking the final executable, add flags to link against the
  3. Foundation, AppKit, and ObjectiveC system frameworks.

Type System Features

Refinement Types

Refinement types allow you to create a subtype from an existing type by adding constraints that are checked at compile time. This is useful for creating more precise and bug-resistant types.

Example
0 type NewTypeName = { value_name : BaseType where condition_expression } ;
value_name A name for the value, used within the where clause.
BaseType The type you are constraining (e.g., i32, string).
condition_expression A boolean expression that must be true for any value of this new type.

Example

You can define a type that only allows positive integers, preventing invalid values at compile time.

- Positive Integer
0 // Definition
1 type PositiveInt = { n : i32 where n > 0 } ;
2
3 // Usage
4 fn set_age ( age : PositiveInt ) {
5 // ... function body
6 }
7
8 set_age ( 10 ) ; // OK
9 set_age ( - 5 ) ; // Compile-time error!

Example: Non-Empty String

Refinements can use functions or properties in their conditions.

Example
0 // Definition
1 type NonEmptyString = { s : string where s . length ( ) > 0 } ;
2
3 // Usage
4 fn print_greeting ( name : NonEmptyString ) {
5 // ...
6 }

The primary benefit is turning potential runtime errors (like invalid arguments) into compile-time errors.

Dependent Types

Dependent types allow a type's definition to depend on a value, not just another type. This lets you encode properties like the size of a collection directly into its type.

Defining Dependent Structs

You define a dependent struct by including value parameters in parentheses () in its declaration.

Syntax

struct TypeName(param_name: ParamType, ...);

Example

Here, the Vect type depends on a length len and a type T.

Length-Indexed Vector
0 // Definition of a vector that knows its length.
1 struct Vect ( len : nat , T : Type ) {
2 // ... implementation details
3 }
4
5 // Usage in a variable declaration.
6 let names : Vect ( 3 , string ) = [ "Alice" , "Bob" , "Charlie" ] ;
7 let empty : Vect ( 0 , i32 ) = [ ] ;

Using Dependent Types in Functions

Function signatures can use these value dependencies to enforce relationships between parameters, making impossible states unrepresentable.

Example

This function to get the first element of a vector can only be called on a non-empty vector.

Safe head function
0 // The type signature guarantees vec has a length of at least 1.
1 fn safe_head ( len : nat , T : Type , vec : Vect ( len + 1 , T ) ) - > T {
2 return vec [ 0 ] ;
3 }
4
5 let first_name : string = safe_head ( 2 , string , names ) ; // OK
6
7 // The following line would cause a compile-time error because the
8 // compiler can see that the type Vect(0, i32) does not match
9 // the required type Vect(n + 1, T).
10 let error = safe_head ( - 1 , i32 , empty ) ;

This feature allows you to build extremely safe APIs by making the compiler aware of properties that are usually only checked at runtime.

Operators

Precedence Operator(s) Description Associativity
1 (Lowest) = Assignment Right-to-left
2 ?? Optional Coalescing Left-to-right
3 || Logical OR Left-to-right
4 && Logical AND Left-to-right
5 | Bitwise OR Left-to-right
6 ^ Bitwise XOR Left-to-right
7 & Bitwise AND Left-to-right
8 == , != Equality / Inequality Left-to-right
9 < , > , <= , >= Comparison Left-to-right
10 << , >> Bitwise Shift Left-to-right
11 + , - Addition / Subtraction Left-to-right
12 * , / , % Multiplication / Division / Modulo Left-to-right
13 ! , - , ~ , & (prefix), await Unary (Logical NOT, Negation, Bitwise NOT, Address-of, Await) Right-to-left
14 (Highest) () , [] , . , ?. , as Function Call, Index Access, Member Access, Type Cast Left-to-right

Flow Control

This document covers all flow control constructs in Aela, including conditionals, loops, and matching.

1. if / else

Syntax

Example
0 if ( )
1 [ else ]

Description

Standard conditional branching.

  • The condition must be an expression evaluating to bool .
  • Both then_branch and else_branch must be statements , usually blocks.

Examples

Example
0 if ( x > 0 ) {
1 print ( "Positive" ) ;
2 } else {
3 print ( "Non - positive" ) ;
4 }
5
6 if ( flag ) doSomething ( ) ;

2. while Loop

Syntax

Example
0 while ( )

Description

Loops as long as the condition evaluates to true .

Example

Example
0 while ( i < 10 ) {
1 i = i + 1 ;
2 }

3. for Loop

Syntax

Example
0 for ( in )

Description

Iterates over a collection or generator. Declarations must bind variables with types.

Example

Example
0 for ( let i : Int in 0 . . 10 ) {
1 print ( " { } " , i ) ;
2 }
3
4 for ( var x : String , var y : String in lines ) {
5 print ( " { } { } " , x , y ) ;
6 }

4. match Expression

Syntax

Example
0 match ( ) {
1 = > ,
2 . . .
3 _ = >
4 }

Description

Exhaustive pattern matching. Each match arm uses a pattern and a block.

  • _ is the wildcard pattern (required if not all cases are covered).
  • Patterns can be:
  • Literals: 1 , "foo" , 'c' , true , false
  • Identifiers: binds the value
  • Constructor patterns: Some(x) , Err(e)

Example

{ print("One"); }, _ => { print("Other"); } }" lang="aela" title="Example" id="36ee863b38fd8">
Example
0 match ( value ) {
1 0 = > { print ( "Zero" ) ; } ,
2 1 = > { print ( "One" ) ; } ,
3 _ = > { print ( "Other" ) ; }
4 }

5. return

Syntax

Example
0 return [ ] ;

Description

Exits a function immediately with an optional return value.

Examples

Example
0 return ;
1 return x + 1 ;

6. break

Syntax

Example
0 break ;

Description

Terminates the nearest enclosing loop.

7. continue

Syntax

Example
0 continue ;

Description

Skips to the next iteration of the nearest enclosing loop.

8. Blocks and Statement Composition

Syntax

Example
0 {
1 ;
2 . . .
3 }

Description

A block groups multiple statements into a single compound statement. Used for control flow bodies.

Example

Example
0 {
1 let x : Int = 1 ;
2 let y : Int = x + 2 ;
3 print ( y ) ;
4 }

9. Expression Statements

Syntax

Example
0 ;

Description

Evaluates an expression for side effects. Common for function calls or assignments.

Example

Example
0 doSomething ( ) ;
1 x = x + 1 ;

Optionals

Optionals provide a safe and explicit way to handle values that may or may not be present. Instead of using special values like null or -1 which can lead to runtime errors, Aela uses the Option type to wrap a potential value. The compiler will then enforce checks to ensure you handle the "empty" case safely.

Declaring an Optional Type

You can declare a variable or field as optional using two equivalent syntaxes:

  1. The `?` Suffix (Recommended) : This is the preferred, idiomatic syntax.
  2. It's a concise way to mark a type as optional.
Example
0 // A variable that might hold a string
1 let name : string ? ;
2
3 // A struct with optional fields
4 struct Profile {
5 age : u32 ? ;
6 bio : string ? ;
7 }
  1. The `Option(T)` Syntax : This is the formal, nominal type. The T?
  2. syntax is simply sugar for this. It can be useful in complex, nested type
  3. signatures for clarity.
Example
0 // This is equivalent to `let name: string?`
1 let name : Option ( string ) ;

Creating Optional Values

An optional variable can be in one of two states: it either contains a value, or it's empty. You use the Some and None keywords to create these states.

None : The Empty State

The None keyword represents the absence of a value. You can assign it to any optional variable, and the compiler will infer the correct type from the context.

Example
0 let age : u32 ? = None ;
1
2 let user : User = {
3 // The profile field is optional
4 profile : None
5 } ;
6 Some ( value ) : The Value - Holding State

To create an optional that contains a value, you wrap the value with the Some constructor.

Example
0 // Create an optional u32 containing the value 30
1 let age : u32 ? = Some ( 30 ) ;
2
3 let user : User = {
4 profile : Some ( {
5 email : "some@example . com" ,
6 age : Some ( 30 )
7 } )
8 } ;

The Optional-Coalescing Operator (??) (For Defaults)

This is the best way to unwrap an optional by providing a fallback value to use if the optional is None. The term "coalesce" means to merge or come together; this operator coalesces the optional's potential value and the default value into a single, guaranteed, non-optional result.

Example
0 // Get the user's email, or use a default if it's None.
1 // `email_address` will be a regular `string`, not a `string?`.
2 let email_address : string = user2 . profile ? . email ? ? "no - email - provided@domain . com" ;
3
4 print ( "Contacting user at : { } " , email_address ) ;

Using Optional Values

Aela provides mechanisms to safely work with optional values, preventing you from accidentally using an empty value as if it contained something.

Optional Chaining (?.)

The primary way to access members of an optional struct is with the optional chaining operator, ?.. If the optional is None, the entire expression short-circuits and evaluates to None. If it contains a value, the member access proceeds.

The result of an optional chain is always another optional.

Example
0 struct Profile {
1 email : string ;
2 }
3
4 struct User {
5 profile : Profile ? ;
6 }
7
8 fn main ( ) - > int {
9 let user1 : User = { profile : Some ( { email : "test@example . com" } ) } ;
10 let user2 : User = { profile : None } ;
11
12 // email1 will be an `Option(string)` containing Some("test@example.com")
13 let email1 : string ? = user1 . profile ? . email ;
14
15 // email2 will be an `Option(string)` containing None
16 let email2 : string ? = user2 . profile ? . email ;
17
18 return 0 ;
19 }

Explicit Checking (Match Statement)

Use match statements to explicitly handle the Some and None cases, allowing you to unwrap the value and perform more complex logic.

io::print("The name is: {}", value), None => io::print("No name was provided."), }" lang="" title="Example" id="c36380aec7d35">
Example
0 let name : string ? = Some ( "Aela" ) ;
1
2 match name {
3 Some ( value ) = > io : : print ( "The name is : { } " , value ) ,
4 None = > io : : print ( "No name was provided . " ) ,
5 }

Mutability

Aela enforces safety and clarity by requiring that any function intending to modify data must be explicitly marked. This prevents accidental changes and makes code easier to reason about. This is achieved through the mut keyword.

The Principle: Safe by Default

In Aela, all function parameters are immutable (read-only) by default. When you pass a variable to a function, you are providing a read-only view of it.

Example
0 fn read_runner ( r : & Runner ) {
1 // This is OK.
2 io : : print ( "Points : { } " , r . point ) ;
3
4 // This would be a COMPILE-TIME ERROR.
5 // r.point = 5;
6 }

Granting Permission to Mutate

To allow a function to modify a parameter, you must use the mut keyword in two places:

  1. The Function Definition: To declare that the function requires mutable
  2. access.
  3. The Call Site: To explicitly acknowledge that you are passing a variable
  4. to be changed.

This two-part system makes mutation a clear and intentional act.

In the Function Definition

Prefix the parameter you want to make mutable with mut . This is the function's "contract," stating its intent to modify the argument.

Example
0 fn reset_runner ( mut r : & Runner ) {
1 // This is now allowed because the parameter `r` is marked as `mut`.
2 r . point = 0 ;
3 r . passed = 0 ;
4 r . failed = 0 ;
5 }

At the Call Site

When you call a function that expects a mutable parameter, you must also prefix the argument with mut . This confirms you understand the variable will be modified.

Example
0 fn main ( ) {
1 // The variable itself must be mutable, declared with 'var'.
2 var my_runner = Runner . new ( ) ;
3
4 // The 'mut' keyword is required here to pass 'my_runner'
5 // to a function that expects a mutable argument.
6 reset_runner ( mut my_runner ) ;
7 }

The compiler will produce an error if you try to pass a mutable argument without the mut keyword, or if you try to pass an immutable ( let ) variable to a function that expects a mutable one. This ensures there are no surprises about where your data can be changed.

Errors

Out-of-the-box errors are simple, the verifier runs the check borrows and the life-times of variables and properties.

An error where mut keyword should have been used
0 Analyzer Error : Cannot assign to field 'point' because 'self' is immutable .
1 - - > / Users / paolofragomeni / projects / aela / lib / test . ae : 16 : 10
2
3 15 | fn ok ( self : & Self , cond : bool , desc : string ) - > bool {
4 16 - > self . point = self . point + 1 ;
5 | ^
6 17 |

Structs, Impl Blocks, and Memory Layout

struct Declarations: The Data Blueprint

A struct defines a composite data type. Its sole purpose is to describe the memory layout of a collection of named fields. Structs contain ONLY data members.

Syntax

Example
0 struct {
1 : ;
2 : ;
3 . . .
4 }

Example

Defines a type named 'Packet' that holds a sequence number, a size, and a single-byte flag.

Example
0 struct Packet {
1 sequence : u32 ;
2 size : u16 ;
3 is_urgent : u8 ;
4 }

impl Blocks: Attaching Behavior

An impl (implementation) block associates functions with an existing struct type. These functions are called methods. The impl block does NOT alter the struct's memory layout or size.

Example
0 impl {
1 // constructor (optional, special method)
2 fn constructor ( self : & Self , . . . ) - > Self { . . . }
3
4 // methods
5 fn ( self : & Self , . . . ) - > { . . . }
6 }

Details

  • The constructor is a special function that initializes the
  • struct's memory. It is called when using the new keyword.
  • Methods are regular functions that receive a reference to an
  • instance of the struct as their first parameter, named self .
  • Self (capital 'S') is a type alias for the struct being implemented.
  • Multiple impl blocks can exist for the same struct. The compiler
  • merges them.

Example

Example
0 impl Packet {
1 fn constructor ( self : & Self , seq : u32 ) - > Self {
2 self . sequence = seq ;
3 self . size = 0 ;
4 self . is_urgent = 0 ;
5 }
6
7 fn mark_urgent ( self : & Self ) - > void {
8 self . is_urgent = 1 ;
9 }
10 }

Memory Layout and Padding

Aela adopts C-style struct memory layout rules, including padding and alignment, to ensure efficient memory access and ABI compatibility.

  1. Sequential Layout: Fields are laid out in memory in the exact
  2. order they are declared in the struct definition.
  1. Alignment: Each field is aligned to a memory address that is a
  2. multiple of its own size (or the platform's word size for larger
  3. types). The compiler inserts unused "padding" bytes to enforce this.
  1. Struct Padding: The total size of the struct itself is padded to be a
  2. multiple of the alignment of its largest member. This ensures that
  3. in an array of structs, every element is properly aligned.

Rules:

Example
0 struct Packet {
1 sequence : u32 ; // 4 bytes
2 size : u16 ; // 2 bytes
3 is_urgent : u8 ; // 1 byte
4 }

Visual Layout (on a typical 64-bit system):

Byte Offset Content
0 sequence (Byte 0)
1 sequence (Byte 1)
2 sequence (Byte 2)
3 sequence (Byte 3) ← 4‑byte
Byte Offset Content
4 size (Byte 0)
5 size (Byte 1) ← 2‑byte
Byte Offset Content
6 is_urgent (Byte 0)
Byte Offset Content
7 PADDING (1 byte) ← struct padded to a multiple of 4 bytes (max)

TOTAL SIZE: 8 bytes

Heap vs. Stack Allocation

Aela supports both heap and stack allocation for structs, giving the programmer control over memory management and performance.

Stack allocation (Default for local variables):

  • How: A struct is allocated on the stack by declaring a variable of
  • the struct type and initializing it with a struct literal. The new
  • keyword is NOT used.
  • Lifetime: The memory is valid only within the scope where it is
  • declared (e.g., inside a function). It is automatically reclaimed
  • when the scope is exited.
  • Performance: Extremely fast. Allocation and deallocation are nearly
  • instant, involving only minor adjustments to the stack pointer.
Example
0 let my_packet : Packet = Packet {
1 sequence : 200 ,
2 size : 128 ,
3 is_urgent : 1
4 } ;

Heap Allocation (Explicit):

  • How: A struct is allocated on the heap using the new keyword, which
  • returns a reference ( & ) to the object.
  • Lifetime: The memory persists until it is no longer referenced. Its
  • lifetime is managed by the runtime's reference counter, not tied to a
  • specific scope.
  • Performance: Slower than stack allocation. Involves a call to the
  • system's memory allocator ( malloc ) and requires runtime overhead for
  • reference counting.
- Explicit Heap Allocation
0 let my_packet_ref : & Packet = new Packet ( 201 ) ;

When to use which:

  • STACK: Use for most local, temporary data. It's the idiomatic and
  • most performant choice for data that does not need to outlive the
  • function in which it was created.
  • HEAP: Use when a struct instance must be shared or returned from a
  • function and needs to have a lifetime independent of any single
  • scope. Also used for very large structs to avoid overflowing the stack.

Opaque Structs

Safety & Undefined Behavior (UB)

The primary benefit of opaque structs is preventing a whole class of undefined behavior by strengthening type safety at the language boundary.

How Safety is Increased

Eliminates Type Confusion: Before, you might have used a generic type like `u64` or `&void` to represent a C handle. The compiler had no way to know that a `u64` from `database_connect()` was different from a `u64` from `file_open()`. You could accidentally pass a database handle to a file function, leading to memory corruption or crashes. Now, `&DatabaseHandle` and `&FileHandle` are distinct, incompatible types *. The Aela compiler will issue a compile-time error if you try to misuse them, completely eliminating this risk.

Prevents Invalid Operations in Aela: * By disallowing member access and instantiation, we prevent Aela code from making assumptions about the C data structure. Aela code cannot accidentally:

Read from or write to a field that doesn't exist or has a different offset (`my_handle.field`). Create a struct of the wrong size on the stack ( let handle: StringBuilder ). * Perform pointer arithmetic on the handle. The only thing Aela code can do is treat the handle as an opaque value to be passed back to the C library, which is the only safe way to interact with it.

For Users of Opaque Structs

Your documentation should include:

  1. Purpose and Syntax: Explain that opaque structs are for safely handling foreign pointers/handles. Show the syntax:
Example
0 // in lib/mylib.ae
1 export struct MyFFIHandle ;
  1. Rules of Engagement: Clearly state the allowed and disallowed operations we implemented.

Allowed: Passing to/from FFI functions, assigning to other variables of the same type, comparing for equality. Disallowed: Member access ( . ), instantiation ( new ), and dereferencing. Always use a reference ( &MyFFIHandle ).

  1. A Mandatory Safety Section on Lifetimes: This section must be prominent. It should explain the dangling pointer risk and establish a clear best practice.

When working with opaque handles, you are responsible for managing their memory. Most C libraries provide functions for creating and destroying these objects. You must call the destruction function to prevent memory leaks and undefined behavior.

&StringBuilder; ffi ae_runtime_sb_append: fn(&StringBuilder, string); ffi ae_runtime_sb_destroy: fn(&StringBuilder); // <-- The cleanup function fn main() -> int { let sb = ae_runtime_sb_new(); ae_runtime_sb_append(sb, "hello"); // CRITICAL: You must call destroy when you are done. ae_runtime_sb_destroy(sb); // Using `sb` after this point is UNDEFINED BEHAVIOR. // ae_runtime_sb_append(sb, " world"); // <-- DANGEROUS! return 0; }" lang="aela" title="Example: Managing Lifetimes" id="0b432d411f723">
Example: Managing Lifetimes
0 `` `aela
1 import { StringBuilder } from " . / runtime . ae" ;
2
3 // FFI Declarations for a C string builder
4 ffi ae_runtime_sb_new : fn ( ) - > & StringBuilder ;
5 ffi ae_runtime_sb_append : fn ( & StringBuilder , string ) ;
6 ffi ae_runtime_sb_destroy : fn ( & StringBuilder ) ; // <-- The cleanup function
7
8 fn main ( ) - > int {
9 let sb = ae_runtime_sb_new ( ) ;
10 ae_runtime_sb_append ( sb , "hello" ) ;
11
12 // CRITICAL: You must call destroy when you are done.
13 ae_runtime_sb_destroy ( sb ) ;
14
15 // Using `sb` after this point is UNDEFINED BEHAVIOR.
16 // ae_runtime_sb_append(sb, " world"); // <-- DANGEROUS!
17
18 return 0 ;
19 }

Interfaces

This document specifies the design and behavior of Aela's system for polymorphism, which is based on interface, struct, and impl...as... declarations.

Overview

Aela's polymorphism is designed to be explicit, safe, and familiar. It allows developers to write flexible code that can operate on different data types in a uniform way, a concept known as dynamic dispatch. This is achieved by separating a contract's definition (the interface) from its implementation (the struct and impl block).

Example
0 interface Element {
1 fn onclick ( event : & Event ) - > void ;
2 }
3
4 struct Button {
5 handle : i64 ;
6 }
7
8 impl Button as Element {
9 fn constructor ( self : & Self , someArg1 : string ) {
10 // fired when new is used
11 }
12 fn init ( self : & Self , someArg1 : string ) {
13 // fired when ever a struct is initialized.
14 }
15 fn onclick ( self : & Self , event : & Event ) - > void {
16 // fired when called directly (statically or dynamically)
17 }
18 }
19
20 impl Button as Element {
21 fn ontoch ( self : & self , event : & Event ) - > void {
22 }
23 }

The core philosophy is:

Interfaces define abstract contracts or capabilities.

Structs define concrete data structures.

impl...as... blocks prove that a concrete struct satisfies an abstract interface.

Components

The interface Declaration

An interface defines a set of method signatures that a concrete type must implement to conform to the contract.

Example
0 interface {
1 fn ( ) - > ;
2 // ... more method signatures
3 }

Rules:

An interface block can only contain method signatures. It cannot contain any data fields.

Method signatures within an interface must not have a body. They must end with a semicolon ;.

The self parameter in an interface method must be of a reference type (e.g., &self).

Example
0 interface Serializable {
1 fn serialize ( & self ) - > string ;
2 }

The struct Declaration

A struct defines a concrete data type. Its role is unchanged.

Example
0 struct {
1 : ;
2 // ... more data fields
3 }

Rules:

A struct can only contain data fields. Method implementations are defined separately in impl blocks.

Example
0 struct User {
1 id : int ;
2 username : string ;
3 }

The impl...as... Declaration

This block connects a concrete struct to an interface, proving that the struct fulfills the contract.

Example
0 impl as {
1 // Implementations for all methods required by the interface
2 fn ( ) - > {
3 // ... method body ...
4 }
5 }

Rules:

The impl block must provide a concrete implementation for every method defined in the .

The signature of each implemented method must be compatible with the corresponding signature in the interface.

A single struct may implement multiple interfaces by using separate impl...as... blocks for each one.

Example
0 impl User as Serializable {
1 fn serialize ( & self ) - > string {
2 // Implementation of the serialize method for the User struct
3 return std : : format ( " { { \"id\" : { } , \"username\" : \" { } \" } } " , self . id , self . username ) ;
4 }
5 }

Interface Types

A variable can be declared with an interface type by using a reference. This creates a "trait object" or "fat pointer" that can hold any concrete type that implements the interface.

Syntax: &

Behavior: A variable of type & is a fat pointer containing two components:

A pointer to the instance data (e.g., a &User).

A pointer to the v-table for the specific (Struct, Interface) implementation.

Example
0 let objects : & Serializable [ ] = [
1 & User { id : 1 , username : "aela" } ,
2 & Document { title : "spec . md" }
3 ] ;
4
5 for ( let obj : & Serializable in objects ) {
6 // This call is dynamically dispatched using the v-table.
7 io : : print ( obj . serialize ( ) ) ;
8 }

Formal Verification

This document specifies the design and behavior of Aela's compile-time verification system , which ensures the correctness of a program through formal specifications — namely, invariant and property declarations embedded in impl blocks.

Overview

Aela enables developers to write mathematically precise specifications that describe the expected state and behavior of a program, which the compiler formally verifies at compile time. These specifications are not runtime code — they do not execute, incur no runtime cost, and exist solely to ensure program correctness before code generation.

There are two key constructs:

`invariant`: A safety condition that must always hold before and after each function in an `impl` block. property : A liveness or temporal condition that must hold across all valid execution traces of a type’s behavior.

These declarations allow Aela to verify complex asynchronous and stateful systems using SMT solvers or model checking , without requiring users to learn a separate formal language.

Example
0 struct BankAccount {
1 balance : i32 ;
2 overdraft_limit : u32 ;
3 }
4
5 impl BankAccount {
6 // Always ensure the balance never drops below the allowed overdraft.
7 invariant balanceIsAboveLimit = balance > = - overdraft_limit
8
9 // Eventually, the system must bring the account back to a non-negative balance.
10 property eventuallyInBalance = eventually ( balance > = 0 )
11
12 fn recoverNow ( ) {
13 requires balance < 0
14 ensures balance = = 0
15
16 balance = 0 ;
17 }
18 }

Components

The invariant Declaration

An invariant specifies a condition that must hold before and after every function in an impl block.

Example
0 invariant =

Rules:

Invariants must be side-effect-free boolean expressions. They may reference any field defined in the corresponding struct . The compiler verifies that every function in the `impl` block preserves the invariant. If the compiler cannot prove an invariant holds across all paths, compilation fails.

Example
0 struct Counter { count : int }
1
2 impl Counter {
3 invariant nonNegative = count > = 0
4
5 fn increment ( ) {
6 ensures count = = old ( count ) + 1
7 count = count + 1 ;
8 }
9 }

In this example, the invariant guarantees that the count is never negative. The verifier ensures this remains true even after increment() executes.


The property Declaration

A property expresses a temporal guarantee — such as liveness or ordering — across the program’s behavior.

Example
0 property =

Temporal expressions may include:

`always`: The condition must hold at all times. eventually : The condition must hold at some point in the future. `until`, `release`: Conditions over time and ordering. forall , exists : Quantifiers over a domain (e.g., tasks, states).

Rules:

Properties do not affect control flow or behavior. They are used by the compiler to prove guarantees about possible program traces . * Property violations produce counterexample traces at compile time.

Example
0 struct Queue {
1 items : Array ( int ) ;
2 }
3
4 impl Queue {
5 invariant lengthNonNegative = std : : length ( items ) > = 0
6
7 // Ensure that any task awaiting the queue will eventually see data.
8 property eventuallyReceivesItem = forall task in Tasks {
9 eventually ( ! std : : is_empty ( items ) )
10 }
11
12 async fn dequeue ( ) - > int {
13 while std : : is_empty ( items ) {
14 await wait_for_item ( ) ;
15 }
16 return std : : pop ( items ) ;
17 }
18
19 fn enqueue ( value : int ) {
20 std : : push ( items , value ) ;
21 notify_waiters ( ) ;
22 }
23 }

Specification Behavior

Construct Scope Verified When Runtime Cost
invariant Per-impl block Before and after each fn / async fn None
property Per-impl block Over all valid execution traces None

`invariant` is used for safety ("nothing bad happens"). property is used for liveness ("something good eventually happens").

The compiler treats both as compile-time-only declarations that participate in verification, not execution.


Old State Reference: old(expr)

The old keyword allows a function’s ensures clause to reference the value of an expression before the function was called.

Example
0 fn increment ( ) {
1 ensures count = = old ( count ) + 1
2 count = count + 1 ;
3 }

The compiler ensures that the post-state ( count ) relates correctly to the pre-state ( old(count) ).


Quantifiers and Temporal Blocks

Quantifiers can be used in properties to express conditions across many elements or tasks.

Example
0 forall in {
1
2 }

Compiler Interactions

- Interacting with the compiler
0 ⚠️ [ formal : invariant ] Detected 1 invariant violation during compilation .
1
2 Invariant :
3 balanceIsAboveLimit = balance > = - overdraft_limit
4
5 Violated in :
6 impl BankAccount → fn emergencyReset ( )
7
8 ╭────┬───────────────────────────────────────────────
9 ╭─│ 12 │
10 │ │ 13 │ balance = - 999 ;
11 │ │ │ ^ ^ ^ ^ ^ violates invariant after execution
12 │ │ 14 │
13 │ ╰────┴───────────────────────────────────────────────
14
15 │ 💡 The compiler attempted to prove that `balance >= -overdraft_limit`
16 │ holds before and after each function in `impl BankAccount` .
17
18 │ But after calling `emergencyReset()` , balance = - 999 ,
19 │ and overdraft_limit = 0 ⇒ invariant fails : - 999 > = 0 ❌
20
21 ╰──────────────────────────────────────────────────────────
22
23 Actions :
24
25 1 . 🔍 Explain why this invariant is failing
26 2 . ✏️ Show me the relevant values and trace
27 3 . 🧪 Temporarily disable this invariant ( not recommended )
28 4 . 🧼 Open `emergencyReset` to fix it
29 5 . ❌ Remove the invariant entirely
30 6 . ❓ Ask any arbitrary question ( chat )
31
32 > I guess I don't really understand invariants
33
34 No problem , let's take a look at how they work .
35 The first thing we . . .

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 that p is borrowed, with kind ∈ {shared, unique} .
  • Program Point q : A location in the control-flow graph (CFG).
  • Alive(L, q) : Predicate meaning loan L is still valid at point q (lexically-free semantics).
  • Operations :
  • reads(p, q) : a read of place p at q .
  • writes(p, q) : a write to place p at q .
  • moves(p, q) : a move from place p at q .
  • Aliases(x, p, q) : x holds a reference to p at q (logical predicate, tracked via loan origins).
  • Escape(r, q) : Reference r escapes its region at q (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]) -> &i32 elaborates to for<ρ> 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 marker A .

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: self implicitly labeled Self , but can also be explicitly labeled: self: &Self as S .
  • Diagnostics: Role-based: “return value tied to param a (label A) but a does not live long enough.” Quick fixes: “Add as A to param.”
&Self T { return &self.0; } fn other(self: &Self as S, other: &Self as O) -> &O T { return &other.0; } } fn demo_methods() { let p1: Pair(int) = (1, 2); let p2: Pair(int) = (3, 4); let r1 = p1.first(); let r2 = p1.other(&p2); print(r1); print(r2); } // 4) Higher-order function: label flows through function type fn map_head(xs: &Vec(T) as A, f: Fn(&A T) -> U) -> &A U { return f(&xs[0]); } fn demo_hof() { let nums: Vec(int) = [1, 2, 3]; let head_ref = map_head(&nums, fn(x: &int) -> &int { return x; }); print(head_ref); } // 5) Multiple inputs require label: `as A` disambiguates the return fn pick(a: &T as A, b: &T) -> &A T { // body could choose either; label tells the checker the result is tied to `a` return a; } // 6) Optional: Outlives relation (advanced) fn choose_longer(a: &T as X, b: &T as Y) -> &X T where Y : X { return a; } // 7) Struct constructor with label reuse struct Window(T) { head: &A T, tail: &B T } fn mk_window(xs: &Vec(T) as A, ys: &Vec(T) as B) -> Window(T) { return Window { head: &xs[0], tail: &ys[0] }; } fn demo_window() { let xs: Vec(int) = [1,2,3]; let ys: Vec(int) = [4,5,6]; let w = mk_window(&xs, &ys); print(w.head); print(w.tail); } // 8) Diagnostics-friendly failure (commented): // fn bad_pick(a: &T, b: &T) -> &T { // if cond { return a; } else { return b; } // // Error: ambiguous return lifetime. Add `as A` to a (or b) and return `&A T`. // }" lang="aela" title="Example" id="bd62fb2cf27e9">
Example
0 // 1) Disambiguate which input a returned reference is tied to
1 fn first_of_two ( a : & T as A , b : & T ) - > & A T {
2 return a ;
3 }
4
5 fn demo_first_of_two ( ) {
6 let x : int = 10 ;
7 let y : int = 20 ;
8 let rx = & x ;
9 let ry = & y ;
10
11 let r = first_of_two ( rx , ry ) ;
12 print ( r ) ;
13 }
14
15 // 2) Struct field referencing an argument via label
16 struct View ( T ) { data : & A T }
17
18 fn make_view ( x : & T as A ) - > View ( T ) {
19 return View { data : x } ;
20 }
21
22 fn demo_view ( ) {
23 let s : string = "hello" ;
24 let v = make_view ( & s ) ;
25 print ( v . data ) ;
26 }
27
28 // 3) Methods: implicit Self label + explicit labels to disambiguate
29 impl Pair ( T ) {
30 fn first ( self : & Self ) - > & Self T {
31 return & self . 0 ;
32 }
33
34 fn other ( self : & Self as S , other : & Self as O ) - > & O T {
35 return & other . 0 ;
36 }
37 }
38
39 fn demo_methods ( ) {
40 let p1 : Pair ( int ) = ( 1 , 2 ) ;
41 let p2 : Pair ( int ) = ( 3 , 4 ) ;
42
43 let r1 = p1 . first ( ) ;
44 let r2 = p1 . other ( & p2 ) ;
45 print ( r1 ) ;
46 print ( r2 ) ;
47 }
48
49 // 4) Higher-order function: label flows through function type
50 fn map_head ( xs : & Vec ( T ) as A , f : Fn ( & A T ) - > U ) - > & A U {
51 return f ( & xs [ 0 ] ) ;
52 }
53
54 fn demo_hof ( ) {
55 let nums : Vec ( int ) = [ 1 , 2 , 3 ] ;
56 let head_ref = map_head ( & nums , fn ( x : & int ) - > & int { return x ; } ) ;
57 print ( head_ref ) ;
58 }
59
60 // 5) Multiple inputs require label: `as A` disambiguates the return
61 fn pick ( a : & T as A , b : & T ) - > & A T {
62 // body could choose either; label tells the checker the result is tied to `a`
63 return a ;
64 }
65
66 // 6) Optional: Outlives relation (advanced)
67 fn choose_longer ( a : & T as X , b : & T as Y ) - > & X T where Y : X {
68 return a ;
69 }
70
71 // 7) Struct constructor with label reuse
72 struct Window ( T ) { head : & A T , tail : & B T }
73
74 fn mk_window ( xs : & Vec ( T ) as A , ys : & Vec ( T ) as B ) - > Window ( T ) {
75 return Window { head : & xs [ 0 ] , tail : & ys [ 0 ] } ;
76 }
77
78 fn demo_window ( ) {
79 let xs : Vec ( int ) = [ 1 , 2 , 3 ] ;
80 let ys : Vec ( int ) = [ 4 , 5 , 6 ] ;
81 let w = mk_window ( & xs , & ys ) ;
82 print ( w . head ) ;
83 print ( w . tail ) ;
84 }
85
86 // 8) Diagnostics-friendly failure (commented):
87 // fn bad_pick(a: &T, b: &T) -> &T {
88 // if cond { return a; } else { return b; }
89 // // Error: ambiguous return lifetime. Add `as A` to a (or b) and return `&A T`.
90 // }

Lifetimes in Data Structures

  • Reference fields implicitly carry region parameters. struct Node { data: &T } elaborates to struct Node<ρ, T> { data: &ρ T } .
  • Construction instantiates ρ from the argument’s region. Error if struct outlives the referenced data.

Place Overlap (I3)

  • p overlaps p .
  • p overlaps p.f . Distinct fields disjoint.
  • arr[i] vs arr[j] : disjoint if indices are distinct compile-time constants; else conservative overlap.
  • Library intrinsics like split_at_mut provide 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 await unless the origin outlives the entire future. Practically: locals cannot cross await ; only borrows from captured fields of the async task can.
  • Phase 2: Desugar async to 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

Example
0 let s : string = "hi" ;
1 let r = & s ; // borrow starts
2 print ( r ) ; // last use of r
3 var t = s ; // OK allowed: r’s region ended at last use (lexically-free)

L1 — Loan Creation

Example
0 var x : int = 0 ;
1 let r = & x ; // L(x, shared)
2 let m = & mut x ; // Not OK conflict later under A1, but creation itself establishes L(x, unique)

L2 — Loan Propagation

Example
0 fn id_ref ( p : & int ) - > & int { p }
1 let x : int = 1 ;
2 let r1 = & x ; // L(x, shared)
3 let r2 = id_ref ( r1 ) ; // loan propagates to r2 until last use
4 print ( r2 ) ; // OK!

A1 — Unique Exclusivity

Example
0 var v : int = 0 ;
1 let m = & mut v ; // L(v, unique)
2 let r = & v ; // Not OK! A1: unique excludes any concurrent shared borrow

A2 — Shared Read‑Only

Example
0 var n : int = 0 ;
1 let a = & n ; // L(n, shared)
2 let b = & n ; // another shared loan
3 print ( * a + * b ) ; // OK! reads allowed
4 n = 5 ; // Not OK! A2: write while shared loans alive

I1 — Write Invalidates

Example
0 var s : string = "hi" ;
1 let r = & s ; // L(s, shared)
2 print ( r ) ;
3 s . push ( " ! " ) ; // Not OK! I1: write to s while shared loan alive (if r not yet last‑used)

I2 — Move Invalidates

Example
0 var s : string = "a" ;
1 let r = & s ; // L(s, shared)
2 var t = s ; // Not OK! I2: move invalidates loans; r would dangle

I3 — Overlap/Fields

Example
0 struct P { x : int , y : int }
1 var p : P = P { x : 1 , y : 2 } ;
2 let mx = & mut p . x ; // L(p.x, unique)
3 let my = & mut p . y ; // OK, disjoint fields: allowed
4
5 var arr : [ int ; 3 ] = [ 0 , 1 , 2 ] ;
6 let a = & mut arr [ 0 ] ;
7 let b = & mut arr [ 1 ] ; // not OK, if indices distinct constants
8 let i = 0 ; let j = read_index ( ) ;
9 let c = & mut arr [ i ] ;
10 let d = & mut arr [ j ] ; // Not OK conservatively, may overlap unless proven disjoint (use split helpers)

U1 — Use Requires Alive

Example
0 let s : string = "hi" ;
1 let r = & s ; // L(s, shared)
2 print ( r ) ; // OK! use while alive
3 // after last use r ends; any further use would be not OK (dead borrow)

U2 — No After‑Free

Example
0 fn bad ( ) {
1 let s : string = "x" ;
2 let r = & s ;
3 drop ( s ) ; // base place dead
4 print ( r ) ; // Not OK: U2: use after base death
5 }

RB1 — Shared from Unique

Example
0 var v : int = 0 ;
1 let mu = & mut v ; // L(v, unique)
2
3 // Shared reborrow of the same place (auto-deref from &mut int to int)
4 let rs : & int = & mu ; // creates L(v, shared)
5
6 print ( rs ) ;
7
8 // Write through the unique borrow (auto-deref on assignment)
9 mu = 3 ; // must end shared loan before writing via `mu`

RB2 — Unique from Unique (optional v1)

Example
0 var v : int = 0 ;
1 let mu1 = & mut v ;
2 let mu2 = & mut mu1 ; // unique reborrow of v; allowed only if no overlapping use via mu1

E1 — No Escaping Borrows

Example
0 fn leak_ref ( ) - > & int {
1 let x : int = 1 ;
2 return & x ; // Not OK! Escapes to caller; Not OK, dies at function end
3 }
4
5 fn head ( s : & string ) - > & char {
6 return & s [ 0 ] ;
7 } // OK! Summary ties return to arg

M1 — Binding Mutability

Example
0 let x : int = 0 ;
1 let rx = & mut x ; // Not OK: M1: &mut requires mutable base
2 var y : int = 0 ;
3 let ry = & mut y ; // OK

M2 — No Shared Mutability Without Atomics

Example
0 shared var n : int = 0 ; // shared location
1 let r1 = & n ; let r2 = & n ; // shared borrows
2 n = n + 1 ; // Not OK concurrent write without atomic discipline
3 atomic var a : int = 0 ; // with atomic, writes are allowed by policy

T1 — Temporaries

Example
0 print ( & ( make_string ( ) ) ) ; // OK temporary lives through call; reference dies at last use
1 let r = & ( make_string ( ) ) ;
2 print ( r ) ; // Not OK if r would outlive the temporary’s last use

T2 — Branches/Loops/Match

Example
0 let s : string = "x" ;
1 let r = & s ;
2 if cond { print ( r ) ; } else { print ( r ) ; }
3 // r is used on both paths; live set at join = intersection ⇒ still alive until after the if
4 var t = s ; // not OK must occur after r’s last use on all paths

F1 — FFI Preconditions

Example
0 ffi puts : ( & char ) - > int = . . . ;
1 let s : string = "hi" ;
2 let r = & s [ 0 ] ;
3 puts ( r ) ; // OK if FFI summary: does not retain; Not OK if it may retain (escape)

F2 — FFI Postconditions

Example
0 ffi c_strchr : ( & char , int ) - > & char = . . . ; // trusted: result aliases input
1
2 fn first_a ( s : & string ) - > & char {
3 c_strchr ( & s [ 0 ] , 'a' ) // OK allowed only because summary ties result to arg region
4 }

RFT1 — Refinement Well‑Formedness

Example
0 let x : int = 5 ;
1 let y : { z : & int where initialized ( z ) } = { z : & x } ; // OK predicate references lifetime‑relevant property

RFT2 — Discharge at Use Sites

Example
0 fn show ( p : { z : & int where not_escaped ( z ) } ) - > void {
1 print ( p . z ) ; // OK! checker discharges `not_escaped` from current loan facts
2 }

inter‑Procedural Summary (Elision)

Example
0 fn get_first ( a : & [ int ] ) - > & int {
1 return & a [ 0 ] ;
2 }
3
4 // elaborated internally: for<ρ> fn(&ρ [int]) -> &ρ int
5 // caller instantiates ρ from its argument; return tied to same ρ

Structs with Reference Fields (Implicit Regions)

Example
0 struct Node ( T ) { data : & T } // elaborates to Node<ρ, T>
1
2 fn demo ( ) - > void {
3 let x : int = 1 ;
4 var n : Node ( int ) = Node { data : & x } ; // OK n: Node<ρx, int>
5 }
6
7 fn bad ( ) - > void {
8 var n : Node ( int ) ;
9
10 {
11 let x : int = 1 ;
12 n = Node { data : & x } ; // Not OK n outlives x ⇒ region check fails
13 }
14 }

Async Phase 1 — No Unique Loans Across Await

Example
0 async fn step ( mut v : & int ) - > void {
1 let m = v ; // borrow unique via parameter
2 await tick ( ) ; // Not OK, unique loan across await in phase 1
3 }

Closures — Capture Classification

void { move x }; // by‑value move ⇒ FnOnce" lang="aela" title="Example" id="de7b5ce457c5">
Example
0 var n : int = 0 ;
1 let c1 = fn ( ) - > void { print ( & n ) ; } ; // shared borrow capture ⇒ Fn
2 let c2 = fn ( ) - > void { n = n + 1 ; } ; // unique borrow capture ⇒ FnMut
3 let x : string = "hi" ;
4 let c3 = fn ( ) - > void { move x } ; // by‑value move ⇒ FnOnce

C/C++ Harmony

Most languages' safty stops at the FFI boundary. But by automating the creation of safe FFI boundaries and embedding more of the C/C++ code's contracts directly into Aela's type system. Instead of just marking a boundary as unsafe and leaving the safety burden entirely on the developer, Aela can actively assist in verifying the C/C++ side of the interaction.

Automatically Generate "Diplomatic" Wrappers

Instead of manually writing write unsafe blocks and wrapper functions (This is tedious and error-prone). Aela can automate this.

Aela Compile can parse C/C++ header files (.h, .hpp) and automatically generatee the FFI bindings and a safe, idiomatic Aela wrapper. It's much more sophisticated than a simple binding generator.

Contract Inference

The tool can analyze C++ code for clues about contracts.

For instance, it can interpret a gsl::not_null or _Nonnull annotation as a non-nullable reference in Aela, automatically adding the necessary runtime checks.

Resource Management

If a C function create_foo() returns a pointer that must be freed with destroy_foo() , the generator can automatically create a smart pointer or RAII object in Aela that calls destroy_foo() on scope exit. This eliminates a huge class of resource leak bugs.

Error Handling

It can translate C-style error codes e.g., return -1; or errno = EINVAL; into Aela's native error-handling mechanism, like Result types or exceptions.

This moves the burden from the developer having to manually ensure safety to the too providing a verifiably safe starting point.

A Type System for C/C++ Interop

Aela's type system can understand C/C++'s quirks better. It encodes invariants about C pointers and memory directly into the types.

Sized Pointers

Instead of a raw pointer, Aela has types like Pointer(T, size_t N), which represent a pointer to a buffer of N elements. This allows the compiler to enforce bounds checking at the FFI boundary.

Nullability and Ownership

Explicitly differentiate between Pointer(T) (nullable) and Reference(T) (non-nullable). And types that encode ownership semantics like OwnedPointer(T) (must be freed) vs. BorrowedPointer(T) (must not be freed).

Tainted Data

Data coming from C/C++ is considered "tainted" by the type system. It needs to be explicitly validated (e.g., checking a string for valid UTF-8, ensuring a value is within an expected range) before it can be used in the safe context.

Integrate Static and Dynamic Analysis

Since Aela is written in C, it can integrate powerful C/C++ analysis tools directly into the build process.

Clang's Analyzers

Aela uses libraries from the Clang/LLVM project to perform static analysis on the C/C++ code. During compilation, it automatically invokes analyzers to check for things like null pointer dereferences, use-after-free, or buffer overflows in the C code being called, and flag a warning or error if a potential issue is found.

Boundary Sanitization

A "debug mode" for FFI that injects runtime checks at the boundary.

When your code calls a C function, it could automatically add canaries or check buffer boundaries. When C code calls back into Aela, it can validate incoming pointers and data. This is similar to running with AddressSanitizer (ASan), but it's focused specifically on the FFI-boundary risks.

By taking these steps, Aela doesn't just stop its safety guarantees at the boundary. It actively polices that boundary, making brownfield integration significantly safer and more robust than the manual, high-discipline approach required by other languages.

FFI

The Foreign Function Interface (FFI) provides a mechanism for Aela code to call functions written in other programming languages, specifically C. This allows you to leverage existing C libraries, write performance-critical code in a lower-level language, or interact directly with the underlying operating system.

The core of Aela's FFI is the ffi definition, which declares a external C functions and their Aela type signatures or varibales and their types. The Aela compiler and runtime use these declarations to handle the "marshalling" of data—the process of converting data between Aela's internal representations and the C Application Binary Interface (ABI).

Declaring an FFI type

You declare a C function or C variable using the ffi keyword.

Example
0 ffi foo = fn ( string ) - > void ;
1 ffi bar = u32 ;

ABI Contract

A stable C ABI ( ae_c_abi.h ) defines the contract. It specifies the C-side string representation: typedef struct { char* ptr; int64_t len; } AeString;

Compiler Type Mapping

The Aela compiler's types.c maps the language's string type to an LLVM struct with an identical memory layout: %aela.string = type { i8*, i64 } .

Passing Convention

Strings are passed to C functions BY VALUE.

  • Aela code generates: call void @c_function(%aela.string %my_string) .
  • C code receives: void c_function(AeString my_string) .

Safety & Ownership

  • This pass-by-value convention is a "defensive" design.
  • The C function gets a copy of the string descriptor, preventing it
  • from modifying the original string's length or pointer in Aela's
  • memory.
  • Aela's runtime retains ownership of the underlying character buffer
  • ( char* ). The AeString struct is just a temporary, non-owning view.
Example
0 ffi = ; . . .

: The exact name of the function as it is defined in the C source code.

: The Aela type signature for the C function. This signature is the crucial contract that tells the Aela compiler how to call the C function correctly.

Example

Let's look at the stdio example from the standard library:

Example
0 ffi ae_stdout_write = fn ( string ) - > void ;

This code does the following:

It declares that there is an external C function named ae_stdout_write.

It specifies that from the Aela side, this function should be treated as one that accepts a single Aela string and returns void.

To call this function, you use the standard module access syntax:

Example
0 ae_stdout_write ( "Hello from C ! " ) ;

The Aela-C ABI and Data Marshalling When an FFI call occurs, the Aela compiler generates "glue" code to translate Aela types into types that C understands. This mapping follows a specific Application Binary Interface (ABI).

Primitive Types

Most Aela primitive types map directly to their C equivalents.

Aela Type C Type
i8, u8 int8_t, uint8_t
i16, u16 int16_t, uint16_t
i32, u32 int32_t, uint32_t
i64, u64 int64_t, uint64_t
f32 float
f64 double
bool bool (or \_Bool)
char uint32_t (UTF-32)
void void

Strings

The Aela string is a "fat pointer" struct containing a pointer to the data and a length. C, however, typically works with null-terminated char* strings.

Aela to C: When you pass an Aela string to an FFI function, the compiler automatically extracts the internal ptr and passes it as a const char* to the C function. The string data is guaranteed to be null-terminated, so standard C string functions can operate on it safely.

Aela's Internal runtime representation
0 struct string {
1 ptr : ptr , // Pointer to UTF-8 data
2 len : i64 // Length of the string
3 }

FFI Call:

The Aela Code
0 ae_stdout_write ( "Hello" ) ;

The C function receives a standard C string.

The C Implementation
0 void ae_stdout_write ( const char * message ) {
1 printf ( " % s" , message ) ;
2 }

Structs, Arrays, and Closures (Complex Types)

Complex aggregate types like structs, arrays, and closures cannot be passed directly by value to C functions. The ABI for these types is simple: you pass a pointer.

Aela to C: When passing a complex type, Aela passes a pointer to the object's memory layout. Your C code receives an opaque pointer (void\*) to this data. It is your responsibility in C to know the memory layout of the Aela type and cast the pointer accordingly to access its fields.

This is an advanced use case and requires careful handling to avoid memory corruption. You must ensure that the struct definition in your C code exactly matches the memory layout of the Aela struct.

Often you end up with an opaque strct in Aela. These can not have methods or properties.

An Opaque Struct
0 struct StringBuilder ;
1 ``
2
3 ## Variadic Functions (...args)
4
5 Variadic arguments are not directly passed through the FFI boundary . The . . . args
6 feature is part of the Aela language and its calling convention , not the C ABI .
7
8 As seen in the io . print example , you must handle variadic arguments within your
9 Aela code and call the FFI function with a concrete , non - variadic signature .
10
11 `` `example
12 // The public-facing Aela function is variadic.
13
14 export fn print ( formatString : string , . . . args ) - > void {
15 stdio : : ae_stdout_write ( std : : format ( formatString , . . . args ) ) ;
16 }

This design provides a safe and clear boundary. The complex, type-safe variadic handling happens within the Aela runtime, while the FFI call itself remains a simple, direct translation of the string argument to a char*.

Linking C Code To make your C functions available to the Aela compiler, you must compile them into an object file (.o) or a library (.a, .so, .dylib) and include it during the final linking step.

The Aela driver will eventually provide flags to specify these external object files. For now, you would typically use a command like clang to link the Aela-generated object file with your C object file.

  1. Compile your Aela code aec your_program.ae -o your_program.o
  1. Compile your C code clang -c my_ffi_functions.c -o my_ffi_functions.o
  1. Link them together clang your_program.o my_ffi_functions.o -o
  2. final_executable

This process creates the final executable where the Aela runtime can find and call your C functions.

Memory & Mutability Model

1. Default Allocation Semantics

  • All values start on the stack by default.
  • Stack allocations are:
  • Fast
  • Scoped
  • Owned directly by the binding.
Example
0 let foo : Foo = { num : 0 } ; // Foo is stack-allocated

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.

Example
0 let x : & T = new { . . . val } ; // Allocate on the heap
1 let x : & T = new shared { . . . val } ; // Single-threaded, mutable
2 let x : & T = new shared atomic { . . . val } ; // Thread-safe atomic
3 var x : & T = new weak { . . . val } ; // Mutable weak reference

Behavior

  • 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)
  • There is no `mut` keyword
  • Mutability is not encoded in types, structs, or fields

Example

Example
0 let a : & Foo = new shared { . . . foo } ; // Immutable heap reference
1 var b : & Foo = new shared { . . . foo } ; // Mutable heap reference

Attempting to mutate a let -bound reference results in a compile-time error .

4. Weak References & Cycle Prevention [NOT IMPLEMENTED]

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'

Example
0 if let strong_ref = weak_ref {
1 // This block only runs if the object is still alive.
2 // 'strong_ref' is a new, temporary strong reference of type &Foo.
3 strong_ref . do_something ( ) ;
4 }

5. Copying Stack Values

  • Heap allocation requires copying the stack value:
Example
0 new shared { . . . foo } // `{ ...foo }` performs a field-wise copy
  • 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.
a copy of the number 10 is passed. // bar(stack_str); -> a copy of the {ptr, i64} struct is passed." lang="example" title="- Stack Allocated" id="d4bdebbe552a">
- Stack Allocated
0 let stack_int : i32 = 10 ; // The variable 'stack_int' IS the number 10.
1 let stack_str : string = "hello" ; // The variable 'stack_str' IS the {ptr, i64} struct.
2
3 // When calling a function:
4 // foo(stack_int); -> a copy of the number 10 is passed.
5 // bar(stack_str); -> a copy of the {ptr, i64} struct is passed.

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.
- Heap allocated via new
0 let heap_int : i32 = new 42 ; // 'heap_int' is a POINTER to a box containing 42.
1 let heap_obj : MyStruct = new { } ; // 'heap_obj' is a POINTER to a box containing a MyStruct.
2 let heap_arr : u8 [ ] = new [ 1 , 2 , 3 ] ; // 'heap_arr' is a POINTER to a box for the array data.
3
4 // When calling a function:
5 // foo(heap_int); -> a copy of the POINTER is passed. The heap data is not touched.

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. The env_ptr points 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.
- Special Case
0 let captured_var = new "text" ;
1
2 let my_closure = fn ( ) {
3 print ( captured_var ) ; // Captures the pointer 'captured_var'.
4 } ;
5
6 // 'my_closure' is a {func_ptr, env_ptr} struct.
7 // 'env_ptr' points to a heap box which contains a copy of the 'captured_var' pointer.

GUI

Aela provides a cross platform GUI library that works on MacOS, iOS, Windows, Linux, & Android.

Install

Install the module into your dependencies.
0 aec install stablestate / ui
Your index.json will then have the dependency.
0 {
1 "name" : "my - program" ,
2 "version" : "0 . 1 . 0" ,
3 "entry" : "main . ae" ,
4 "output" : "build / app" ,
5 "include" : [ "src /**/ * . ae" ] ,
6 "exclude" : [ "tests / * * " ] ,
7 "dependencies" : [
8 {
9 "name" : "ui" ,
10 "url" : "https : / / github . com / stablestate / ui"
11 }
12 ]
13 }

Example

int { fn App () -> &View { let onclick: EventCallback = fn (event: &Event) -> void { // do something }; return new View { children: [ WebView { url: "https://news.ycombinator.com" }, Button { label: "save" }, Button { label: "cancel", onclick } ], onclick }; } let app: &View = App(); let loop: &io::AeEventLoop = io::getLoop(); initLoop(loop); let windowOptions: &WindowOptions = new { title: "Aela UI Test", width: 800.0, height: 600.0 }; let window: &Window = new Window(windowOptions); window.show(); window.render(app); runLoop(); return 0; }" lang="ae" title="Getting started" id="5286b485edc61">
Getting started
0 import {
1 initLoop ,
2 runLoop ,
3 WindowOptions ,
4 Window
5 } from "ui" ;
6
7 async fn main ( args : string [ ] ) - > int {
8 fn App ( ) - > & View {
9 let onclick : EventCallback = fn ( event : & Event ) - > void {
10 // do something
11 } ;
12
13 return new View {
14 children : [
15 WebView { url : "https : / / news . ycombinator . com" } ,
16 Button { label : "save" } ,
17 Button { label : "cancel" , onclick }
18 ] ,
19 onclick
20 } ;
21 }
22
23 let app : & View = App ( ) ;
24
25 let loop : & io : : AeEventLoop = io : : getLoop ( ) ;
26 initLoop ( loop ) ;
27
28 let windowOptions : & WindowOptions = new {
29 title : "Aela UI Test" ,
30 width : 800 . 0 ,
31 height : 600 . 0
32 } ;
33
34 let window : & Window = new Window ( windowOptions ) ;
35
36 window . show ( ) ;
37 window . render ( app ) ;
38
39 runLoop ( ) ;
40
41 return 0 ;
42 }

Why 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.

Formal Grammar Spec

Grammar
0 ( * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = )
1 ( Aela Language Grammar 0 . 0 . 6 )
2 ( Finalized : 2025 - 09 - 06 )
3 ( = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = * )
4
5 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
6 ( PROGRAM )
7 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
8
9 Program : : = { TopLevelDeclaration } EOF
10
11 TopLevelDeclaration : : =
12 ImportStatement
13 | ReExportDeclaration
14 | [ KW_EXPORT ] (
15 FfiDeclaration
16 | VarDeclaration
17 | FunctionDeclaration
18 | StructDeclaration
19 | ImplBlock
20 | EnumDeclaration
21 | TypeAliasDeclaration
22 | SystemDeclaration
23 )
24
25 TypeAliasDeclaration : : = KW_TYPE IDENTIFIER '=' Type ';'
26
27 ReExportDeclaration : : =
28 KW_EXPORT ( NamedImport | IDENTIFIER )
29 KW_FROM STRING_LITERAL ';'
30
31 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
32 ( IMPORTS )
33 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
34
35 ImportStatement : : =
36 KW_IMPORT ( NamedImport | IDENTIFIER )
37 KW_FROM STRING_LITERAL ';'
38
39 NamedImport : : = '{' [ ImportSpecifier { ',' ImportSpecifier } [ ',' ] ] '}'
40 ImportSpecifier : : = IDENTIFIER [ ':' IDENTIFIER ]
41
42 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
43 ( FFI ( Foreign Function Interface ) )
44 ( - Contracts are compile - time enforced to be UB - free )
45 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
46
47 FfiDeclaration : : = KW_FFI IDENTIFIER '=' Type ';'
48
49 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
50 ( DECLARATIONS )
51 ( - var is mutable , let is immutable )
52 ( - aliases are borrow - checked by the analyzer )
53 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
54
55 VarDeclaration : : = ( KW_VAR | KW_LET ) IDENTIFIER ':' Type
56 [ '=' Expression ] ';'
57
58 StructDeclaration : : = KW_STRUCT IDENTIFIER [ GenericParameters ] ( '{' { StructFieldDeclaration } '}' | ';' )
59
60 StructFieldDeclaration : : =
61 ( IDENTIFIER ':' Type ';' )
62 | ( '...' IDENTIFIER ';' )
63
64 FnModifiers : : =
65 [ ( KW_THREAD [ KW_PURE ] ) // thread; pure+thread allowed
66 | ( KW_PURE [ KW_THREAD ] ) // pure; optional thread
67 | KW_ASYNC // async alone (no pure)
68 ]
69
70 ImplBlock : : = KW_IMPL Type [ GenericParameters ] '{' { FunctionDeclaration | InvariantDeclaration } '}'
71
72 FunctionDeclaration : : =
73 FnModifiers KW_FN IDENTIFIER [ GenericParameters ]
74 '(' [ FunctionParameters ] ')' '->' Type ( Block | ';' )
75
76 EnumDeclaration : : = KW_ENUM IDENTIFIER [ GenericParameters ]
77 '{' [ EnumVariant { ',' EnumVariant } [ ',' ] ] '}'
78
79 EnumVariant : : = IDENTIFIER [ '(' Type { ',' Type } ')' ]
80
81 GenericParameters : : = '(' IDENTIFIER { ',' IDENTIFIER } ')'
82
83 SystemDeclaration : : = KW_SYSTEM IDENTIFIER '{'
84 { ActionDeclaration
85 | InvariantDeclaration
86 | PropertyDeclaration
87 }
88 '}'
89
90 ActionDeclaration : : = KW_ACTION IDENTIFIER
91 '(' [ FunctionParameters ] ')'
92 [ RequiresClause ]
93 [ EnsuresClause ]
94 Block
95
96 RequiresClause : : = KW_REQUIRES Expression
97 EnsuresClause : : = KW_ENSURES Expression
98
99 InvariantDeclaration : : = KW_INVARIANT IDENTIFIER ':' Expression
100 PropertyDeclaration : : = KW_PROPERTY IDENTIFIER ':' TemporalExpression
101
102 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
103 ( TYPES )
104 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
105
106 Type : : = [ '&' ] PostfixType
107
108 PostfixType : : = SimpleType { ArrayTypeModifier | TypeApplication | '?' }
109
110 MapType : : = KW_MAP '(' Type ',' Type ')'
111
112 SimpleType : : = PrimitiveType
113 | KW_VOID
114 | FunctionTypeSignature
115 | PathExpression
116 | MapType
117 | RefinementType
118 | '(' Type ')'
119
120 FunctionTypeSignature : : =
121 FnModifiers KW_FN '(' [ FunctionTypeParameters ] ')' '->' Type
122
123 RefinementType : : = '{' IDENTIFIER ':' Type KW_WHERE Expression '}'
124
125 PrimitiveType : : = KW_U8 | KW_I8 | KW_U16 | KW_I16 | KW_U32 | KW_I32
126 | KW_U64 | KW_I64 | KW_F32 | KW_F64 | KW_BOOL
127 | KW_CHAR | KW_STRING | KW_INT
128
129 TypeApplication : : = '(' [ Type { ',' Type } ] ')'
130
131 FunctionParameters : : = Parameter { ',' Parameter }
132 Parameter : : = [ '...' ] [ KW_MUT ] IDENTIFIER ':' Type
133 FunctionTypeParameters : : = FunctionTypeParameter { ',' FunctionTypeParameter }
134 FunctionTypeParameter : : = [ KW_MUT ] Type
135 ArrayTypeModifier : : = '[' [ Expression ] ']'
136
137 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
138 ( COMMENTS )
139 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
140
141 ( * A single - line comment starts with // and continues to the end of the line )
142 SingleLineComment : : = '//' { ~('\n' | '\r') }
143
144 ( * A multi - line comment starts with /* and ends with */ )
145 MultiLineComment : : = '/*' { . } '*/'
146
147 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
148 ( STATEMENTS ( Unambiguous ) )
149 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
150
151 Statement : : = MatchedStatement | UnmatchedStatement
152
153 MatchedStatement : : =
154 KW_IF '(' Expression ')' MatchedStatement KW_ELSE MatchedStatement
155 | KW_IF KW_LET Pattern '=' Expression Block KW_ELSE MatchedStatement
156 | Block
157 | KW_RETURN [ Expression ] ';'
158 | BreakStatement
159 | ContinueStatement
160 | WhileStatement
161 | ForStatement
162 | MatchStatement
163 | ExpressionStatement
164 | VarDeclaration
165 | FunctionDeclaration
166 | ';'
167
168 UnmatchedStatement : : =
169 KW_IF '(' Expression ')' Statement
170 | KW_IF '(' Expression ')' MatchedStatement KW_ELSE UnmatchedStatement
171 | KW_IF KW_LET Pattern '=' Expression Block
172 | KW_IF KW_LET Pattern '=' Expression Block KW_ELSE UnmatchedStatement
173
174 Block : : = '{' { Statement } '}'
175 ExpressionStatement : : = Expression ';'
176 BreakStatement : : = KW_BREAK ';'
177 ContinueStatement : : = KW_CONTINUE ';'
178
179 WhileStatement : : = KW_WHILE '(' Expression ')' Statement
180
181 ForStatement : : = KW_FOR '(' ForDeclaratorList KW_IN Expression ')' Statement
182 ForDeclaratorList : : = ForDeclarator { ',' ForDeclarator }
183 ForDeclarator : : = ( KW_LET | KW_VAR ) IDENTIFIER ':' Type
184
185 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
186 ( MATCH ( Mandatory Exhaustive ) )
187 ( - Expression form for atomic initialization . )
188 ( - Statement form for control flow . )
189 ( - Guards , @ - bindings , and nesting are disallowed . )
190 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
191
192 MatchStatement : : = KW_MATCH '(' Expression ')' '{'
193 [ MatchStmtArm { ',' MatchStmtArm } [ ',' ] ]
194 '}'
195
196 MatchStmtArm : : = MatchArmPattern '=>' ( Block | ';' )
197
198 MatchArmPattern : : = Pattern { '|' Pattern }
199
200 Pattern : : =
201 LiteralPattern
202 | IDENTIFIER // binding
203 | '_' // wildcard
204 | PathExpression [ '(' [ PatternList ] ')' ] // unit/tuple-variant
205 | TuplePattern
206 | StructPattern
207
208 TuplePattern : : = '(' [ PatternList [ ',' ] ] ')'
209
210 StructPattern : : = '{' [ StructFieldPat { ',' StructFieldPat } [ ',' ] ] '}'
211 StructFieldPat : : = IDENTIFIER ( ':' Pattern ) ? | '...' IDENTIFIER
212
213 PatternList : : = Pattern { ',' Pattern }
214
215 RangePattern : : =
216 INT_LITERAL ( '..' | '..=' ) INT_LITERAL
217 | CHAR_LITERAL ( '..' | '..=' ) CHAR_LITERAL
218
219 LiteralPattern : : = INT_LITERAL | STRING_LITERAL | CHAR_LITERAL | KW_TRUE | KW_FALSE | RangePattern
220
221 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
222 ( EXPRESSIONS ( Pratt Parser Aligned ) )
223 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
224
225 Expression : : = AssignmentExpression
226
227 AssignmentExpression : : = CoalescingExpression [ '=' AssignmentExpression ]
228
229 CoalescingExpression : : = LogicalOrExpression { '??' LogicalOrExpression }
230
231 LogicalOrExpression : : = LogicalAndExpression { '||' LogicalAndExpression }
232
233 LogicalAndExpression : : = BitwiseOrExpression { '&&' BitwiseOrExpression }
234
235 BitwiseOrExpression : : = BitwiseXorExpression { '|' BitwiseXorExpression }
236
237 BitwiseXorExpression : : = BitwiseAndExpression { '^' BitwiseAndExpression }
238
239 BitwiseAndExpression : : = ShiftExpression { '&' ShiftExpression }
240
241 ShiftExpression : : = EqualityExpression { ( '<<' | '>>' ) EqualityExpression }
242
243 EqualityExpression : : = ComparisonExpression { ( '==' | '!=' ) ComparisonExpression }
244
245 ComparisonExpression : : = AdditiveExpression { ( '<' | '<=' | '>' | '>=' ) AdditiveExpression }
246
247 AdditiveExpression : : = MultiplicativeExpression { ( '+' | '-' ) MultiplicativeExpression }
248
249 MultiplicativeExpression : : = CastExpression { ( '*' | '/' | '%' ) CastExpression }
250
251 CastExpression : : = UnaryExpression { KW_AS Type }
252
253 UnaryExpression : : =
254 ( KW_AWAIT | '-' | '!' | '&' | '~' ) UnaryExpression
255 | PostfixExpression
256
257 PostfixExpression : : =
258 PrimaryExpression {
259 '(' [ ArgumentList ] ')'
260 | '[' Expression ']'
261 | ( '.' | '?.' ) IDENTIFIER
262 }
263
264 PrimaryExpression : : =
265 PathExpression
266 | Literal
267 | '(' Expression ')'
268 | ArrayLiteral
269 | NamedStructLiteral
270 | AnonymousStructLiteral
271 | FunctionExpression
272 | NewExpression
273 | MatchExpression
274
275 MatchExpression : : = KW_MATCH '(' Expression ')' '{'
276 [ MatchExprArm { ',' MatchExprArm } [ ',' ] ]
277 '}'
278
279 MatchExprArm : : = MatchArmPattern '=>' Expression
280
281 PathExpression : : = IDENTIFIER { '::' IDENTIFIER }
282
283 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
284 ( TEMPORAL EXPRESSIONS )
285 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
286
287 TemporalExpression : : =
288 KW_ALWAYS Expression
289 | KW_EVENTUALLY Expression
290 | KW_NEXT Expression
291 | Expression KW_UNTIL Expression
292 | Expression KW_RELEASE Expression
293 | KW_FORALL IDENTIFIER KW_IN Expression ':' Expression
294 | KW_EXISTS IDENTIFIER KW_IN Expression ':' Expression
295 | Expression
296
297 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
298 ( LITERALS & HELPER RULES )
299 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
300
301 Literal : : =
302 INT_LITERAL | FLOAT_LITERAL | STRING_LITERAL | STRING_MULTILINE
303 | CHAR_LITERAL | KW_TRUE | KW_FALSE
304
305 ArrayLiteral : : = '[' [ ArgumentList ] ']'
306 NamedStructLiteral : : = PathExpression StructLiteralBody
307 AnonymousStructLiteral : : = StructLiteralBody
308 StructLiteralBody : : = '{' [ StructElement { ',' StructElement } [ ',' ] ] '}'
309 StructElement : : = ( IDENTIFIER ':' Expression ) | IDENTIFIER | '...' Expression
310
311 ArgumentList : : = CallArgument { ',' CallArgument }
312 CallArgument : : = [ '...' ] [ KW_MUT ] Expression
313
314 FunctionExpression : : = FnModifiers KW_FN '(' [ FunctionParameters ] ')' [ '->' Type ] Block
315
316 NewExpression : : = KW_NEW PrimaryExpression
317
318 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
319 ( TERMINALS ( TOKENS ) )
320 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
321
322 IDENTIFIER
323 INT_LITERAL , FLOAT_LITERAL , STRING_LITERAL , STRING_MULTILINE , CHAR_LITERAL
324
325 ( * Keywords : * )
326 KW_LET , KW_VAR , KW_FN , KW_IF , KW_IN , KW_ELSE , KW_WHILE , KW_FOR ,
327 KW_RETURN , KW_BREAK , KW_CONTINUE , KW_WHERE ,
328 KW_ASYNC , KW_AWAIT , KW_AS , KW_STRUCT , KW_IMPL , KW_THREAD ,
329 KW_ENUM , KW_MATCH , KW_TYPE , KW_VOID , KW_INT ,
330 KW_U8 , KW_I8 , KW_U16 , KW_I16 , KW_U32 , KW_I32 , KW_U64 , KW_I64 ,
331 KW_F32 , KW_F64 , KW_BOOL , KW_CHAR , KW_STRING , KW_TRUE , KW_FALSE ,
332 KW_IMPORT , KW_EXPORT , KW_FROM , KW_FFI , KW_MAP ,
333
334 ( * No shared mutability without atomics ! * )
335 KW_NEW , KW_SHARED , KW_ATOMIC , KW_WEAK , KW_MUT ,
336
337 KW_SYSTEM , KW_ACTION , KW_REQUIRES , KW_ENSURES ,
338 KW_INVARIANT , KW_PROPERTY ,
339 KW_FORALL , KW_EXISTS , KW_ALWAYS , KW_EVENTUALLY , KW_NEXT , KW_UNTIL ,
340 KW_RELEASE , KW_PURE
341
342 ( * Operators and Delimiters : Arithmetic Wraps )
343 '=' , '+' , '-' , '*' , '/' , '%' , '&' , '==' , '!=' , '<' , '<=' , '>' , '>=' ,
344 '!' , '&&' , '||' , '|' , '^' , '~' , '<<' , '>>' , '(' , ')' , '{' , '}' , '[' , ']' ,
345 ',' , ';' , '.' , ':' , '::' , '?' , '?.' , '??' , '...' , '..=' , '..' , '->' , '_' , '=>'
346
347 EOF

Get Started

Aela is a software platform for creating formally verifiable, memory safe, and highly reliable applications. What's included in the compiler:

  • Works in any editor
  • Provides a built in linter, formatter, and LSP.
  • A local, offline-first agent that understands the compiler and your codebase and can talk to your other AI services.
  • Supports JIT module (that's hot reloading for compiled programs, aka: edit and continue)

Install the compiler.

Example
0 sudo sh - c 'curl -fsSL https://stablestate.ai/$CUSTOMER_ID | bash'

In a new directory, create a new project using the following command.

Example
0 aec init

This will create some default files.

Example
0 .
1 ├── index . json
2 └── src
3 └── main . ae

Edit the index.json file to name your project.

Example
0 {
1 "name" : "aela - tests" ,
2 "version" : "0 . 1 . 0" ,
3 "entry" : "main . ae" ,
4 "output" : "build / main" ,
5 "include" : [ "src /**/ * . ae" ] ,
6 "exclude" : [ "tests / * * " ] ,
7 "dependencies" : [
8 {
9 "name" : "ui" ,
10 "url" : " . . / lib / ui"
11 }
12 ]
13 }

Next you’ll edit the main.ae file

int { io::print("Hello, Aela!"); return 0; }" lang="rust" title="Example" id="8580699c4d9ff">
Example
0 // Aela Hello World
1
2 import io from "io" ;
3
4 fn main ( args : string [ ] ) - > int {
5 io : : print ( "Hello , Aela ! " ) ;
6 return 0 ;
7 }

To build your project, run the following command.

Example
0 aec build

You’ll see a new directory with the compiled program that you can run.

- new files
0 .
1 ├── build
2 │ └── main
3 ├── index . json
4 └── src
5 └── main . ae

Compiler Modes

aec build

This is your traditional, one-shot Ahead-Of-Time (AOT) compiler command.

What Compiles your entire project from source into a final, optimized executable binary.
How It invokes the compiler engine, which runs all formal verifications, performs release-level optimizations (which can be slow), and links everything together. It's a non-interactive process designed for final output.
Why Running in a Continuous Integration (CI) pipeline or when you're ready to create a production version of your application.

aec run

This is a convenience command for development.

What Builds and immediately executes your program.
How It's a simple wrapper that first performs an aec build (likely with fewer optimizations to be faster than a release build) and then runs the resulting binary.
Why Quickly testing a command-line application's behavior without needing a full watch session.

aec watch

This command exposes the engine's interactive mode directly to the command line.

What Starts the persistent, incremental engine to monitor files and provide continuous feedback in the terminal.
How It activates the same underlying engine that the LSP uses. On every change, it re-verifies your code and can be configured to take an action, such as re-running your test suite (aec watch --exec test) or restarting your application via the JIT.
Why For developers who prefer working in the terminal, it enables a very fast Test-Driven Development (TDD) loop without the overhead of an IDE.

aec package

This is a higher-level workflow and distribution tool.

What Bundles your project into a distributable format.
How It would first run aec build --release to create the optimized executable. Then, it would gather other assets—like documentation, licenses, and configuration files—and package them into a compressed archive (like a .tar.gz) for publishing to a registry or for distribution.
Why Publishing a new version of your library or application for others to use.

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 wont 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

- /path/to/my-app/index.json
0 {
1 "name" : "my - app" ,
2 "version" : "0 . 1 . 0" ,
3 "entry" : "src / main . ae"
4 }

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.json to the
  • dependency's root directory.

Example

- index.json for a project that uses a UI library
0 {
1 "name" : "my - gui - app" ,
2 "version" : "1 . 0 . 0" ,
3 "entry" : "src / app . ae" ,
4 "dependencies" : {
5 "ui" : " . . / libs / aela - ui" ,
6 "database" : " . . / libs / aela - db"
7 }
8 }

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

- ../libs/aela-ui/src/window.ae
0 // This struct can be imported by other modules.
1 export struct WindowOptions {
2 title : string ;
3 width : int ;
4 height : int ;
5 }
6
7 // This function is private to the window.ae module.
8 fn internal_helper ( ) - > void {
9 // ...
10 }

Re-exports

- re-export pattern
0 import { Thing1 , Thing2 } from "things . ae" ;
1
2 export Thing1 ;
3 export Thing2 ;

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

";" lang="" title="Example" id="ab5bd6fe58171">
Example
0 import from " " ;

Example

void { // Access symbols using the `::` namespace operator. let opts: &ui::WindowOptions = new ui::WindowOptions(); helpers::do_something(); }" lang="example" title="- src/app.ae" id="49a0501874e3d">
- src/app.ae
0 // Import the "ui" package, which was defined in index.json.
1 import ui from "ui" ;
2
3 // Import a local utility file using a relative path.
4 import helpers from " . / helpers . ae" ;
5
6 fn main ( ) - > void {
7 // Access symbols using the `::` namespace operator.
8 let opts : & ui : : WindowOptions = new ui : : WindowOptions ( ) ;
9 helpers : : do_something ( ) ;
10 }

Named Imports

This allows you to import specific functions or types directly into the current scope, without needing a namespace qualifier.

Syntax

";" lang="" title="Example" id="2068a58526a41">
Example
0 import { , : } from " " ;

Example EXAMPLE

void { // `Window` can be used directly. let win: &Window = new Window(); // `WindowOptions` is available under the alias `Options`. let opts: &Options = new Options(); }" lang="example" title="- src/app.ae" id="5ed0ea6e30563">
- src/app.ae
0 import { Window , WindowOptions : Options } from "ui" ;
1
2 fn main ( ) - > void {
3 // `Window` can be used directly.
4 let win : & Window = new Window ( ) ;
5
6 // `WindowOptions` is available under the alias `Options`.
7 let opts : & Options = new Options ( ) ;
8 }

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

- index.json for an app with native UI code on macOS
0 {
1 "name" : "aela - native - app" ,
2 "version" : "0 . 1 . 0" ,
3 "entry" : "src / main . ae" ,
4 "sources" : {
5 "darwin" : [ "src / native / apple . mm" ]
6 } ,
7 "link" : {
8 "platform" : {
9 "darwin" : [
10 " - framework" , "Foundation" ,
11 " - framework" , "AppKit" ,
12 " - framework" , "ObjectiveC"
13 ]
14 }
15 }
16 }

This configuration tells the Aela compiler:

  1. On macOS, compile the src/native/apple.mm Objective-C++ file.
  2. When linking the final executable, add flags to link against the
  3. Foundation, AppKit, and ObjectiveC system frameworks.

Type System Features

Refinement Types

Refinement types allow you to create a subtype from an existing type by adding constraints that are checked at compile time. This is useful for creating more precise and bug-resistant types.

Example
0 type NewTypeName = { value_name : BaseType where condition_expression } ;
value_name A name for the value, used within the where clause.
BaseType The type you are constraining (e.g., i32, string).
condition_expression A boolean expression that must be true for any value of this new type.

Example

You can define a type that only allows positive integers, preventing invalid values at compile time.

- Positive Integer
0 // Definition
1 type PositiveInt = { n : i32 where n > 0 } ;
2
3 // Usage
4 fn set_age ( age : PositiveInt ) {
5 // ... function body
6 }
7
8 set_age ( 10 ) ; // OK
9 set_age ( - 5 ) ; // Compile-time error!

Example: Non-Empty String

Refinements can use functions or properties in their conditions.

Example
0 // Definition
1 type NonEmptyString = { s : string where s . length ( ) > 0 } ;
2
3 // Usage
4 fn print_greeting ( name : NonEmptyString ) {
5 // ...
6 }

The primary benefit is turning potential runtime errors (like invalid arguments) into compile-time errors.

Dependent Types

Dependent types allow a type's definition to depend on a value, not just another type. This lets you encode properties like the size of a collection directly into its type.

Defining Dependent Structs

You define a dependent struct by including value parameters in parentheses () in its declaration.

Syntax

struct TypeName(param_name: ParamType, ...);

Example

Here, the Vect type depends on a length len and a type T.

Length-Indexed Vector
0 // Definition of a vector that knows its length.
1 struct Vect ( len : nat , T : Type ) {
2 // ... implementation details
3 }
4
5 // Usage in a variable declaration.
6 let names : Vect ( 3 , string ) = [ "Alice" , "Bob" , "Charlie" ] ;
7 let empty : Vect ( 0 , i32 ) = [ ] ;

Using Dependent Types in Functions

Function signatures can use these value dependencies to enforce relationships between parameters, making impossible states unrepresentable.

Example

This function to get the first element of a vector can only be called on a non-empty vector.

Safe head function
0 // The type signature guarantees vec has a length of at least 1.
1 fn safe_head ( len : nat , T : Type , vec : Vect ( len + 1 , T ) ) - > T {
2 return vec [ 0 ] ;
3 }
4
5 let first_name : string = safe_head ( 2 , string , names ) ; // OK
6
7 // The following line would cause a compile-time error because the
8 // compiler can see that the type Vect(0, i32) does not match
9 // the required type Vect(n + 1, T).
10 let error = safe_head ( - 1 , i32 , empty ) ;

This feature allows you to build extremely safe APIs by making the compiler aware of properties that are usually only checked at runtime.

Operators

Precedence Operator(s) Description Associativity
1 (Lowest) = Assignment Right-to-left
2 ?? Optional Coalescing Left-to-right
3 || Logical OR Left-to-right
4 && Logical AND Left-to-right
5 | Bitwise OR Left-to-right
6 ^ Bitwise XOR Left-to-right
7 & Bitwise AND Left-to-right
8 == , != Equality / Inequality Left-to-right
9 < , > , <= , >= Comparison Left-to-right
10 << , >> Bitwise Shift Left-to-right
11 + , - Addition / Subtraction Left-to-right
12 * , / , % Multiplication / Division / Modulo Left-to-right
13 ! , - , ~ , & (prefix), await Unary (Logical NOT, Negation, Bitwise NOT, Address-of, Await) Right-to-left
14 (Highest) () , [] , . , ?. , as Function Call, Index Access, Member Access, Type Cast Left-to-right

Flow Control

This document covers all flow control constructs in Aela, including conditionals, loops, and matching.

1. if / else

Syntax

Example
0 if ( )
1 [ else ]

Description

Standard conditional branching.

  • The condition must be an expression evaluating to bool .
  • Both then_branch and else_branch must be statements , usually blocks.

Examples

Example
0 if ( x > 0 ) {
1 print ( "Positive" ) ;
2 } else {
3 print ( "Non - positive" ) ;
4 }
5
6 if ( flag ) doSomething ( ) ;

2. while Loop

Syntax

Example
0 while ( )

Description

Loops as long as the condition evaluates to true .

Example

Example
0 while ( i < 10 ) {
1 i = i + 1 ;
2 }

3. for Loop

Syntax

Example
0 for ( in )

Description

Iterates over a collection or generator. Declarations must bind variables with types.

Example

Example
0 for ( let i : Int in 0 . . 10 ) {
1 print ( " { } " , i ) ;
2 }
3
4 for ( var x : String , var y : String in lines ) {
5 print ( " { } { } " , x , y ) ;
6 }

4. match Expression

Syntax

Example
0 match ( ) {
1 = > ,
2 . . .
3 _ = >
4 }

Description

Exhaustive pattern matching. Each match arm uses a pattern and a block.

  • _ is the wildcard pattern (required if not all cases are covered).
  • Patterns can be:
  • Literals: 1 , "foo" , 'c' , true , false
  • Identifiers: binds the value
  • Constructor patterns: Some(x) , Err(e)

Example

{ print("One"); }, _ => { print("Other"); } }" lang="aela" title="Example" id="36ee863b38fd8">
Example
0 match ( value ) {
1 0 = > { print ( "Zero" ) ; } ,
2 1 = > { print ( "One" ) ; } ,
3 _ = > { print ( "Other" ) ; }
4 }

5. return

Syntax

Example
0 return [ ] ;

Description

Exits a function immediately with an optional return value.

Examples

Example
0 return ;
1 return x + 1 ;

6. break

Syntax

Example
0 break ;

Description

Terminates the nearest enclosing loop.

7. continue

Syntax

Example
0 continue ;

Description

Skips to the next iteration of the nearest enclosing loop.

8. Blocks and Statement Composition

Syntax

Example
0 {
1 ;
2 . . .
3 }

Description

A block groups multiple statements into a single compound statement. Used for control flow bodies.

Example

Example
0 {
1 let x : Int = 1 ;
2 let y : Int = x + 2 ;
3 print ( y ) ;
4 }

9. Expression Statements

Syntax

Example
0 ;

Description

Evaluates an expression for side effects. Common for function calls or assignments.

Example

Example
0 doSomething ( ) ;
1 x = x + 1 ;

Optionals

Optionals provide a safe and explicit way to handle values that may or may not be present. Instead of using special values like null or -1 which can lead to runtime errors, Aela uses the Option type to wrap a potential value. The compiler will then enforce checks to ensure you handle the "empty" case safely.

Declaring an Optional Type

You can declare a variable or field as optional using two equivalent syntaxes:

  1. The `?` Suffix (Recommended) : This is the preferred, idiomatic syntax.
  2. It's a concise way to mark a type as optional.
Example
0 // A variable that might hold a string
1 let name : string ? ;
2
3 // A struct with optional fields
4 struct Profile {
5 age : u32 ? ;
6 bio : string ? ;
7 }
  1. The `Option(T)` Syntax : This is the formal, nominal type. The T?
  2. syntax is simply sugar for this. It can be useful in complex, nested type
  3. signatures for clarity.
Example
0 // This is equivalent to `let name: string?`
1 let name : Option ( string ) ;

Creating Optional Values

An optional variable can be in one of two states: it either contains a value, or it's empty. You use the Some and None keywords to create these states.

None : The Empty State

The None keyword represents the absence of a value. You can assign it to any optional variable, and the compiler will infer the correct type from the context.

Example
0 let age : u32 ? = None ;
1
2 let user : User = {
3 // The profile field is optional
4 profile : None
5 } ;
6 Some ( value ) : The Value - Holding State

To create an optional that contains a value, you wrap the value with the Some constructor.

Example
0 // Create an optional u32 containing the value 30
1 let age : u32 ? = Some ( 30 ) ;
2
3 let user : User = {
4 profile : Some ( {
5 email : "some@example . com" ,
6 age : Some ( 30 )
7 } )
8 } ;

The Optional-Coalescing Operator (??) (For Defaults)

This is the best way to unwrap an optional by providing a fallback value to use if the optional is None. The term "coalesce" means to merge or come together; this operator coalesces the optional's potential value and the default value into a single, guaranteed, non-optional result.

Example
0 // Get the user's email, or use a default if it's None.
1 // `email_address` will be a regular `string`, not a `string?`.
2 let email_address : string = user2 . profile ? . email ? ? "no - email - provided@domain . com" ;
3
4 print ( "Contacting user at : { } " , email_address ) ;

Using Optional Values

Aela provides mechanisms to safely work with optional values, preventing you from accidentally using an empty value as if it contained something.

Optional Chaining (?.)

The primary way to access members of an optional struct is with the optional chaining operator, ?.. If the optional is None, the entire expression short-circuits and evaluates to None. If it contains a value, the member access proceeds.

The result of an optional chain is always another optional.

Example
0 struct Profile {
1 email : string ;
2 }
3
4 struct User {
5 profile : Profile ? ;
6 }
7
8 fn main ( ) - > int {
9 let user1 : User = { profile : Some ( { email : "test@example . com" } ) } ;
10 let user2 : User = { profile : None } ;
11
12 // email1 will be an `Option(string)` containing Some("test@example.com")
13 let email1 : string ? = user1 . profile ? . email ;
14
15 // email2 will be an `Option(string)` containing None
16 let email2 : string ? = user2 . profile ? . email ;
17
18 return 0 ;
19 }

Explicit Checking (Match Statement)

Use match statements to explicitly handle the Some and None cases, allowing you to unwrap the value and perform more complex logic.

io::print("The name is: {}", value), None => io::print("No name was provided."), }" lang="" title="Example" id="c36380aec7d35">
Example
0 let name : string ? = Some ( "Aela" ) ;
1
2 match name {
3 Some ( value ) = > io : : print ( "The name is : { } " , value ) ,
4 None = > io : : print ( "No name was provided . " ) ,
5 }

Mutability

Aela enforces safety and clarity by requiring that any function intending to modify data must be explicitly marked. This prevents accidental changes and makes code easier to reason about. This is achieved through the mut keyword.

The Principle: Safe by Default

In Aela, all function parameters are immutable (read-only) by default. When you pass a variable to a function, you are providing a read-only view of it.

Example
0 fn read_runner ( r : & Runner ) {
1 // This is OK.
2 io : : print ( "Points : { } " , r . point ) ;
3
4 // This would be a COMPILE-TIME ERROR.
5 // r.point = 5;
6 }

Granting Permission to Mutate

To allow a function to modify a parameter, you must use the mut keyword in two places:

  1. The Function Definition: To declare that the function requires mutable
  2. access.
  3. The Call Site: To explicitly acknowledge that you are passing a variable
  4. to be changed.

This two-part system makes mutation a clear and intentional act.

In the Function Definition

Prefix the parameter you want to make mutable with mut . This is the function's "contract," stating its intent to modify the argument.

Example
0 fn reset_runner ( mut r : & Runner ) {
1 // This is now allowed because the parameter `r` is marked as `mut`.
2 r . point = 0 ;
3 r . passed = 0 ;
4 r . failed = 0 ;
5 }

At the Call Site

When you call a function that expects a mutable parameter, you must also prefix the argument with mut . This confirms you understand the variable will be modified.

Example
0 fn main ( ) {
1 // The variable itself must be mutable, declared with 'var'.
2 var my_runner = Runner . new ( ) ;
3
4 // The 'mut' keyword is required here to pass 'my_runner'
5 // to a function that expects a mutable argument.
6 reset_runner ( mut my_runner ) ;
7 }

The compiler will produce an error if you try to pass a mutable argument without the mut keyword, or if you try to pass an immutable ( let ) variable to a function that expects a mutable one. This ensures there are no surprises about where your data can be changed.

Errors

Out-of-the-box errors are simple, the verifier runs the check borrows and the life-times of variables and properties.

An error where mut keyword should have been used
0 Analyzer Error : Cannot assign to field 'point' because 'self' is immutable .
1 - - > / Users / paolofragomeni / projects / aela / lib / test . ae : 16 : 10
2
3 15 | fn ok ( self : & Self , cond : bool , desc : string ) - > bool {
4 16 - > self . point = self . point + 1 ;
5 | ^
6 17 |

Structs, Impl Blocks, and Memory Layout

struct Declarations: The Data Blueprint

A struct defines a composite data type. Its sole purpose is to describe the memory layout of a collection of named fields. Structs contain ONLY data members.

Syntax

Example
0 struct {
1 : ;
2 : ;
3 . . .
4 }

Example

Defines a type named 'Packet' that holds a sequence number, a size, and a single-byte flag.

Example
0 struct Packet {
1 sequence : u32 ;
2 size : u16 ;
3 is_urgent : u8 ;
4 }

impl Blocks: Attaching Behavior

An impl (implementation) block associates functions with an existing struct type. These functions are called methods. The impl block does NOT alter the struct's memory layout or size.

Example
0 impl {
1 // constructor (optional, special method)
2 fn constructor ( self : & Self , . . . ) - > Self { . . . }
3
4 // methods
5 fn ( self : & Self , . . . ) - > { . . . }
6 }

Details

  • The constructor is a special function that initializes the
  • struct's memory. It is called when using the new keyword.
  • Methods are regular functions that receive a reference to an
  • instance of the struct as their first parameter, named self .
  • Self (capital 'S') is a type alias for the struct being implemented.
  • Multiple impl blocks can exist for the same struct. The compiler
  • merges them.

Example

Example
0 impl Packet {
1 fn constructor ( self : & Self , seq : u32 ) - > Self {
2 self . sequence = seq ;
3 self . size = 0 ;
4 self . is_urgent = 0 ;
5 }
6
7 fn mark_urgent ( self : & Self ) - > void {
8 self . is_urgent = 1 ;
9 }
10 }

Memory Layout and Padding

Aela adopts C-style struct memory layout rules, including padding and alignment, to ensure efficient memory access and ABI compatibility.

  1. Sequential Layout: Fields are laid out in memory in the exact
  2. order they are declared in the struct definition.
  1. Alignment: Each field is aligned to a memory address that is a
  2. multiple of its own size (or the platform's word size for larger
  3. types). The compiler inserts unused "padding" bytes to enforce this.
  1. Struct Padding: The total size of the struct itself is padded to be a
  2. multiple of the alignment of its largest member. This ensures that
  3. in an array of structs, every element is properly aligned.

Rules:

Example
0 struct Packet {
1 sequence : u32 ; // 4 bytes
2 size : u16 ; // 2 bytes
3 is_urgent : u8 ; // 1 byte
4 }

Visual Layout (on a typical 64-bit system):

Byte Offset Content
0 sequence (Byte 0)
1 sequence (Byte 1)
2 sequence (Byte 2)
3 sequence (Byte 3) ← 4‑byte
Byte Offset Content
4 size (Byte 0)
5 size (Byte 1) ← 2‑byte
Byte Offset Content
6 is_urgent (Byte 0)
Byte Offset Content
7 PADDING (1 byte) ← struct padded to a multiple of 4 bytes (max)

TOTAL SIZE: 8 bytes

Heap vs. Stack Allocation

Aela supports both heap and stack allocation for structs, giving the programmer control over memory management and performance.

Stack allocation (Default for local variables):

  • How: A struct is allocated on the stack by declaring a variable of
  • the struct type and initializing it with a struct literal. The new
  • keyword is NOT used.
  • Lifetime: The memory is valid only within the scope where it is
  • declared (e.g., inside a function). It is automatically reclaimed
  • when the scope is exited.
  • Performance: Extremely fast. Allocation and deallocation are nearly
  • instant, involving only minor adjustments to the stack pointer.
Example
0 let my_packet : Packet = Packet {
1 sequence : 200 ,
2 size : 128 ,
3 is_urgent : 1
4 } ;

Heap Allocation (Explicit):

  • How: A struct is allocated on the heap using the new keyword, which
  • returns a reference ( & ) to the object.
  • Lifetime: The memory persists until it is no longer referenced. Its
  • lifetime is managed by the runtime's reference counter, not tied to a
  • specific scope.
  • Performance: Slower than stack allocation. Involves a call to the
  • system's memory allocator ( malloc ) and requires runtime overhead for
  • reference counting.
- Explicit Heap Allocation
0 let my_packet_ref : & Packet = new Packet ( 201 ) ;

When to use which:

  • STACK: Use for most local, temporary data. It's the idiomatic and
  • most performant choice for data that does not need to outlive the
  • function in which it was created.
  • HEAP: Use when a struct instance must be shared or returned from a
  • function and needs to have a lifetime independent of any single
  • scope. Also used for very large structs to avoid overflowing the stack.

Opaque Structs

Safety & Undefined Behavior (UB)

The primary benefit of opaque structs is preventing a whole class of undefined behavior by strengthening type safety at the language boundary.

How Safety is Increased

Eliminates Type Confusion: Before, you might have used a generic type like `u64` or `&void` to represent a C handle. The compiler had no way to know that a `u64` from `database_connect()` was different from a `u64` from `file_open()`. You could accidentally pass a database handle to a file function, leading to memory corruption or crashes. Now, `&DatabaseHandle` and `&FileHandle` are distinct, incompatible types *. The Aela compiler will issue a compile-time error if you try to misuse them, completely eliminating this risk.

Prevents Invalid Operations in Aela: * By disallowing member access and instantiation, we prevent Aela code from making assumptions about the C data structure. Aela code cannot accidentally:

Read from or write to a field that doesn't exist or has a different offset (`my_handle.field`). Create a struct of the wrong size on the stack ( let handle: StringBuilder ). * Perform pointer arithmetic on the handle. The only thing Aela code can do is treat the handle as an opaque value to be passed back to the C library, which is the only safe way to interact with it.

For Users of Opaque Structs

Your documentation should include:

  1. Purpose and Syntax: Explain that opaque structs are for safely handling foreign pointers/handles. Show the syntax:
Example
0 // in lib/mylib.ae
1 export struct MyFFIHandle ;
  1. Rules of Engagement: Clearly state the allowed and disallowed operations we implemented.

Allowed: Passing to/from FFI functions, assigning to other variables of the same type, comparing for equality. Disallowed: Member access ( . ), instantiation ( new ), and dereferencing. Always use a reference ( &MyFFIHandle ).

  1. A Mandatory Safety Section on Lifetimes: This section must be prominent. It should explain the dangling pointer risk and establish a clear best practice.

When working with opaque handles, you are responsible for managing their memory. Most C libraries provide functions for creating and destroying these objects. You must call the destruction function to prevent memory leaks and undefined behavior.

&StringBuilder; ffi ae_runtime_sb_append: fn(&StringBuilder, string); ffi ae_runtime_sb_destroy: fn(&StringBuilder); // <-- The cleanup function fn main() -> int { let sb = ae_runtime_sb_new(); ae_runtime_sb_append(sb, "hello"); // CRITICAL: You must call destroy when you are done. ae_runtime_sb_destroy(sb); // Using `sb` after this point is UNDEFINED BEHAVIOR. // ae_runtime_sb_append(sb, " world"); // <-- DANGEROUS! return 0; }" lang="aela" title="Example: Managing Lifetimes" id="0b432d411f723">
Example: Managing Lifetimes
0 `` `aela
1 import { StringBuilder } from " . / runtime . ae" ;
2
3 // FFI Declarations for a C string builder
4 ffi ae_runtime_sb_new : fn ( ) - > & StringBuilder ;
5 ffi ae_runtime_sb_append : fn ( & StringBuilder , string ) ;
6 ffi ae_runtime_sb_destroy : fn ( & StringBuilder ) ; // <-- The cleanup function
7
8 fn main ( ) - > int {
9 let sb = ae_runtime_sb_new ( ) ;
10 ae_runtime_sb_append ( sb , "hello" ) ;
11
12 // CRITICAL: You must call destroy when you are done.
13 ae_runtime_sb_destroy ( sb ) ;
14
15 // Using `sb` after this point is UNDEFINED BEHAVIOR.
16 // ae_runtime_sb_append(sb, " world"); // <-- DANGEROUS!
17
18 return 0 ;
19 }

Interfaces

This document specifies the design and behavior of Aela's system for polymorphism, which is based on interface, struct, and impl...as... declarations.

Overview

Aela's polymorphism is designed to be explicit, safe, and familiar. It allows developers to write flexible code that can operate on different data types in a uniform way, a concept known as dynamic dispatch. This is achieved by separating a contract's definition (the interface) from its implementation (the struct and impl block).

Example
0 interface Element {
1 fn onclick ( event : & Event ) - > void ;
2 }
3
4 struct Button {
5 handle : i64 ;
6 }
7
8 impl Button as Element {
9 fn constructor ( self : & Self , someArg1 : string ) {
10 // fired when new is used
11 }
12 fn init ( self : & Self , someArg1 : string ) {
13 // fired when ever a struct is initialized.
14 }
15 fn onclick ( self : & Self , event : & Event ) - > void {
16 // fired when called directly (statically or dynamically)
17 }
18 }
19
20 impl Button as Element {
21 fn ontoch ( self : & self , event : & Event ) - > void {
22 }
23 }

The core philosophy is:

Interfaces define abstract contracts or capabilities.

Structs define concrete data structures.

impl...as... blocks prove that a concrete struct satisfies an abstract interface.

Components

The interface Declaration

An interface defines a set of method signatures that a concrete type must implement to conform to the contract.

Example
0 interface {
1 fn ( ) - > ;
2 // ... more method signatures
3 }

Rules:

An interface block can only contain method signatures. It cannot contain any data fields.

Method signatures within an interface must not have a body. They must end with a semicolon ;.

The self parameter in an interface method must be of a reference type (e.g., &self).

Example
0 interface Serializable {
1 fn serialize ( & self ) - > string ;
2 }

The struct Declaration

A struct defines a concrete data type. Its role is unchanged.

Example
0 struct {
1 : ;
2 // ... more data fields
3 }

Rules:

A struct can only contain data fields. Method implementations are defined separately in impl blocks.

Example
0 struct User {
1 id : int ;
2 username : string ;
3 }

The impl...as... Declaration

This block connects a concrete struct to an interface, proving that the struct fulfills the contract.

Example
0 impl as {
1 // Implementations for all methods required by the interface
2 fn ( ) - > {
3 // ... method body ...
4 }
5 }

Rules:

The impl block must provide a concrete implementation for every method defined in the .

The signature of each implemented method must be compatible with the corresponding signature in the interface.

A single struct may implement multiple interfaces by using separate impl...as... blocks for each one.

Example
0 impl User as Serializable {
1 fn serialize ( & self ) - > string {
2 // Implementation of the serialize method for the User struct
3 return std : : format ( " { { \"id\" : { } , \"username\" : \" { } \" } } " , self . id , self . username ) ;
4 }
5 }

Interface Types

A variable can be declared with an interface type by using a reference. This creates a "trait object" or "fat pointer" that can hold any concrete type that implements the interface.

Syntax: &

Behavior: A variable of type & is a fat pointer containing two components:

A pointer to the instance data (e.g., a &User).

A pointer to the v-table for the specific (Struct, Interface) implementation.

Example
0 let objects : & Serializable [ ] = [
1 & User { id : 1 , username : "aela" } ,
2 & Document { title : "spec . md" }
3 ] ;
4
5 for ( let obj : & Serializable in objects ) {
6 // This call is dynamically dispatched using the v-table.
7 io : : print ( obj . serialize ( ) ) ;
8 }

Formal Verification

This document specifies the design and behavior of Aela's compile-time verification system , which ensures the correctness of a program through formal specifications — namely, invariant and property declarations embedded in impl blocks.

Overview

Aela enables developers to write mathematically precise specifications that describe the expected state and behavior of a program, which the compiler formally verifies at compile time. These specifications are not runtime code — they do not execute, incur no runtime cost, and exist solely to ensure program correctness before code generation.

There are two key constructs:

`invariant`: A safety condition that must always hold before and after each function in an `impl` block. property : A liveness or temporal condition that must hold across all valid execution traces of a type’s behavior.

These declarations allow Aela to verify complex asynchronous and stateful systems using SMT solvers or model checking , without requiring users to learn a separate formal language.

Example
0 struct BankAccount {
1 balance : i32 ;
2 overdraft_limit : u32 ;
3 }
4
5 impl BankAccount {
6 // Always ensure the balance never drops below the allowed overdraft.
7 invariant balanceIsAboveLimit = balance > = - overdraft_limit
8
9 // Eventually, the system must bring the account back to a non-negative balance.
10 property eventuallyInBalance = eventually ( balance > = 0 )
11
12 fn recoverNow ( ) {
13 requires balance < 0
14 ensures balance = = 0
15
16 balance = 0 ;
17 }
18 }

Components

The invariant Declaration

An invariant specifies a condition that must hold before and after every function in an impl block.

Example
0 invariant =

Rules:

Invariants must be side-effect-free boolean expressions. They may reference any field defined in the corresponding struct . The compiler verifies that every function in the `impl` block preserves the invariant. If the compiler cannot prove an invariant holds across all paths, compilation fails.

Example
0 struct Counter { count : int }
1
2 impl Counter {
3 invariant nonNegative = count > = 0
4
5 fn increment ( ) {
6 ensures count = = old ( count ) + 1
7 count = count + 1 ;
8 }
9 }

In this example, the invariant guarantees that the count is never negative. The verifier ensures this remains true even after increment() executes.


The property Declaration

A property expresses a temporal guarantee — such as liveness or ordering — across the program’s behavior.

Example
0 property =

Temporal expressions may include:

`always`: The condition must hold at all times. eventually : The condition must hold at some point in the future. `until`, `release`: Conditions over time and ordering. forall , exists : Quantifiers over a domain (e.g., tasks, states).

Rules:

Properties do not affect control flow or behavior. They are used by the compiler to prove guarantees about possible program traces . * Property violations produce counterexample traces at compile time.

Example
0 struct Queue {
1 items : Array ( int ) ;
2 }
3
4 impl Queue {
5 invariant lengthNonNegative = std : : length ( items ) > = 0
6
7 // Ensure that any task awaiting the queue will eventually see data.
8 property eventuallyReceivesItem = forall task in Tasks {
9 eventually ( ! std : : is_empty ( items ) )
10 }
11
12 async fn dequeue ( ) - > int {
13 while std : : is_empty ( items ) {
14 await wait_for_item ( ) ;
15 }
16 return std : : pop ( items ) ;
17 }
18
19 fn enqueue ( value : int ) {
20 std : : push ( items , value ) ;
21 notify_waiters ( ) ;
22 }
23 }

Specification Behavior

Construct Scope Verified When Runtime Cost
invariant Per-impl block Before and after each fn / async fn None
property Per-impl block Over all valid execution traces None

`invariant` is used for safety ("nothing bad happens"). property is used for liveness ("something good eventually happens").

The compiler treats both as compile-time-only declarations that participate in verification, not execution.


Old State Reference: old(expr)

The old keyword allows a function’s ensures clause to reference the value of an expression before the function was called.

Example
0 fn increment ( ) {
1 ensures count = = old ( count ) + 1
2 count = count + 1 ;
3 }

The compiler ensures that the post-state ( count ) relates correctly to the pre-state ( old(count) ).


Quantifiers and Temporal Blocks

Quantifiers can be used in properties to express conditions across many elements or tasks.

Example
0 forall in {
1
2 }

Compiler Interactions

- Interacting with the compiler
0 ⚠️ [ formal : invariant ] Detected 1 invariant violation during compilation .
1
2 Invariant :
3 balanceIsAboveLimit = balance > = - overdraft_limit
4
5 Violated in :
6 impl BankAccount → fn emergencyReset ( )
7
8 ╭────┬───────────────────────────────────────────────
9 ╭─│ 12 │
10 │ │ 13 │ balance = - 999 ;
11 │ │ │ ^ ^ ^ ^ ^ violates invariant after execution
12 │ │ 14 │
13 │ ╰────┴───────────────────────────────────────────────
14
15 │ 💡 The compiler attempted to prove that `balance >= -overdraft_limit`
16 │ holds before and after each function in `impl BankAccount` .
17
18 │ But after calling `emergencyReset()` , balance = - 999 ,
19 │ and overdraft_limit = 0 ⇒ invariant fails : - 999 > = 0 ❌
20
21 ╰──────────────────────────────────────────────────────────
22
23 Actions :
24
25 1 . 🔍 Explain why this invariant is failing
26 2 . ✏️ Show me the relevant values and trace
27 3 . 🧪 Temporarily disable this invariant ( not recommended )
28 4 . 🧼 Open `emergencyReset` to fix it
29 5 . ❌ Remove the invariant entirely
30 6 . ❓ Ask any arbitrary question ( chat )
31
32 > I guess I don't really understand invariants
33
34 No problem , let's take a look at how they work .
35 The first thing we . . .

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 that p is borrowed, with kind ∈ {shared, unique} .
  • Program Point q : A location in the control-flow graph (CFG).
  • Alive(L, q) : Predicate meaning loan L is still valid at point q (lexically-free semantics).
  • Operations :
  • reads(p, q) : a read of place p at q .
  • writes(p, q) : a write to place p at q .
  • moves(p, q) : a move from place p at q .
  • Aliases(x, p, q) : x holds a reference to p at q (logical predicate, tracked via loan origins).
  • Escape(r, q) : Reference r escapes its region at q (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]) -> &i32 elaborates to for<ρ> 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 marker A .

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: self implicitly labeled Self , but can also be explicitly labeled: self: &Self as S .
  • Diagnostics: Role-based: “return value tied to param a (label A) but a does not live long enough.” Quick fixes: “Add as A to param.”
&Self T { return &self.0; } fn other(self: &Self as S, other: &Self as O) -> &O T { return &other.0; } } fn demo_methods() { let p1: Pair(int) = (1, 2); let p2: Pair(int) = (3, 4); let r1 = p1.first(); let r2 = p1.other(&p2); print(r1); print(r2); } // 4) Higher-order function: label flows through function type fn map_head(xs: &Vec(T) as A, f: Fn(&A T) -> U) -> &A U { return f(&xs[0]); } fn demo_hof() { let nums: Vec(int) = [1, 2, 3]; let head_ref = map_head(&nums, fn(x: &int) -> &int { return x; }); print(head_ref); } // 5) Multiple inputs require label: `as A` disambiguates the return fn pick(a: &T as A, b: &T) -> &A T { // body could choose either; label tells the checker the result is tied to `a` return a; } // 6) Optional: Outlives relation (advanced) fn choose_longer(a: &T as X, b: &T as Y) -> &X T where Y : X { return a; } // 7) Struct constructor with label reuse struct Window(T) { head: &A T, tail: &B T } fn mk_window(xs: &Vec(T) as A, ys: &Vec(T) as B) -> Window(T) { return Window { head: &xs[0], tail: &ys[0] }; } fn demo_window() { let xs: Vec(int) = [1,2,3]; let ys: Vec(int) = [4,5,6]; let w = mk_window(&xs, &ys); print(w.head); print(w.tail); } // 8) Diagnostics-friendly failure (commented): // fn bad_pick(a: &T, b: &T) -> &T { // if cond { return a; } else { return b; } // // Error: ambiguous return lifetime. Add `as A` to a (or b) and return `&A T`. // }" lang="aela" title="Example" id="bd62fb2cf27e9">
Example
0 // 1) Disambiguate which input a returned reference is tied to
1 fn first_of_two ( a : & T as A , b : & T ) - > & A T {
2 return a ;
3 }
4
5 fn demo_first_of_two ( ) {
6 let x : int = 10 ;
7 let y : int = 20 ;
8 let rx = & x ;
9 let ry = & y ;
10
11 let r = first_of_two ( rx , ry ) ;
12 print ( r ) ;
13 }
14
15 // 2) Struct field referencing an argument via label
16 struct View ( T ) { data : & A T }
17
18 fn make_view ( x : & T as A ) - > View ( T ) {
19 return View { data : x } ;
20 }
21
22 fn demo_view ( ) {
23 let s : string = "hello" ;
24 let v = make_view ( & s ) ;
25 print ( v . data ) ;
26 }
27
28 // 3) Methods: implicit Self label + explicit labels to disambiguate
29 impl Pair ( T ) {
30 fn first ( self : & Self ) - > & Self T {
31 return & self . 0 ;
32 }
33
34 fn other ( self : & Self as S , other : & Self as O ) - > & O T {
35 return & other . 0 ;
36 }
37 }
38
39 fn demo_methods ( ) {
40 let p1 : Pair ( int ) = ( 1 , 2 ) ;
41 let p2 : Pair ( int ) = ( 3 , 4 ) ;
42
43 let r1 = p1 . first ( ) ;
44 let r2 = p1 . other ( & p2 ) ;
45 print ( r1 ) ;
46 print ( r2 ) ;
47 }
48
49 // 4) Higher-order function: label flows through function type
50 fn map_head ( xs : & Vec ( T ) as A , f : Fn ( & A T ) - > U ) - > & A U {
51 return f ( & xs [ 0 ] ) ;
52 }
53
54 fn demo_hof ( ) {
55 let nums : Vec ( int ) = [ 1 , 2 , 3 ] ;
56 let head_ref = map_head ( & nums , fn ( x : & int ) - > & int { return x ; } ) ;
57 print ( head_ref ) ;
58 }
59
60 // 5) Multiple inputs require label: `as A` disambiguates the return
61 fn pick ( a : & T as A , b : & T ) - > & A T {
62 // body could choose either; label tells the checker the result is tied to `a`
63 return a ;
64 }
65
66 // 6) Optional: Outlives relation (advanced)
67 fn choose_longer ( a : & T as X , b : & T as Y ) - > & X T where Y : X {
68 return a ;
69 }
70
71 // 7) Struct constructor with label reuse
72 struct Window ( T ) { head : & A T , tail : & B T }
73
74 fn mk_window ( xs : & Vec ( T ) as A , ys : & Vec ( T ) as B ) - > Window ( T ) {
75 return Window { head : & xs [ 0 ] , tail : & ys [ 0 ] } ;
76 }
77
78 fn demo_window ( ) {
79 let xs : Vec ( int ) = [ 1 , 2 , 3 ] ;
80 let ys : Vec ( int ) = [ 4 , 5 , 6 ] ;
81 let w = mk_window ( & xs , & ys ) ;
82 print ( w . head ) ;
83 print ( w . tail ) ;
84 }
85
86 // 8) Diagnostics-friendly failure (commented):
87 // fn bad_pick(a: &T, b: &T) -> &T {
88 // if cond { return a; } else { return b; }
89 // // Error: ambiguous return lifetime. Add `as A` to a (or b) and return `&A T`.
90 // }

Lifetimes in Data Structures

  • Reference fields implicitly carry region parameters. struct Node { data: &T } elaborates to struct Node<ρ, T> { data: &ρ T } .
  • Construction instantiates ρ from the argument’s region. Error if struct outlives the referenced data.

Place Overlap (I3)

  • p overlaps p .
  • p overlaps p.f . Distinct fields disjoint.
  • arr[i] vs arr[j] : disjoint if indices are distinct compile-time constants; else conservative overlap.
  • Library intrinsics like split_at_mut provide 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 await unless the origin outlives the entire future. Practically: locals cannot cross await ; only borrows from captured fields of the async task can.
  • Phase 2: Desugar async to 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

Example
0 let s : string = "hi" ;
1 let r = & s ; // borrow starts
2 print ( r ) ; // last use of r
3 var t = s ; // OK allowed: r’s region ended at last use (lexically-free)

L1 — Loan Creation

Example
0 var x : int = 0 ;
1 let r = & x ; // L(x, shared)
2 let m = & mut x ; // Not OK conflict later under A1, but creation itself establishes L(x, unique)

L2 — Loan Propagation

Example
0 fn id_ref ( p : & int ) - > & int { p }
1 let x : int = 1 ;
2 let r1 = & x ; // L(x, shared)
3 let r2 = id_ref ( r1 ) ; // loan propagates to r2 until last use
4 print ( r2 ) ; // OK!

A1 — Unique Exclusivity

Example
0 var v : int = 0 ;
1 let m = & mut v ; // L(v, unique)
2 let r = & v ; // Not OK! A1: unique excludes any concurrent shared borrow

A2 — Shared Read‑Only

Example
0 var n : int = 0 ;
1 let a = & n ; // L(n, shared)
2 let b = & n ; // another shared loan
3 print ( * a + * b ) ; // OK! reads allowed
4 n = 5 ; // Not OK! A2: write while shared loans alive

I1 — Write Invalidates

Example
0 var s : string = "hi" ;
1 let r = & s ; // L(s, shared)
2 print ( r ) ;
3 s . push ( " ! " ) ; // Not OK! I1: write to s while shared loan alive (if r not yet last‑used)

I2 — Move Invalidates

Example
0 var s : string = "a" ;
1 let r = & s ; // L(s, shared)
2 var t = s ; // Not OK! I2: move invalidates loans; r would dangle

I3 — Overlap/Fields

Example
0 struct P { x : int , y : int }
1 var p : P = P { x : 1 , y : 2 } ;
2 let mx = & mut p . x ; // L(p.x, unique)
3 let my = & mut p . y ; // OK, disjoint fields: allowed
4
5 var arr : [ int ; 3 ] = [ 0 , 1 , 2 ] ;
6 let a = & mut arr [ 0 ] ;
7 let b = & mut arr [ 1 ] ; // not OK, if indices distinct constants
8 let i = 0 ; let j = read_index ( ) ;
9 let c = & mut arr [ i ] ;
10 let d = & mut arr [ j ] ; // Not OK conservatively, may overlap unless proven disjoint (use split helpers)

U1 — Use Requires Alive

Example
0 let s : string = "hi" ;
1 let r = & s ; // L(s, shared)
2 print ( r ) ; // OK! use while alive
3 // after last use r ends; any further use would be not OK (dead borrow)

U2 — No After‑Free

Example
0 fn bad ( ) {
1 let s : string = "x" ;
2 let r = & s ;
3 drop ( s ) ; // base place dead
4 print ( r ) ; // Not OK: U2: use after base death
5 }

RB1 — Shared from Unique

Example
0 var v : int = 0 ;
1 let mu = & mut v ; // L(v, unique)
2
3 // Shared reborrow of the same place (auto-deref from &mut int to int)
4 let rs : & int = & mu ; // creates L(v, shared)
5
6 print ( rs ) ;
7
8 // Write through the unique borrow (auto-deref on assignment)
9 mu = 3 ; // must end shared loan before writing via `mu`

RB2 — Unique from Unique (optional v1)

Example
0 var v : int = 0 ;
1 let mu1 = & mut v ;
2 let mu2 = & mut mu1 ; // unique reborrow of v; allowed only if no overlapping use via mu1

E1 — No Escaping Borrows

Example
0 fn leak_ref ( ) - > & int {
1 let x : int = 1 ;
2 return & x ; // Not OK! Escapes to caller; Not OK, dies at function end
3 }
4
5 fn head ( s : & string ) - > & char {
6 return & s [ 0 ] ;
7 } // OK! Summary ties return to arg

M1 — Binding Mutability

Example
0 let x : int = 0 ;
1 let rx = & mut x ; // Not OK: M1: &mut requires mutable base
2 var y : int = 0 ;
3 let ry = & mut y ; // OK

M2 — No Shared Mutability Without Atomics

Example
0 shared var n : int = 0 ; // shared location
1 let r1 = & n ; let r2 = & n ; // shared borrows
2 n = n + 1 ; // Not OK concurrent write without atomic discipline
3 atomic var a : int = 0 ; // with atomic, writes are allowed by policy

T1 — Temporaries

Example
0 print ( & ( make_string ( ) ) ) ; // OK temporary lives through call; reference dies at last use
1 let r = & ( make_string ( ) ) ;
2 print ( r ) ; // Not OK if r would outlive the temporary’s last use

T2 — Branches/Loops/Match

Example
0 let s : string = "x" ;
1 let r = & s ;
2 if cond { print ( r ) ; } else { print ( r ) ; }
3 // r is used on both paths; live set at join = intersection ⇒ still alive until after the if
4 var t = s ; // not OK must occur after r’s last use on all paths

F1 — FFI Preconditions

Example
0 ffi puts : ( & char ) - > int = . . . ;
1 let s : string = "hi" ;
2 let r = & s [ 0 ] ;
3 puts ( r ) ; // OK if FFI summary: does not retain; Not OK if it may retain (escape)

F2 — FFI Postconditions

Example
0 ffi c_strchr : ( & char , int ) - > & char = . . . ; // trusted: result aliases input
1
2 fn first_a ( s : & string ) - > & char {
3 c_strchr ( & s [ 0 ] , 'a' ) // OK allowed only because summary ties result to arg region
4 }

RFT1 — Refinement Well‑Formedness

Example
0 let x : int = 5 ;
1 let y : { z : & int where initialized ( z ) } = { z : & x } ; // OK predicate references lifetime‑relevant property

RFT2 — Discharge at Use Sites

Example
0 fn show ( p : { z : & int where not_escaped ( z ) } ) - > void {
1 print ( p . z ) ; // OK! checker discharges `not_escaped` from current loan facts
2 }

inter‑Procedural Summary (Elision)

Example
0 fn get_first ( a : & [ int ] ) - > & int {
1 return & a [ 0 ] ;
2 }
3
4 // elaborated internally: for<ρ> fn(&ρ [int]) -> &ρ int
5 // caller instantiates ρ from its argument; return tied to same ρ

Structs with Reference Fields (Implicit Regions)

Example
0 struct Node ( T ) { data : & T } // elaborates to Node<ρ, T>
1
2 fn demo ( ) - > void {
3 let x : int = 1 ;
4 var n : Node ( int ) = Node { data : & x } ; // OK n: Node<ρx, int>
5 }
6
7 fn bad ( ) - > void {
8 var n : Node ( int ) ;
9
10 {
11 let x : int = 1 ;
12 n = Node { data : & x } ; // Not OK n outlives x ⇒ region check fails
13 }
14 }

Async Phase 1 — No Unique Loans Across Await

Example
0 async fn step ( mut v : & int ) - > void {
1 let m = v ; // borrow unique via parameter
2 await tick ( ) ; // Not OK, unique loan across await in phase 1
3 }

Closures — Capture Classification

void { move x }; // by‑value move ⇒ FnOnce" lang="aela" title="Example" id="de7b5ce457c5">
Example
0 var n : int = 0 ;
1 let c1 = fn ( ) - > void { print ( & n ) ; } ; // shared borrow capture ⇒ Fn
2 let c2 = fn ( ) - > void { n = n + 1 ; } ; // unique borrow capture ⇒ FnMut
3 let x : string = "hi" ;
4 let c3 = fn ( ) - > void { move x } ; // by‑value move ⇒ FnOnce

C/C++ Harmony

Most languages' safty stops at the FFI boundary. But by automating the creation of safe FFI boundaries and embedding more of the C/C++ code's contracts directly into Aela's type system. Instead of just marking a boundary as unsafe and leaving the safety burden entirely on the developer, Aela can actively assist in verifying the C/C++ side of the interaction.

Automatically Generate "Diplomatic" Wrappers

Instead of manually writing write unsafe blocks and wrapper functions (This is tedious and error-prone). Aela can automate this.

Aela Compile can parse C/C++ header files (.h, .hpp) and automatically generatee the FFI bindings and a safe, idiomatic Aela wrapper. It's much more sophisticated than a simple binding generator.

Contract Inference

The tool can analyze C++ code for clues about contracts.

For instance, it can interpret a gsl::not_null or _Nonnull annotation as a non-nullable reference in Aela, automatically adding the necessary runtime checks.

Resource Management

If a C function create_foo() returns a pointer that must be freed with destroy_foo() , the generator can automatically create a smart pointer or RAII object in Aela that calls destroy_foo() on scope exit. This eliminates a huge class of resource leak bugs.

Error Handling

It can translate C-style error codes e.g., return -1; or errno = EINVAL; into Aela's native error-handling mechanism, like Result types or exceptions.

This moves the burden from the developer having to manually ensure safety to the too providing a verifiably safe starting point.

A Type System for C/C++ Interop

Aela's type system can understand C/C++'s quirks better. It encodes invariants about C pointers and memory directly into the types.

Sized Pointers

Instead of a raw pointer, Aela has types like Pointer(T, size_t N), which represent a pointer to a buffer of N elements. This allows the compiler to enforce bounds checking at the FFI boundary.

Nullability and Ownership

Explicitly differentiate between Pointer(T) (nullable) and Reference(T) (non-nullable). And types that encode ownership semantics like OwnedPointer(T) (must be freed) vs. BorrowedPointer(T) (must not be freed).

Tainted Data

Data coming from C/C++ is considered "tainted" by the type system. It needs to be explicitly validated (e.g., checking a string for valid UTF-8, ensuring a value is within an expected range) before it can be used in the safe context.

Integrate Static and Dynamic Analysis

Since Aela is written in C, it can integrate powerful C/C++ analysis tools directly into the build process.

Clang's Analyzers

Aela uses libraries from the Clang/LLVM project to perform static analysis on the C/C++ code. During compilation, it automatically invokes analyzers to check for things like null pointer dereferences, use-after-free, or buffer overflows in the C code being called, and flag a warning or error if a potential issue is found.

Boundary Sanitization

A "debug mode" for FFI that injects runtime checks at the boundary.

When your code calls a C function, it could automatically add canaries or check buffer boundaries. When C code calls back into Aela, it can validate incoming pointers and data. This is similar to running with AddressSanitizer (ASan), but it's focused specifically on the FFI-boundary risks.

By taking these steps, Aela doesn't just stop its safety guarantees at the boundary. It actively polices that boundary, making brownfield integration significantly safer and more robust than the manual, high-discipline approach required by other languages.

FFI

The Foreign Function Interface (FFI) provides a mechanism for Aela code to call functions written in other programming languages, specifically C. This allows you to leverage existing C libraries, write performance-critical code in a lower-level language, or interact directly with the underlying operating system.

The core of Aela's FFI is the ffi definition, which declares a external C functions and their Aela type signatures or varibales and their types. The Aela compiler and runtime use these declarations to handle the "marshalling" of data—the process of converting data between Aela's internal representations and the C Application Binary Interface (ABI).

Declaring an FFI type

You declare a C function or C variable using the ffi keyword.

Example
0 ffi foo = fn ( string ) - > void ;
1 ffi bar = u32 ;

ABI Contract

A stable C ABI ( ae_c_abi.h ) defines the contract. It specifies the C-side string representation: typedef struct { char* ptr; int64_t len; } AeString;

Compiler Type Mapping

The Aela compiler's types.c maps the language's string type to an LLVM struct with an identical memory layout: %aela.string = type { i8*, i64 } .

Passing Convention

Strings are passed to C functions BY VALUE.

  • Aela code generates: call void @c_function(%aela.string %my_string) .
  • C code receives: void c_function(AeString my_string) .

Safety & Ownership

  • This pass-by-value convention is a "defensive" design.
  • The C function gets a copy of the string descriptor, preventing it
  • from modifying the original string's length or pointer in Aela's
  • memory.
  • Aela's runtime retains ownership of the underlying character buffer
  • ( char* ). The AeString struct is just a temporary, non-owning view.
Example
0 ffi = ; . . .

: The exact name of the function as it is defined in the C source code.

: The Aela type signature for the C function. This signature is the crucial contract that tells the Aela compiler how to call the C function correctly.

Example

Let's look at the stdio example from the standard library:

Example
0 ffi ae_stdout_write = fn ( string ) - > void ;

This code does the following:

It declares that there is an external C function named ae_stdout_write.

It specifies that from the Aela side, this function should be treated as one that accepts a single Aela string and returns void.

To call this function, you use the standard module access syntax:

Example
0 ae_stdout_write ( "Hello from C ! " ) ;

The Aela-C ABI and Data Marshalling When an FFI call occurs, the Aela compiler generates "glue" code to translate Aela types into types that C understands. This mapping follows a specific Application Binary Interface (ABI).

Primitive Types

Most Aela primitive types map directly to their C equivalents.

Aela Type C Type
i8, u8 int8_t, uint8_t
i16, u16 int16_t, uint16_t
i32, u32 int32_t, uint32_t
i64, u64 int64_t, uint64_t
f32 float
f64 double
bool bool (or \_Bool)
char uint32_t (UTF-32)
void void

Strings

The Aela string is a "fat pointer" struct containing a pointer to the data and a length. C, however, typically works with null-terminated char* strings.

Aela to C: When you pass an Aela string to an FFI function, the compiler automatically extracts the internal ptr and passes it as a const char* to the C function. The string data is guaranteed to be null-terminated, so standard C string functions can operate on it safely.

Aela's Internal runtime representation
0 struct string {
1 ptr : ptr , // Pointer to UTF-8 data
2 len : i64 // Length of the string
3 }

FFI Call:

The Aela Code
0 ae_stdout_write ( "Hello" ) ;

The C function receives a standard C string.

The C Implementation
0 void ae_stdout_write ( const char * message ) {
1 printf ( " % s" , message ) ;
2 }

Structs, Arrays, and Closures (Complex Types)

Complex aggregate types like structs, arrays, and closures cannot be passed directly by value to C functions. The ABI for these types is simple: you pass a pointer.

Aela to C: When passing a complex type, Aela passes a pointer to the object's memory layout. Your C code receives an opaque pointer (void\*) to this data. It is your responsibility in C to know the memory layout of the Aela type and cast the pointer accordingly to access its fields.

This is an advanced use case and requires careful handling to avoid memory corruption. You must ensure that the struct definition in your C code exactly matches the memory layout of the Aela struct.

Often you end up with an opaque strct in Aela. These can not have methods or properties.

An Opaque Struct
0 struct StringBuilder ;
1 ``
2
3 ## Variadic Functions (...args)
4
5 Variadic arguments are not directly passed through the FFI boundary . The . . . args
6 feature is part of the Aela language and its calling convention , not the C ABI .
7
8 As seen in the io . print example , you must handle variadic arguments within your
9 Aela code and call the FFI function with a concrete , non - variadic signature .
10
11 `` `example
12 // The public-facing Aela function is variadic.
13
14 export fn print ( formatString : string , . . . args ) - > void {
15 stdio : : ae_stdout_write ( std : : format ( formatString , . . . args ) ) ;
16 }

This design provides a safe and clear boundary. The complex, type-safe variadic handling happens within the Aela runtime, while the FFI call itself remains a simple, direct translation of the string argument to a char*.

Linking C Code To make your C functions available to the Aela compiler, you must compile them into an object file (.o) or a library (.a, .so, .dylib) and include it during the final linking step.

The Aela driver will eventually provide flags to specify these external object files. For now, you would typically use a command like clang to link the Aela-generated object file with your C object file.

  1. Compile your Aela code aec your_program.ae -o your_program.o
  1. Compile your C code clang -c my_ffi_functions.c -o my_ffi_functions.o
  1. Link them together clang your_program.o my_ffi_functions.o -o
  2. final_executable

This process creates the final executable where the Aela runtime can find and call your C functions.

Memory & Mutability Model

1. Default Allocation Semantics

  • All values start on the stack by default.
  • Stack allocations are:
  • Fast
  • Scoped
  • Owned directly by the binding.
Example
0 let foo : Foo = { num : 0 } ; // Foo is stack-allocated

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.

Example
0 let x : & T = new { . . . val } ; // Allocate on the heap
1 let x : & T = new shared { . . . val } ; // Single-threaded, mutable
2 let x : & T = new shared atomic { . . . val } ; // Thread-safe atomic
3 var x : & T = new weak { . . . val } ; // Mutable weak reference

Behavior

  • 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)
  • There is no `mut` keyword
  • Mutability is not encoded in types, structs, or fields

Example

Example
0 let a : & Foo = new shared { . . . foo } ; // Immutable heap reference
1 var b : & Foo = new shared { . . . foo } ; // Mutable heap reference

Attempting to mutate a let -bound reference results in a compile-time error .

4. Weak References & Cycle Prevention [NOT IMPLEMENTED]

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'

Example
0 if let strong_ref = weak_ref {
1 // This block only runs if the object is still alive.
2 // 'strong_ref' is a new, temporary strong reference of type &Foo.
3 strong_ref . do_something ( ) ;
4 }

5. Copying Stack Values

  • Heap allocation requires copying the stack value:
Example
0 new shared { . . . foo } // `{ ...foo }` performs a field-wise copy
  • 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.
a copy of the number 10 is passed. // bar(stack_str); -> a copy of the {ptr, i64} struct is passed." lang="example" title="- Stack Allocated" id="d4bdebbe552a">
- Stack Allocated
0 let stack_int : i32 = 10 ; // The variable 'stack_int' IS the number 10.
1 let stack_str : string = "hello" ; // The variable 'stack_str' IS the {ptr, i64} struct.
2
3 // When calling a function:
4 // foo(stack_int); -> a copy of the number 10 is passed.
5 // bar(stack_str); -> a copy of the {ptr, i64} struct is passed.

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.
- Heap allocated via new
0 let heap_int : i32 = new 42 ; // 'heap_int' is a POINTER to a box containing 42.
1 let heap_obj : MyStruct = new { } ; // 'heap_obj' is a POINTER to a box containing a MyStruct.
2 let heap_arr : u8 [ ] = new [ 1 , 2 , 3 ] ; // 'heap_arr' is a POINTER to a box for the array data.
3
4 // When calling a function:
5 // foo(heap_int); -> a copy of the POINTER is passed. The heap data is not touched.

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. The env_ptr points 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.
- Special Case
0 let captured_var = new "text" ;
1
2 let my_closure = fn ( ) {
3 print ( captured_var ) ; // Captures the pointer 'captured_var'.
4 } ;
5
6 // 'my_closure' is a {func_ptr, env_ptr} struct.
7 // 'env_ptr' points to a heap box which contains a copy of the 'captured_var' pointer.

GUI

Aela provides a cross platform GUI library that works on MacOS, iOS, Windows, Linux, & Android.

Install

Install the module into your dependencies.
0 aec install stablestate / ui
Your index.json will then have the dependency.
0 {
1 "name" : "my - program" ,
2 "version" : "0 . 1 . 0" ,
3 "entry" : "main . ae" ,
4 "output" : "build / app" ,
5 "include" : [ "src /**/ * . ae" ] ,
6 "exclude" : [ "tests / * * " ] ,
7 "dependencies" : [
8 {
9 "name" : "ui" ,
10 "url" : "https : / / github . com / stablestate / ui"
11 }
12 ]
13 }

Example

int { fn App () -> &View { let onclick: EventCallback = fn (event: &Event) -> void { // do something }; return new View { children: [ WebView { url: "https://news.ycombinator.com" }, Button { label: "save" }, Button { label: "cancel", onclick } ], onclick }; } let app: &View = App(); let loop: &io::AeEventLoop = io::getLoop(); initLoop(loop); let windowOptions: &WindowOptions = new { title: "Aela UI Test", width: 800.0, height: 600.0 }; let window: &Window = new Window(windowOptions); window.show(); window.render(app); runLoop(); return 0; }" lang="ae" title="Getting started" id="5286b485edc61">
Getting started
0 import {
1 initLoop ,
2 runLoop ,
3 WindowOptions ,
4 Window
5 } from "ui" ;
6
7 async fn main ( args : string [ ] ) - > int {
8 fn App ( ) - > & View {
9 let onclick : EventCallback = fn ( event : & Event ) - > void {
10 // do something
11 } ;
12
13 return new View {
14 children : [
15 WebView { url : "https : / / news . ycombinator . com" } ,
16 Button { label : "save" } ,
17 Button { label : "cancel" , onclick }
18 ] ,
19 onclick
20 } ;
21 }
22
23 let app : & View = App ( ) ;
24
25 let loop : & io : : AeEventLoop = io : : getLoop ( ) ;
26 initLoop ( loop ) ;
27
28 let windowOptions : & WindowOptions = new {
29 title : "Aela UI Test" ,
30 width : 800 . 0 ,
31 height : 600 . 0
32 } ;
33
34 let window : & Window = new Window ( windowOptions ) ;
35
36 window . show ( ) ;
37 window . render ( app ) ;
38
39 runLoop ( ) ;
40
41 return 0 ;
42 }

Why 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.

Formal Grammar Spec

Grammar
0 ( * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = )
1 ( Aela Language Grammar 0 . 0 . 6 )
2 ( Finalized : 2025 - 09 - 06 )
3 ( = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = * )
4
5 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
6 ( PROGRAM )
7 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
8
9 Program : : = { TopLevelDeclaration } EOF
10
11 TopLevelDeclaration : : =
12 ImportStatement
13 | ReExportDeclaration
14 | [ KW_EXPORT ] (
15 FfiDeclaration
16 | VarDeclaration
17 | FunctionDeclaration
18 | StructDeclaration
19 | ImplBlock
20 | EnumDeclaration
21 | TypeAliasDeclaration
22 | SystemDeclaration
23 )
24
25 TypeAliasDeclaration : : = KW_TYPE IDENTIFIER '=' Type ';'
26
27 ReExportDeclaration : : =
28 KW_EXPORT ( NamedImport | IDENTIFIER )
29 KW_FROM STRING_LITERAL ';'
30
31 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
32 ( IMPORTS )
33 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
34
35 ImportStatement : : =
36 KW_IMPORT ( NamedImport | IDENTIFIER )
37 KW_FROM STRING_LITERAL ';'
38
39 NamedImport : : = '{' [ ImportSpecifier { ',' ImportSpecifier } [ ',' ] ] '}'
40 ImportSpecifier : : = IDENTIFIER [ ':' IDENTIFIER ]
41
42 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
43 ( FFI ( Foreign Function Interface ) )
44 ( - Contracts are compile - time enforced to be UB - free )
45 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
46
47 FfiDeclaration : : = KW_FFI IDENTIFIER '=' Type ';'
48
49 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
50 ( DECLARATIONS )
51 ( - var is mutable , let is immutable )
52 ( - aliases are borrow - checked by the analyzer )
53 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
54
55 VarDeclaration : : = ( KW_VAR | KW_LET ) IDENTIFIER ':' Type
56 [ '=' Expression ] ';'
57
58 StructDeclaration : : = KW_STRUCT IDENTIFIER [ GenericParameters ] ( '{' { StructFieldDeclaration } '}' | ';' )
59
60 StructFieldDeclaration : : =
61 ( IDENTIFIER ':' Type ';' )
62 | ( '...' IDENTIFIER ';' )
63
64 FnModifiers : : =
65 [ ( KW_THREAD [ KW_PURE ] ) // thread; pure+thread allowed
66 | ( KW_PURE [ KW_THREAD ] ) // pure; optional thread
67 | KW_ASYNC // async alone (no pure)
68 ]
69
70 ImplBlock : : = KW_IMPL Type [ GenericParameters ] '{' { FunctionDeclaration | InvariantDeclaration } '}'
71
72 FunctionDeclaration : : =
73 FnModifiers KW_FN IDENTIFIER [ GenericParameters ]
74 '(' [ FunctionParameters ] ')' '->' Type ( Block | ';' )
75
76 EnumDeclaration : : = KW_ENUM IDENTIFIER [ GenericParameters ]
77 '{' [ EnumVariant { ',' EnumVariant } [ ',' ] ] '}'
78
79 EnumVariant : : = IDENTIFIER [ '(' Type { ',' Type } ')' ]
80
81 GenericParameters : : = '(' IDENTIFIER { ',' IDENTIFIER } ')'
82
83 SystemDeclaration : : = KW_SYSTEM IDENTIFIER '{'
84 { ActionDeclaration
85 | InvariantDeclaration
86 | PropertyDeclaration
87 }
88 '}'
89
90 ActionDeclaration : : = KW_ACTION IDENTIFIER
91 '(' [ FunctionParameters ] ')'
92 [ RequiresClause ]
93 [ EnsuresClause ]
94 Block
95
96 RequiresClause : : = KW_REQUIRES Expression
97 EnsuresClause : : = KW_ENSURES Expression
98
99 InvariantDeclaration : : = KW_INVARIANT IDENTIFIER ':' Expression
100 PropertyDeclaration : : = KW_PROPERTY IDENTIFIER ':' TemporalExpression
101
102 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
103 ( TYPES )
104 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
105
106 Type : : = [ '&' ] PostfixType
107
108 PostfixType : : = SimpleType { ArrayTypeModifier | TypeApplication | '?' }
109
110 MapType : : = KW_MAP '(' Type ',' Type ')'
111
112 SimpleType : : = PrimitiveType
113 | KW_VOID
114 | FunctionTypeSignature
115 | PathExpression
116 | MapType
117 | RefinementType
118 | '(' Type ')'
119
120 FunctionTypeSignature : : =
121 FnModifiers KW_FN '(' [ FunctionTypeParameters ] ')' '->' Type
122
123 RefinementType : : = '{' IDENTIFIER ':' Type KW_WHERE Expression '}'
124
125 PrimitiveType : : = KW_U8 | KW_I8 | KW_U16 | KW_I16 | KW_U32 | KW_I32
126 | KW_U64 | KW_I64 | KW_F32 | KW_F64 | KW_BOOL
127 | KW_CHAR | KW_STRING | KW_INT
128
129 TypeApplication : : = '(' [ Type { ',' Type } ] ')'
130
131 FunctionParameters : : = Parameter { ',' Parameter }
132 Parameter : : = [ '...' ] [ KW_MUT ] IDENTIFIER ':' Type
133 FunctionTypeParameters : : = FunctionTypeParameter { ',' FunctionTypeParameter }
134 FunctionTypeParameter : : = [ KW_MUT ] Type
135 ArrayTypeModifier : : = '[' [ Expression ] ']'
136
137 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
138 ( COMMENTS )
139 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
140
141 ( * A single - line comment starts with // and continues to the end of the line )
142 SingleLineComment : : = '//' { ~('\n' | '\r') }
143
144 ( * A multi - line comment starts with /* and ends with */ )
145 MultiLineComment : : = '/*' { . } '*/'
146
147 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
148 ( STATEMENTS ( Unambiguous ) )
149 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
150
151 Statement : : = MatchedStatement | UnmatchedStatement
152
153 MatchedStatement : : =
154 KW_IF '(' Expression ')' MatchedStatement KW_ELSE MatchedStatement
155 | KW_IF KW_LET Pattern '=' Expression Block KW_ELSE MatchedStatement
156 | Block
157 | KW_RETURN [ Expression ] ';'
158 | BreakStatement
159 | ContinueStatement
160 | WhileStatement
161 | ForStatement
162 | MatchStatement
163 | ExpressionStatement
164 | VarDeclaration
165 | FunctionDeclaration
166 | ';'
167
168 UnmatchedStatement : : =
169 KW_IF '(' Expression ')' Statement
170 | KW_IF '(' Expression ')' MatchedStatement KW_ELSE UnmatchedStatement
171 | KW_IF KW_LET Pattern '=' Expression Block
172 | KW_IF KW_LET Pattern '=' Expression Block KW_ELSE UnmatchedStatement
173
174 Block : : = '{' { Statement } '}'
175 ExpressionStatement : : = Expression ';'
176 BreakStatement : : = KW_BREAK ';'
177 ContinueStatement : : = KW_CONTINUE ';'
178
179 WhileStatement : : = KW_WHILE '(' Expression ')' Statement
180
181 ForStatement : : = KW_FOR '(' ForDeclaratorList KW_IN Expression ')' Statement
182 ForDeclaratorList : : = ForDeclarator { ',' ForDeclarator }
183 ForDeclarator : : = ( KW_LET | KW_VAR ) IDENTIFIER ':' Type
184
185 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
186 ( MATCH ( Mandatory Exhaustive ) )
187 ( - Expression form for atomic initialization . )
188 ( - Statement form for control flow . )
189 ( - Guards , @ - bindings , and nesting are disallowed . )
190 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
191
192 MatchStatement : : = KW_MATCH '(' Expression ')' '{'
193 [ MatchStmtArm { ',' MatchStmtArm } [ ',' ] ]
194 '}'
195
196 MatchStmtArm : : = MatchArmPattern '=>' ( Block | ';' )
197
198 MatchArmPattern : : = Pattern { '|' Pattern }
199
200 Pattern : : =
201 LiteralPattern
202 | IDENTIFIER // binding
203 | '_' // wildcard
204 | PathExpression [ '(' [ PatternList ] ')' ] // unit/tuple-variant
205 | TuplePattern
206 | StructPattern
207
208 TuplePattern : : = '(' [ PatternList [ ',' ] ] ')'
209
210 StructPattern : : = '{' [ StructFieldPat { ',' StructFieldPat } [ ',' ] ] '}'
211 StructFieldPat : : = IDENTIFIER ( ':' Pattern ) ? | '...' IDENTIFIER
212
213 PatternList : : = Pattern { ',' Pattern }
214
215 RangePattern : : =
216 INT_LITERAL ( '..' | '..=' ) INT_LITERAL
217 | CHAR_LITERAL ( '..' | '..=' ) CHAR_LITERAL
218
219 LiteralPattern : : = INT_LITERAL | STRING_LITERAL | CHAR_LITERAL | KW_TRUE | KW_FALSE | RangePattern
220
221 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
222 ( EXPRESSIONS ( Pratt Parser Aligned ) )
223 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
224
225 Expression : : = AssignmentExpression
226
227 AssignmentExpression : : = CoalescingExpression [ '=' AssignmentExpression ]
228
229 CoalescingExpression : : = LogicalOrExpression { '??' LogicalOrExpression }
230
231 LogicalOrExpression : : = LogicalAndExpression { '||' LogicalAndExpression }
232
233 LogicalAndExpression : : = BitwiseOrExpression { '&&' BitwiseOrExpression }
234
235 BitwiseOrExpression : : = BitwiseXorExpression { '|' BitwiseXorExpression }
236
237 BitwiseXorExpression : : = BitwiseAndExpression { '^' BitwiseAndExpression }
238
239 BitwiseAndExpression : : = ShiftExpression { '&' ShiftExpression }
240
241 ShiftExpression : : = EqualityExpression { ( '<<' | '>>' ) EqualityExpression }
242
243 EqualityExpression : : = ComparisonExpression { ( '==' | '!=' ) ComparisonExpression }
244
245 ComparisonExpression : : = AdditiveExpression { ( '<' | '<=' | '>' | '>=' ) AdditiveExpression }
246
247 AdditiveExpression : : = MultiplicativeExpression { ( '+' | '-' ) MultiplicativeExpression }
248
249 MultiplicativeExpression : : = CastExpression { ( '*' | '/' | '%' ) CastExpression }
250
251 CastExpression : : = UnaryExpression { KW_AS Type }
252
253 UnaryExpression : : =
254 ( KW_AWAIT | '-' | '!' | '&' | '~' ) UnaryExpression
255 | PostfixExpression
256
257 PostfixExpression : : =
258 PrimaryExpression {
259 '(' [ ArgumentList ] ')'
260 | '[' Expression ']'
261 | ( '.' | '?.' ) IDENTIFIER
262 }
263
264 PrimaryExpression : : =
265 PathExpression
266 | Literal
267 | '(' Expression ')'
268 | ArrayLiteral
269 | NamedStructLiteral
270 | AnonymousStructLiteral
271 | FunctionExpression
272 | NewExpression
273 | MatchExpression
274
275 MatchExpression : : = KW_MATCH '(' Expression ')' '{'
276 [ MatchExprArm { ',' MatchExprArm } [ ',' ] ]
277 '}'
278
279 MatchExprArm : : = MatchArmPattern '=>' Expression
280
281 PathExpression : : = IDENTIFIER { '::' IDENTIFIER }
282
283 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
284 ( TEMPORAL EXPRESSIONS )
285 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
286
287 TemporalExpression : : =
288 KW_ALWAYS Expression
289 | KW_EVENTUALLY Expression
290 | KW_NEXT Expression
291 | Expression KW_UNTIL Expression
292 | Expression KW_RELEASE Expression
293 | KW_FORALL IDENTIFIER KW_IN Expression ':' Expression
294 | KW_EXISTS IDENTIFIER KW_IN Expression ':' Expression
295 | Expression
296
297 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
298 ( LITERALS & HELPER RULES )
299 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
300
301 Literal : : =
302 INT_LITERAL | FLOAT_LITERAL | STRING_LITERAL | STRING_MULTILINE
303 | CHAR_LITERAL | KW_TRUE | KW_FALSE
304
305 ArrayLiteral : : = '[' [ ArgumentList ] ']'
306 NamedStructLiteral : : = PathExpression StructLiteralBody
307 AnonymousStructLiteral : : = StructLiteralBody
308 StructLiteralBody : : = '{' [ StructElement { ',' StructElement } [ ',' ] ] '}'
309 StructElement : : = ( IDENTIFIER ':' Expression ) | IDENTIFIER | '...' Expression
310
311 ArgumentList : : = CallArgument { ',' CallArgument }
312 CallArgument : : = [ '...' ] [ KW_MUT ] Expression
313
314 FunctionExpression : : = FnModifiers KW_FN '(' [ FunctionParameters ] ')' [ '->' Type ] Block
315
316 NewExpression : : = KW_NEW PrimaryExpression
317
318 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
319 ( TERMINALS ( TOKENS ) )
320 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
321
322 IDENTIFIER
323 INT_LITERAL , FLOAT_LITERAL , STRING_LITERAL , STRING_MULTILINE , CHAR_LITERAL
324
325 ( * Keywords : * )
326 KW_LET , KW_VAR , KW_FN , KW_IF , KW_IN , KW_ELSE , KW_WHILE , KW_FOR ,
327 KW_RETURN , KW_BREAK , KW_CONTINUE , KW_WHERE ,
328 KW_ASYNC , KW_AWAIT , KW_AS , KW_STRUCT , KW_IMPL , KW_THREAD ,
329 KW_ENUM , KW_MATCH , KW_TYPE , KW_VOID , KW_INT ,
330 KW_U8 , KW_I8 , KW_U16 , KW_I16 , KW_U32 , KW_I32 , KW_U64 , KW_I64 ,
331 KW_F32 , KW_F64 , KW_BOOL , KW_CHAR , KW_STRING , KW_TRUE , KW_FALSE ,
332 KW_IMPORT , KW_EXPORT , KW_FROM , KW_FFI , KW_MAP ,
333
334 ( * No shared mutability without atomics ! * )
335 KW_NEW , KW_SHARED , KW_ATOMIC , KW_WEAK , KW_MUT ,
336
337 KW_SYSTEM , KW_ACTION , KW_REQUIRES , KW_ENSURES ,
338 KW_INVARIANT , KW_PROPERTY ,
339 KW_FORALL , KW_EXISTS , KW_ALWAYS , KW_EVENTUALLY , KW_NEXT , KW_UNTIL ,
340 KW_RELEASE , KW_PURE
341
342 ( * Operators and Delimiters : Arithmetic Wraps )
343 '=' , '+' , '-' , '*' , '/' , '%' , '&' , '==' , '!=' , '<' , '<=' , '>' , '>=' ,
344 '!' , '&&' , '||' , '|' , '^' , '~' , '<<' , '>>' , '(' , ')' , '{' , '}' , '[' , ']' ,
345 ',' , ';' , '.' , ':' , '::' , '?' , '?.' , '??' , '...' , '..=' , '..' , '->' , '_' , '=>'
346
347 EOF

Get Started

Aela is a software platform for creating formally verifiable, memory safe, and highly reliable applications. What's included in the compiler:

  • Works in any editor
  • Provides a built in linter, formatter, and LSP.
  • A local, offline-first agent that understands the compiler and your codebase and can talk to your other AI services.
  • Supports JIT module (that's hot reloading for compiled programs, aka: edit and continue)

Install the compiler.

Example
0 sudo sh - c 'curl -fsSL https://stablestate.ai/$CUSTOMER_ID | bash'

In a new directory, create a new project using the following command.

Example
0 aec init

This will create some default files.

Example
0 .
1 ├── index . json
2 └── src
3 └── main . ae

Edit the index.json file to name your project.

Example
0 {
1 "name" : "aela - tests" ,
2 "version" : "0 . 1 . 0" ,
3 "entry" : "main . ae" ,
4 "output" : "build / main" ,
5 "include" : [ "src /**/ * . ae" ] ,
6 "exclude" : [ "tests / * * " ] ,
7 "dependencies" : [
8 {
9 "name" : "ui" ,
10 "url" : " . . / lib / ui"
11 }
12 ]
13 }

Next you’ll edit the main.ae file

int { io::print("Hello, Aela!"); return 0; }" lang="rust" title="Example" id="8580699c4d9ff">
Example
0 // Aela Hello World
1
2 import io from "io" ;
3
4 fn main ( args : string [ ] ) - > int {
5 io : : print ( "Hello , Aela ! " ) ;
6 return 0 ;
7 }

To build your project, run the following command.

Example
0 aec build

You’ll see a new directory with the compiled program that you can run.

- new files
0 .
1 ├── build
2 │ └── main
3 ├── index . json
4 └── src
5 └── main . ae

Compiler Modes

aec build

This is your traditional, one-shot Ahead-Of-Time (AOT) compiler command.

What Compiles your entire project from source into a final, optimized executable binary.
How It invokes the compiler engine, which runs all formal verifications, performs release-level optimizations (which can be slow), and links everything together. It's a non-interactive process designed for final output.
Why Running in a Continuous Integration (CI) pipeline or when you're ready to create a production version of your application.

aec run

This is a convenience command for development.

What Builds and immediately executes your program.
How It's a simple wrapper that first performs an aec build (likely with fewer optimizations to be faster than a release build) and then runs the resulting binary.
Why Quickly testing a command-line application's behavior without needing a full watch session.

aec watch

This command exposes the engine's interactive mode directly to the command line.

What Starts the persistent, incremental engine to monitor files and provide continuous feedback in the terminal.
How It activates the same underlying engine that the LSP uses. On every change, it re-verifies your code and can be configured to take an action, such as re-running your test suite (aec watch --exec test) or restarting your application via the JIT.
Why For developers who prefer working in the terminal, it enables a very fast Test-Driven Development (TDD) loop without the overhead of an IDE.

aec package

This is a higher-level workflow and distribution tool.

What Bundles your project into a distributable format.
How It would first run aec build --release to create the optimized executable. Then, it would gather other assets—like documentation, licenses, and configuration files—and package them into a compressed archive (like a .tar.gz) for publishing to a registry or for distribution.
Why Publishing a new version of your library or application for others to use.

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 wont 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

- /path/to/my-app/index.json
0 {
1 "name" : "my - app" ,
2 "version" : "0 . 1 . 0" ,
3 "entry" : "src / main . ae"
4 }

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.json to the
  • dependency's root directory.

Example

- index.json for a project that uses a UI library
0 {
1 "name" : "my - gui - app" ,
2 "version" : "1 . 0 . 0" ,
3 "entry" : "src / app . ae" ,
4 "dependencies" : {
5 "ui" : " . . / libs / aela - ui" ,
6 "database" : " . . / libs / aela - db"
7 }
8 }

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

- ../libs/aela-ui/src/window.ae
0 // This struct can be imported by other modules.
1 export struct WindowOptions {
2 title : string ;
3 width : int ;
4 height : int ;
5 }
6
7 // This function is private to the window.ae module.
8 fn internal_helper ( ) - > void {
9 // ...
10 }

Re-exports

- re-export pattern
0 import { Thing1 , Thing2 } from "things . ae" ;
1
2 export Thing1 ;
3 export Thing2 ;

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

";" lang="" title="Example" id="ab5bd6fe58171">
Example
0 import from " " ;

Example

void { // Access symbols using the `::` namespace operator. let opts: &ui::WindowOptions = new ui::WindowOptions(); helpers::do_something(); }" lang="example" title="- src/app.ae" id="49a0501874e3d">
- src/app.ae
0 // Import the "ui" package, which was defined in index.json.
1 import ui from "ui" ;
2
3 // Import a local utility file using a relative path.
4 import helpers from " . / helpers . ae" ;
5
6 fn main ( ) - > void {
7 // Access symbols using the `::` namespace operator.
8 let opts : & ui : : WindowOptions = new ui : : WindowOptions ( ) ;
9 helpers : : do_something ( ) ;
10 }

Named Imports

This allows you to import specific functions or types directly into the current scope, without needing a namespace qualifier.

Syntax

";" lang="" title="Example" id="2068a58526a41">
Example
0 import { , : } from " " ;

Example EXAMPLE

void { // `Window` can be used directly. let win: &Window = new Window(); // `WindowOptions` is available under the alias `Options`. let opts: &Options = new Options(); }" lang="example" title="- src/app.ae" id="5ed0ea6e30563">
- src/app.ae
0 import { Window , WindowOptions : Options } from "ui" ;
1
2 fn main ( ) - > void {
3 // `Window` can be used directly.
4 let win : & Window = new Window ( ) ;
5
6 // `WindowOptions` is available under the alias `Options`.
7 let opts : & Options = new Options ( ) ;
8 }

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

- index.json for an app with native UI code on macOS
0 {
1 "name" : "aela - native - app" ,
2 "version" : "0 . 1 . 0" ,
3 "entry" : "src / main . ae" ,
4 "sources" : {
5 "darwin" : [ "src / native / apple . mm" ]
6 } ,
7 "link" : {
8 "platform" : {
9 "darwin" : [
10 " - framework" , "Foundation" ,
11 " - framework" , "AppKit" ,
12 " - framework" , "ObjectiveC"
13 ]
14 }
15 }
16 }

This configuration tells the Aela compiler:

  1. On macOS, compile the src/native/apple.mm Objective-C++ file.
  2. When linking the final executable, add flags to link against the
  3. Foundation, AppKit, and ObjectiveC system frameworks.

Type System Features

Refinement Types

Refinement types allow you to create a subtype from an existing type by adding constraints that are checked at compile time. This is useful for creating more precise and bug-resistant types.

Example
0 type NewTypeName = { value_name : BaseType where condition_expression } ;
value_name A name for the value, used within the where clause.
BaseType The type you are constraining (e.g., i32, string).
condition_expression A boolean expression that must be true for any value of this new type.

Example

You can define a type that only allows positive integers, preventing invalid values at compile time.

- Positive Integer
0 // Definition
1 type PositiveInt = { n : i32 where n > 0 } ;
2
3 // Usage
4 fn set_age ( age : PositiveInt ) {
5 // ... function body
6 }
7
8 set_age ( 10 ) ; // OK
9 set_age ( - 5 ) ; // Compile-time error!

Example: Non-Empty String

Refinements can use functions or properties in their conditions.

Example
0 // Definition
1 type NonEmptyString = { s : string where s . length ( ) > 0 } ;
2
3 // Usage
4 fn print_greeting ( name : NonEmptyString ) {
5 // ...
6 }

The primary benefit is turning potential runtime errors (like invalid arguments) into compile-time errors.

Dependent Types

Dependent types allow a type's definition to depend on a value, not just another type. This lets you encode properties like the size of a collection directly into its type.

Defining Dependent Structs

You define a dependent struct by including value parameters in parentheses () in its declaration.

Syntax

struct TypeName(param_name: ParamType, ...);

Example

Here, the Vect type depends on a length len and a type T.

Length-Indexed Vector
0 // Definition of a vector that knows its length.
1 struct Vect ( len : nat , T : Type ) {
2 // ... implementation details
3 }
4
5 // Usage in a variable declaration.
6 let names : Vect ( 3 , string ) = [ "Alice" , "Bob" , "Charlie" ] ;
7 let empty : Vect ( 0 , i32 ) = [ ] ;

Using Dependent Types in Functions

Function signatures can use these value dependencies to enforce relationships between parameters, making impossible states unrepresentable.

Example

This function to get the first element of a vector can only be called on a non-empty vector.

Safe head function
0 // The type signature guarantees vec has a length of at least 1.
1 fn safe_head ( len : nat , T : Type , vec : Vect ( len + 1 , T ) ) - > T {
2 return vec [ 0 ] ;
3 }
4
5 let first_name : string = safe_head ( 2 , string , names ) ; // OK
6
7 // The following line would cause a compile-time error because the
8 // compiler can see that the type Vect(0, i32) does not match
9 // the required type Vect(n + 1, T).
10 let error = safe_head ( - 1 , i32 , empty ) ;

This feature allows you to build extremely safe APIs by making the compiler aware of properties that are usually only checked at runtime.

Operators

Precedence Operator(s) Description Associativity
1 (Lowest) = Assignment Right-to-left
2 ?? Optional Coalescing Left-to-right
3 || Logical OR Left-to-right
4 && Logical AND Left-to-right
5 | Bitwise OR Left-to-right
6 ^ Bitwise XOR Left-to-right
7 & Bitwise AND Left-to-right
8 == , != Equality / Inequality Left-to-right
9 < , > , <= , >= Comparison Left-to-right
10 << , >> Bitwise Shift Left-to-right
11 + , - Addition / Subtraction Left-to-right
12 * , / , % Multiplication / Division / Modulo Left-to-right
13 ! , - , ~ , & (prefix), await Unary (Logical NOT, Negation, Bitwise NOT, Address-of, Await) Right-to-left
14 (Highest) () , [] , . , ?. , as Function Call, Index Access, Member Access, Type Cast Left-to-right

Flow Control

This document covers all flow control constructs in Aela, including conditionals, loops, and matching.

1. if / else

Syntax

Example
0 if ( )
1 [ else ]

Description

Standard conditional branching.

  • The condition must be an expression evaluating to bool .
  • Both then_branch and else_branch must be statements , usually blocks.

Examples

Example
0 if ( x > 0 ) {
1 print ( "Positive" ) ;
2 } else {
3 print ( "Non - positive" ) ;
4 }
5
6 if ( flag ) doSomething ( ) ;

2. while Loop

Syntax

Example
0 while ( )

Description

Loops as long as the condition evaluates to true .

Example

Example
0 while ( i < 10 ) {
1 i = i + 1 ;
2 }

3. for Loop

Syntax

Example
0 for ( in )

Description

Iterates over a collection or generator. Declarations must bind variables with types.

Example

Example
0 for ( let i : Int in 0 . . 10 ) {
1 print ( " { } " , i ) ;
2 }
3
4 for ( var x : String , var y : String in lines ) {
5 print ( " { } { } " , x , y ) ;
6 }

4. match Expression

Syntax

Example
0 match ( ) {
1 = > ,
2 . . .
3 _ = >
4 }

Description

Exhaustive pattern matching. Each match arm uses a pattern and a block.

  • _ is the wildcard pattern (required if not all cases are covered).
  • Patterns can be:
  • Literals: 1 , "foo" , 'c' , true , false
  • Identifiers: binds the value
  • Constructor patterns: Some(x) , Err(e)

Example

{ print("One"); }, _ => { print("Other"); } }" lang="aela" title="Example" id="36ee863b38fd8">
Example
0 match ( value ) {
1 0 = > { print ( "Zero" ) ; } ,
2 1 = > { print ( "One" ) ; } ,
3 _ = > { print ( "Other" ) ; }
4 }

5. return

Syntax

Example
0 return [ ] ;

Description

Exits a function immediately with an optional return value.

Examples

Example
0 return ;
1 return x + 1 ;

6. break

Syntax

Example
0 break ;

Description

Terminates the nearest enclosing loop.

7. continue

Syntax

Example
0 continue ;

Description

Skips to the next iteration of the nearest enclosing loop.

8. Blocks and Statement Composition

Syntax

Example
0 {
1 ;
2 . . .
3 }

Description

A block groups multiple statements into a single compound statement. Used for control flow bodies.

Example

Example
0 {
1 let x : Int = 1 ;
2 let y : Int = x + 2 ;
3 print ( y ) ;
4 }

9. Expression Statements

Syntax

Example
0 ;

Description

Evaluates an expression for side effects. Common for function calls or assignments.

Example

Example
0 doSomething ( ) ;
1 x = x + 1 ;

Optionals

Optionals provide a safe and explicit way to handle values that may or may not be present. Instead of using special values like null or -1 which can lead to runtime errors, Aela uses the Option type to wrap a potential value. The compiler will then enforce checks to ensure you handle the "empty" case safely.

Declaring an Optional Type

You can declare a variable or field as optional using two equivalent syntaxes:

  1. The `?` Suffix (Recommended) : This is the preferred, idiomatic syntax.
  2. It's a concise way to mark a type as optional.
Example
0 // A variable that might hold a string
1 let name : string ? ;
2
3 // A struct with optional fields
4 struct Profile {
5 age : u32 ? ;
6 bio : string ? ;
7 }
  1. The `Option(T)` Syntax : This is the formal, nominal type. The T?
  2. syntax is simply sugar for this. It can be useful in complex, nested type
  3. signatures for clarity.
Example
0 // This is equivalent to `let name: string?`
1 let name : Option ( string ) ;

Creating Optional Values

An optional variable can be in one of two states: it either contains a value, or it's empty. You use the Some and None keywords to create these states.

None : The Empty State

The None keyword represents the absence of a value. You can assign it to any optional variable, and the compiler will infer the correct type from the context.

Example
0 let age : u32 ? = None ;
1
2 let user : User = {
3 // The profile field is optional
4 profile : None
5 } ;
6 Some ( value ) : The Value - Holding State

To create an optional that contains a value, you wrap the value with the Some constructor.

Example
0 // Create an optional u32 containing the value 30
1 let age : u32 ? = Some ( 30 ) ;
2
3 let user : User = {
4 profile : Some ( {
5 email : "some@example . com" ,
6 age : Some ( 30 )
7 } )
8 } ;

The Optional-Coalescing Operator (??) (For Defaults)

This is the best way to unwrap an optional by providing a fallback value to use if the optional is None. The term "coalesce" means to merge or come together; this operator coalesces the optional's potential value and the default value into a single, guaranteed, non-optional result.

Example
0 // Get the user's email, or use a default if it's None.
1 // `email_address` will be a regular `string`, not a `string?`.
2 let email_address : string = user2 . profile ? . email ? ? "no - email - provided@domain . com" ;
3
4 print ( "Contacting user at : { } " , email_address ) ;

Using Optional Values

Aela provides mechanisms to safely work with optional values, preventing you from accidentally using an empty value as if it contained something.

Optional Chaining (?.)

The primary way to access members of an optional struct is with the optional chaining operator, ?.. If the optional is None, the entire expression short-circuits and evaluates to None. If it contains a value, the member access proceeds.

The result of an optional chain is always another optional.

Example
0 struct Profile {
1 email : string ;
2 }
3
4 struct User {
5 profile : Profile ? ;
6 }
7
8 fn main ( ) - > int {
9 let user1 : User = { profile : Some ( { email : "test@example . com" } ) } ;
10 let user2 : User = { profile : None } ;
11
12 // email1 will be an `Option(string)` containing Some("test@example.com")
13 let email1 : string ? = user1 . profile ? . email ;
14
15 // email2 will be an `Option(string)` containing None
16 let email2 : string ? = user2 . profile ? . email ;
17
18 return 0 ;
19 }

Explicit Checking (Match Statement)

Use match statements to explicitly handle the Some and None cases, allowing you to unwrap the value and perform more complex logic.

io::print("The name is: {}", value), None => io::print("No name was provided."), }" lang="" title="Example" id="c36380aec7d35">
Example
0 let name : string ? = Some ( "Aela" ) ;
1
2 match name {
3 Some ( value ) = > io : : print ( "The name is : { } " , value ) ,
4 None = > io : : print ( "No name was provided . " ) ,
5 }

Mutability

Aela enforces safety and clarity by requiring that any function intending to modify data must be explicitly marked. This prevents accidental changes and makes code easier to reason about. This is achieved through the mut keyword.

The Principle: Safe by Default

In Aela, all function parameters are immutable (read-only) by default. When you pass a variable to a function, you are providing a read-only view of it.

Example
0 fn read_runner ( r : & Runner ) {
1 // This is OK.
2 io : : print ( "Points : { } " , r . point ) ;
3
4 // This would be a COMPILE-TIME ERROR.
5 // r.point = 5;
6 }

Granting Permission to Mutate

To allow a function to modify a parameter, you must use the mut keyword in two places:

  1. The Function Definition: To declare that the function requires mutable
  2. access.
  3. The Call Site: To explicitly acknowledge that you are passing a variable
  4. to be changed.

This two-part system makes mutation a clear and intentional act.

In the Function Definition

Prefix the parameter you want to make mutable with mut . This is the function's "contract," stating its intent to modify the argument.

Example
0 fn reset_runner ( mut r : & Runner ) {
1 // This is now allowed because the parameter `r` is marked as `mut`.
2 r . point = 0 ;
3 r . passed = 0 ;
4 r . failed = 0 ;
5 }

At the Call Site

When you call a function that expects a mutable parameter, you must also prefix the argument with mut . This confirms you understand the variable will be modified.

Example
0 fn main ( ) {
1 // The variable itself must be mutable, declared with 'var'.
2 var my_runner = Runner . new ( ) ;
3
4 // The 'mut' keyword is required here to pass 'my_runner'
5 // to a function that expects a mutable argument.
6 reset_runner ( mut my_runner ) ;
7 }

The compiler will produce an error if you try to pass a mutable argument without the mut keyword, or if you try to pass an immutable ( let ) variable to a function that expects a mutable one. This ensures there are no surprises about where your data can be changed.

Errors

Out-of-the-box errors are simple, the verifier runs the check borrows and the life-times of variables and properties.

An error where mut keyword should have been used
0 Analyzer Error : Cannot assign to field 'point' because 'self' is immutable .
1 - - > / Users / paolofragomeni / projects / aela / lib / test . ae : 16 : 10
2
3 15 | fn ok ( self : & Self , cond : bool , desc : string ) - > bool {
4 16 - > self . point = self . point + 1 ;
5 | ^
6 17 |

Structs, Impl Blocks, and Memory Layout

struct Declarations: The Data Blueprint

A struct defines a composite data type. Its sole purpose is to describe the memory layout of a collection of named fields. Structs contain ONLY data members.

Syntax

Example
0 struct {
1 : ;
2 : ;
3 . . .
4 }

Example

Defines a type named 'Packet' that holds a sequence number, a size, and a single-byte flag.

Example
0 struct Packet {
1 sequence : u32 ;
2 size : u16 ;
3 is_urgent : u8 ;
4 }

impl Blocks: Attaching Behavior

An impl (implementation) block associates functions with an existing struct type. These functions are called methods. The impl block does NOT alter the struct's memory layout or size.

Example
0 impl {
1 // constructor (optional, special method)
2 fn constructor ( self : & Self , . . . ) - > Self { . . . }
3
4 // methods
5 fn ( self : & Self , . . . ) - > { . . . }
6 }

Details

  • The constructor is a special function that initializes the
  • struct's memory. It is called when using the new keyword.
  • Methods are regular functions that receive a reference to an
  • instance of the struct as their first parameter, named self .
  • Self (capital 'S') is a type alias for the struct being implemented.
  • Multiple impl blocks can exist for the same struct. The compiler
  • merges them.

Example

Example
0 impl Packet {
1 fn constructor ( self : & Self , seq : u32 ) - > Self {
2 self . sequence = seq ;
3 self . size = 0 ;
4 self . is_urgent = 0 ;
5 }
6
7 fn mark_urgent ( self : & Self ) - > void {
8 self . is_urgent = 1 ;
9 }
10 }

Memory Layout and Padding

Aela adopts C-style struct memory layout rules, including padding and alignment, to ensure efficient memory access and ABI compatibility.

  1. Sequential Layout: Fields are laid out in memory in the exact
  2. order they are declared in the struct definition.
  1. Alignment: Each field is aligned to a memory address that is a
  2. multiple of its own size (or the platform's word size for larger
  3. types). The compiler inserts unused "padding" bytes to enforce this.
  1. Struct Padding: The total size of the struct itself is padded to be a
  2. multiple of the alignment of its largest member. This ensures that
  3. in an array of structs, every element is properly aligned.

Rules:

Example
0 struct Packet {
1 sequence : u32 ; // 4 bytes
2 size : u16 ; // 2 bytes
3 is_urgent : u8 ; // 1 byte
4 }

Visual Layout (on a typical 64-bit system):

Byte Offset Content
0 sequence (Byte 0)
1 sequence (Byte 1)
2 sequence (Byte 2)
3 sequence (Byte 3) ← 4‑byte
Byte Offset Content
4 size (Byte 0)
5 size (Byte 1) ← 2‑byte
Byte Offset Content
6 is_urgent (Byte 0)
Byte Offset Content
7 PADDING (1 byte) ← struct padded to a multiple of 4 bytes (max)

TOTAL SIZE: 8 bytes

Heap vs. Stack Allocation

Aela supports both heap and stack allocation for structs, giving the programmer control over memory management and performance.

Stack allocation (Default for local variables):

  • How: A struct is allocated on the stack by declaring a variable of
  • the struct type and initializing it with a struct literal. The new
  • keyword is NOT used.
  • Lifetime: The memory is valid only within the scope where it is
  • declared (e.g., inside a function). It is automatically reclaimed
  • when the scope is exited.
  • Performance: Extremely fast. Allocation and deallocation are nearly
  • instant, involving only minor adjustments to the stack pointer.
Example
0 let my_packet : Packet = Packet {
1 sequence : 200 ,
2 size : 128 ,
3 is_urgent : 1
4 } ;

Heap Allocation (Explicit):

  • How: A struct is allocated on the heap using the new keyword, which
  • returns a reference ( & ) to the object.
  • Lifetime: The memory persists until it is no longer referenced. Its
  • lifetime is managed by the runtime's reference counter, not tied to a
  • specific scope.
  • Performance: Slower than stack allocation. Involves a call to the
  • system's memory allocator ( malloc ) and requires runtime overhead for
  • reference counting.
- Explicit Heap Allocation
0 let my_packet_ref : & Packet = new Packet ( 201 ) ;

When to use which:

  • STACK: Use for most local, temporary data. It's the idiomatic and
  • most performant choice for data that does not need to outlive the
  • function in which it was created.
  • HEAP: Use when a struct instance must be shared or returned from a
  • function and needs to have a lifetime independent of any single
  • scope. Also used for very large structs to avoid overflowing the stack.

Opaque Structs

Safety & Undefined Behavior (UB)

The primary benefit of opaque structs is preventing a whole class of undefined behavior by strengthening type safety at the language boundary.

How Safety is Increased

Eliminates Type Confusion: Before, you might have used a generic type like `u64` or `&void` to represent a C handle. The compiler had no way to know that a `u64` from `database_connect()` was different from a `u64` from `file_open()`. You could accidentally pass a database handle to a file function, leading to memory corruption or crashes. Now, `&DatabaseHandle` and `&FileHandle` are distinct, incompatible types *. The Aela compiler will issue a compile-time error if you try to misuse them, completely eliminating this risk.

Prevents Invalid Operations in Aela: * By disallowing member access and instantiation, we prevent Aela code from making assumptions about the C data structure. Aela code cannot accidentally:

Read from or write to a field that doesn't exist or has a different offset (`my_handle.field`). Create a struct of the wrong size on the stack ( let handle: StringBuilder ). * Perform pointer arithmetic on the handle. The only thing Aela code can do is treat the handle as an opaque value to be passed back to the C library, which is the only safe way to interact with it.

For Users of Opaque Structs

Your documentation should include:

  1. Purpose and Syntax: Explain that opaque structs are for safely handling foreign pointers/handles. Show the syntax:
Example
0 // in lib/mylib.ae
1 export struct MyFFIHandle ;
  1. Rules of Engagement: Clearly state the allowed and disallowed operations we implemented.

Allowed: Passing to/from FFI functions, assigning to other variables of the same type, comparing for equality. Disallowed: Member access ( . ), instantiation ( new ), and dereferencing. Always use a reference ( &MyFFIHandle ).

  1. A Mandatory Safety Section on Lifetimes: This section must be prominent. It should explain the dangling pointer risk and establish a clear best practice.

When working with opaque handles, you are responsible for managing their memory. Most C libraries provide functions for creating and destroying these objects. You must call the destruction function to prevent memory leaks and undefined behavior.

&StringBuilder; ffi ae_runtime_sb_append: fn(&StringBuilder, string); ffi ae_runtime_sb_destroy: fn(&StringBuilder); // <-- The cleanup function fn main() -> int { let sb = ae_runtime_sb_new(); ae_runtime_sb_append(sb, "hello"); // CRITICAL: You must call destroy when you are done. ae_runtime_sb_destroy(sb); // Using `sb` after this point is UNDEFINED BEHAVIOR. // ae_runtime_sb_append(sb, " world"); // <-- DANGEROUS! return 0; }" lang="aela" title="Example: Managing Lifetimes" id="0b432d411f723">
Example: Managing Lifetimes
0 `` `aela
1 import { StringBuilder } from " . / runtime . ae" ;
2
3 // FFI Declarations for a C string builder
4 ffi ae_runtime_sb_new : fn ( ) - > & StringBuilder ;
5 ffi ae_runtime_sb_append : fn ( & StringBuilder , string ) ;
6 ffi ae_runtime_sb_destroy : fn ( & StringBuilder ) ; // <-- The cleanup function
7
8 fn main ( ) - > int {
9 let sb = ae_runtime_sb_new ( ) ;
10 ae_runtime_sb_append ( sb , "hello" ) ;
11
12 // CRITICAL: You must call destroy when you are done.
13 ae_runtime_sb_destroy ( sb ) ;
14
15 // Using `sb` after this point is UNDEFINED BEHAVIOR.
16 // ae_runtime_sb_append(sb, " world"); // <-- DANGEROUS!
17
18 return 0 ;
19 }

Interfaces

This document specifies the design and behavior of Aela's system for polymorphism, which is based on interface, struct, and impl...as... declarations.

Overview

Aela's polymorphism is designed to be explicit, safe, and familiar. It allows developers to write flexible code that can operate on different data types in a uniform way, a concept known as dynamic dispatch. This is achieved by separating a contract's definition (the interface) from its implementation (the struct and impl block).

Example
0 interface Element {
1 fn onclick ( event : & Event ) - > void ;
2 }
3
4 struct Button {
5 handle : i64 ;
6 }
7
8 impl Button as Element {
9 fn constructor ( self : & Self , someArg1 : string ) {
10 // fired when new is used
11 }
12 fn init ( self : & Self , someArg1 : string ) {
13 // fired when ever a struct is initialized.
14 }
15 fn onclick ( self : & Self , event : & Event ) - > void {
16 // fired when called directly (statically or dynamically)
17 }
18 }
19
20 impl Button as Element {
21 fn ontoch ( self : & self , event : & Event ) - > void {
22 }
23 }

The core philosophy is:

Interfaces define abstract contracts or capabilities.

Structs define concrete data structures.

impl...as... blocks prove that a concrete struct satisfies an abstract interface.

Components

The interface Declaration

An interface defines a set of method signatures that a concrete type must implement to conform to the contract.

Example
0 interface {
1 fn ( ) - > ;
2 // ... more method signatures
3 }

Rules:

An interface block can only contain method signatures. It cannot contain any data fields.

Method signatures within an interface must not have a body. They must end with a semicolon ;.

The self parameter in an interface method must be of a reference type (e.g., &self).

Example
0 interface Serializable {
1 fn serialize ( & self ) - > string ;
2 }

The struct Declaration

A struct defines a concrete data type. Its role is unchanged.

Example
0 struct {
1 : ;
2 // ... more data fields
3 }

Rules:

A struct can only contain data fields. Method implementations are defined separately in impl blocks.

Example
0 struct User {
1 id : int ;
2 username : string ;
3 }

The impl...as... Declaration

This block connects a concrete struct to an interface, proving that the struct fulfills the contract.

Example
0 impl as {
1 // Implementations for all methods required by the interface
2 fn ( ) - > {
3 // ... method body ...
4 }
5 }

Rules:

The impl block must provide a concrete implementation for every method defined in the .

The signature of each implemented method must be compatible with the corresponding signature in the interface.

A single struct may implement multiple interfaces by using separate impl...as... blocks for each one.

Example
0 impl User as Serializable {
1 fn serialize ( & self ) - > string {
2 // Implementation of the serialize method for the User struct
3 return std : : format ( " { { \"id\" : { } , \"username\" : \" { } \" } } " , self . id , self . username ) ;
4 }
5 }

Interface Types

A variable can be declared with an interface type by using a reference. This creates a "trait object" or "fat pointer" that can hold any concrete type that implements the interface.

Syntax: &

Behavior: A variable of type & is a fat pointer containing two components:

A pointer to the instance data (e.g., a &User).

A pointer to the v-table for the specific (Struct, Interface) implementation.

Example
0 let objects : & Serializable [ ] = [
1 & User { id : 1 , username : "aela" } ,
2 & Document { title : "spec . md" }
3 ] ;
4
5 for ( let obj : & Serializable in objects ) {
6 // This call is dynamically dispatched using the v-table.
7 io : : print ( obj . serialize ( ) ) ;
8 }

Formal Verification

This document specifies the design and behavior of Aela's compile-time verification system , which ensures the correctness of a program through formal specifications — namely, invariant and property declarations embedded in impl blocks.

Overview

Aela enables developers to write mathematically precise specifications that describe the expected state and behavior of a program, which the compiler formally verifies at compile time. These specifications are not runtime code — they do not execute, incur no runtime cost, and exist solely to ensure program correctness before code generation.

There are two key constructs:

`invariant`: A safety condition that must always hold before and after each function in an `impl` block. property : A liveness or temporal condition that must hold across all valid execution traces of a type’s behavior.

These declarations allow Aela to verify complex asynchronous and stateful systems using SMT solvers or model checking , without requiring users to learn a separate formal language.

Example
0 struct BankAccount {
1 balance : i32 ;
2 overdraft_limit : u32 ;
3 }
4
5 impl BankAccount {
6 // Always ensure the balance never drops below the allowed overdraft.
7 invariant balanceIsAboveLimit = balance > = - overdraft_limit
8
9 // Eventually, the system must bring the account back to a non-negative balance.
10 property eventuallyInBalance = eventually ( balance > = 0 )
11
12 fn recoverNow ( ) {
13 requires balance < 0
14 ensures balance = = 0
15
16 balance = 0 ;
17 }
18 }

Components

The invariant Declaration

An invariant specifies a condition that must hold before and after every function in an impl block.

Example
0 invariant =

Rules:

Invariants must be side-effect-free boolean expressions. They may reference any field defined in the corresponding struct . The compiler verifies that every function in the `impl` block preserves the invariant. If the compiler cannot prove an invariant holds across all paths, compilation fails.

Example
0 struct Counter { count : int }
1
2 impl Counter {
3 invariant nonNegative = count > = 0
4
5 fn increment ( ) {
6 ensures count = = old ( count ) + 1
7 count = count + 1 ;
8 }
9 }

In this example, the invariant guarantees that the count is never negative. The verifier ensures this remains true even after increment() executes.


The property Declaration

A property expresses a temporal guarantee — such as liveness or ordering — across the program’s behavior.

Example
0 property =

Temporal expressions may include:

`always`: The condition must hold at all times. eventually : The condition must hold at some point in the future. `until`, `release`: Conditions over time and ordering. forall , exists : Quantifiers over a domain (e.g., tasks, states).

Rules:

Properties do not affect control flow or behavior. They are used by the compiler to prove guarantees about possible program traces . * Property violations produce counterexample traces at compile time.

Example
0 struct Queue {
1 items : Array ( int ) ;
2 }
3
4 impl Queue {
5 invariant lengthNonNegative = std : : length ( items ) > = 0
6
7 // Ensure that any task awaiting the queue will eventually see data.
8 property eventuallyReceivesItem = forall task in Tasks {
9 eventually ( ! std : : is_empty ( items ) )
10 }
11
12 async fn dequeue ( ) - > int {
13 while std : : is_empty ( items ) {
14 await wait_for_item ( ) ;
15 }
16 return std : : pop ( items ) ;
17 }
18
19 fn enqueue ( value : int ) {
20 std : : push ( items , value ) ;
21 notify_waiters ( ) ;
22 }
23 }

Specification Behavior

Construct Scope Verified When Runtime Cost
invariant Per-impl block Before and after each fn / async fn None
property Per-impl block Over all valid execution traces None

`invariant` is used for safety ("nothing bad happens"). property is used for liveness ("something good eventually happens").

The compiler treats both as compile-time-only declarations that participate in verification, not execution.


Old State Reference: old(expr)

The old keyword allows a function’s ensures clause to reference the value of an expression before the function was called.

Example
0 fn increment ( ) {
1 ensures count = = old ( count ) + 1
2 count = count + 1 ;
3 }

The compiler ensures that the post-state ( count ) relates correctly to the pre-state ( old(count) ).


Quantifiers and Temporal Blocks

Quantifiers can be used in properties to express conditions across many elements or tasks.

Example
0 forall in {
1
2 }

Compiler Interactions

- Interacting with the compiler
0 ⚠️ [ formal : invariant ] Detected 1 invariant violation during compilation .
1
2 Invariant :
3 balanceIsAboveLimit = balance > = - overdraft_limit
4
5 Violated in :
6 impl BankAccount → fn emergencyReset ( )
7
8 ╭────┬───────────────────────────────────────────────
9 ╭─│ 12 │
10 │ │ 13 │ balance = - 999 ;
11 │ │ │ ^ ^ ^ ^ ^ violates invariant after execution
12 │ │ 14 │
13 │ ╰────┴───────────────────────────────────────────────
14
15 │ 💡 The compiler attempted to prove that `balance >= -overdraft_limit`
16 │ holds before and after each function in `impl BankAccount` .
17
18 │ But after calling `emergencyReset()` , balance = - 999 ,
19 │ and overdraft_limit = 0 ⇒ invariant fails : - 999 > = 0 ❌
20
21 ╰──────────────────────────────────────────────────────────
22
23 Actions :
24
25 1 . 🔍 Explain why this invariant is failing
26 2 . ✏️ Show me the relevant values and trace
27 3 . 🧪 Temporarily disable this invariant ( not recommended )
28 4 . 🧼 Open `emergencyReset` to fix it
29 5 . ❌ Remove the invariant entirely
30 6 . ❓ Ask any arbitrary question ( chat )
31
32 > I guess I don't really understand invariants
33
34 No problem , let's take a look at how they work .
35 The first thing we . . .

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 that p is borrowed, with kind ∈ {shared, unique} .
  • Program Point q : A location in the control-flow graph (CFG).
  • Alive(L, q) : Predicate meaning loan L is still valid at point q (lexically-free semantics).
  • Operations :
  • reads(p, q) : a read of place p at q .
  • writes(p, q) : a write to place p at q .
  • moves(p, q) : a move from place p at q .
  • Aliases(x, p, q) : x holds a reference to p at q (logical predicate, tracked via loan origins).
  • Escape(r, q) : Reference r escapes its region at q (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]) -> &i32 elaborates to for<ρ> 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 marker A .

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: self implicitly labeled Self , but can also be explicitly labeled: self: &Self as S .
  • Diagnostics: Role-based: “return value tied to param a (label A) but a does not live long enough.” Quick fixes: “Add as A to param.”
&Self T { return &self.0; } fn other(self: &Self as S, other: &Self as O) -> &O T { return &other.0; } } fn demo_methods() { let p1: Pair(int) = (1, 2); let p2: Pair(int) = (3, 4); let r1 = p1.first(); let r2 = p1.other(&p2); print(r1); print(r2); } // 4) Higher-order function: label flows through function type fn map_head(xs: &Vec(T) as A, f: Fn(&A T) -> U) -> &A U { return f(&xs[0]); } fn demo_hof() { let nums: Vec(int) = [1, 2, 3]; let head_ref = map_head(&nums, fn(x: &int) -> &int { return x; }); print(head_ref); } // 5) Multiple inputs require label: `as A` disambiguates the return fn pick(a: &T as A, b: &T) -> &A T { // body could choose either; label tells the checker the result is tied to `a` return a; } // 6) Optional: Outlives relation (advanced) fn choose_longer(a: &T as X, b: &T as Y) -> &X T where Y : X { return a; } // 7) Struct constructor with label reuse struct Window(T) { head: &A T, tail: &B T } fn mk_window(xs: &Vec(T) as A, ys: &Vec(T) as B) -> Window(T) { return Window { head: &xs[0], tail: &ys[0] }; } fn demo_window() { let xs: Vec(int) = [1,2,3]; let ys: Vec(int) = [4,5,6]; let w = mk_window(&xs, &ys); print(w.head); print(w.tail); } // 8) Diagnostics-friendly failure (commented): // fn bad_pick(a: &T, b: &T) -> &T { // if cond { return a; } else { return b; } // // Error: ambiguous return lifetime. Add `as A` to a (or b) and return `&A T`. // }" lang="aela" title="Example" id="bd62fb2cf27e9">
Example
0 // 1) Disambiguate which input a returned reference is tied to
1 fn first_of_two ( a : & T as A , b : & T ) - > & A T {
2 return a ;
3 }
4
5 fn demo_first_of_two ( ) {
6 let x : int = 10 ;
7 let y : int = 20 ;
8 let rx = & x ;
9 let ry = & y ;
10
11 let r = first_of_two ( rx , ry ) ;
12 print ( r ) ;
13 }
14
15 // 2) Struct field referencing an argument via label
16 struct View ( T ) { data : & A T }
17
18 fn make_view ( x : & T as A ) - > View ( T ) {
19 return View { data : x } ;
20 }
21
22 fn demo_view ( ) {
23 let s : string = "hello" ;
24 let v = make_view ( & s ) ;
25 print ( v . data ) ;
26 }
27
28 // 3) Methods: implicit Self label + explicit labels to disambiguate
29 impl Pair ( T ) {
30 fn first ( self : & Self ) - > & Self T {
31 return & self . 0 ;
32 }
33
34 fn other ( self : & Self as S , other : & Self as O ) - > & O T {
35 return & other . 0 ;
36 }
37 }
38
39 fn demo_methods ( ) {
40 let p1 : Pair ( int ) = ( 1 , 2 ) ;
41 let p2 : Pair ( int ) = ( 3 , 4 ) ;
42
43 let r1 = p1 . first ( ) ;
44 let r2 = p1 . other ( & p2 ) ;
45 print ( r1 ) ;
46 print ( r2 ) ;
47 }
48
49 // 4) Higher-order function: label flows through function type
50 fn map_head ( xs : & Vec ( T ) as A , f : Fn ( & A T ) - > U ) - > & A U {
51 return f ( & xs [ 0 ] ) ;
52 }
53
54 fn demo_hof ( ) {
55 let nums : Vec ( int ) = [ 1 , 2 , 3 ] ;
56 let head_ref = map_head ( & nums , fn ( x : & int ) - > & int { return x ; } ) ;
57 print ( head_ref ) ;
58 }
59
60 // 5) Multiple inputs require label: `as A` disambiguates the return
61 fn pick ( a : & T as A , b : & T ) - > & A T {
62 // body could choose either; label tells the checker the result is tied to `a`
63 return a ;
64 }
65
66 // 6) Optional: Outlives relation (advanced)
67 fn choose_longer ( a : & T as X , b : & T as Y ) - > & X T where Y : X {
68 return a ;
69 }
70
71 // 7) Struct constructor with label reuse
72 struct Window ( T ) { head : & A T , tail : & B T }
73
74 fn mk_window ( xs : & Vec ( T ) as A , ys : & Vec ( T ) as B ) - > Window ( T ) {
75 return Window { head : & xs [ 0 ] , tail : & ys [ 0 ] } ;
76 }
77
78 fn demo_window ( ) {
79 let xs : Vec ( int ) = [ 1 , 2 , 3 ] ;
80 let ys : Vec ( int ) = [ 4 , 5 , 6 ] ;
81 let w = mk_window ( & xs , & ys ) ;
82 print ( w . head ) ;
83 print ( w . tail ) ;
84 }
85
86 // 8) Diagnostics-friendly failure (commented):
87 // fn bad_pick(a: &T, b: &T) -> &T {
88 // if cond { return a; } else { return b; }
89 // // Error: ambiguous return lifetime. Add `as A` to a (or b) and return `&A T`.
90 // }

Lifetimes in Data Structures

  • Reference fields implicitly carry region parameters. struct Node { data: &T } elaborates to struct Node<ρ, T> { data: &ρ T } .
  • Construction instantiates ρ from the argument’s region. Error if struct outlives the referenced data.

Place Overlap (I3)

  • p overlaps p .
  • p overlaps p.f . Distinct fields disjoint.
  • arr[i] vs arr[j] : disjoint if indices are distinct compile-time constants; else conservative overlap.
  • Library intrinsics like split_at_mut provide 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 await unless the origin outlives the entire future. Practically: locals cannot cross await ; only borrows from captured fields of the async task can.
  • Phase 2: Desugar async to 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

Example
0 let s : string = "hi" ;
1 let r = & s ; // borrow starts
2 print ( r ) ; // last use of r
3 var t = s ; // OK allowed: r’s region ended at last use (lexically-free)

L1 — Loan Creation

Example
0 var x : int = 0 ;
1 let r = & x ; // L(x, shared)
2 let m = & mut x ; // Not OK conflict later under A1, but creation itself establishes L(x, unique)

L2 — Loan Propagation

Example
0 fn id_ref ( p : & int ) - > & int { p }
1 let x : int = 1 ;
2 let r1 = & x ; // L(x, shared)
3 let r2 = id_ref ( r1 ) ; // loan propagates to r2 until last use
4 print ( r2 ) ; // OK!

A1 — Unique Exclusivity

Example
0 var v : int = 0 ;
1 let m = & mut v ; // L(v, unique)
2 let r = & v ; // Not OK! A1: unique excludes any concurrent shared borrow

A2 — Shared Read‑Only

Example
0 var n : int = 0 ;
1 let a = & n ; // L(n, shared)
2 let b = & n ; // another shared loan
3 print ( * a + * b ) ; // OK! reads allowed
4 n = 5 ; // Not OK! A2: write while shared loans alive

I1 — Write Invalidates

Example
0 var s : string = "hi" ;
1 let r = & s ; // L(s, shared)
2 print ( r ) ;
3 s . push ( " ! " ) ; // Not OK! I1: write to s while shared loan alive (if r not yet last‑used)

I2 — Move Invalidates

Example
0 var s : string = "a" ;
1 let r = & s ; // L(s, shared)
2 var t = s ; // Not OK! I2: move invalidates loans; r would dangle

I3 — Overlap/Fields

Example
0 struct P { x : int , y : int }
1 var p : P = P { x : 1 , y : 2 } ;
2 let mx = & mut p . x ; // L(p.x, unique)
3 let my = & mut p . y ; // OK, disjoint fields: allowed
4
5 var arr : [ int ; 3 ] = [ 0 , 1 , 2 ] ;
6 let a = & mut arr [ 0 ] ;
7 let b = & mut arr [ 1 ] ; // not OK, if indices distinct constants
8 let i = 0 ; let j = read_index ( ) ;
9 let c = & mut arr [ i ] ;
10 let d = & mut arr [ j ] ; // Not OK conservatively, may overlap unless proven disjoint (use split helpers)

U1 — Use Requires Alive

Example
0 let s : string = "hi" ;
1 let r = & s ; // L(s, shared)
2 print ( r ) ; // OK! use while alive
3 // after last use r ends; any further use would be not OK (dead borrow)

U2 — No After‑Free

Example
0 fn bad ( ) {
1 let s : string = "x" ;
2 let r = & s ;
3 drop ( s ) ; // base place dead
4 print ( r ) ; // Not OK: U2: use after base death
5 }

RB1 — Shared from Unique

Example
0 var v : int = 0 ;
1 let mu = & mut v ; // L(v, unique)
2
3 // Shared reborrow of the same place (auto-deref from &mut int to int)
4 let rs : & int = & mu ; // creates L(v, shared)
5
6 print ( rs ) ;
7
8 // Write through the unique borrow (auto-deref on assignment)
9 mu = 3 ; // must end shared loan before writing via `mu`

RB2 — Unique from Unique (optional v1)

Example
0 var v : int = 0 ;
1 let mu1 = & mut v ;
2 let mu2 = & mut mu1 ; // unique reborrow of v; allowed only if no overlapping use via mu1

E1 — No Escaping Borrows

Example
0 fn leak_ref ( ) - > & int {
1 let x : int = 1 ;
2 return & x ; // Not OK! Escapes to caller; Not OK, dies at function end
3 }
4
5 fn head ( s : & string ) - > & char {
6 return & s [ 0 ] ;
7 } // OK! Summary ties return to arg

M1 — Binding Mutability

Example
0 let x : int = 0 ;
1 let rx = & mut x ; // Not OK: M1: &mut requires mutable base
2 var y : int = 0 ;
3 let ry = & mut y ; // OK

M2 — No Shared Mutability Without Atomics

Example
0 shared var n : int = 0 ; // shared location
1 let r1 = & n ; let r2 = & n ; // shared borrows
2 n = n + 1 ; // Not OK concurrent write without atomic discipline
3 atomic var a : int = 0 ; // with atomic, writes are allowed by policy

T1 — Temporaries

Example
0 print ( & ( make_string ( ) ) ) ; // OK temporary lives through call; reference dies at last use
1 let r = & ( make_string ( ) ) ;
2 print ( r ) ; // Not OK if r would outlive the temporary’s last use

T2 — Branches/Loops/Match

Example
0 let s : string = "x" ;
1 let r = & s ;
2 if cond { print ( r ) ; } else { print ( r ) ; }
3 // r is used on both paths; live set at join = intersection ⇒ still alive until after the if
4 var t = s ; // not OK must occur after r’s last use on all paths

F1 — FFI Preconditions

Example
0 ffi puts : ( & char ) - > int = . . . ;
1 let s : string = "hi" ;
2 let r = & s [ 0 ] ;
3 puts ( r ) ; // OK if FFI summary: does not retain; Not OK if it may retain (escape)

F2 — FFI Postconditions

Example
0 ffi c_strchr : ( & char , int ) - > & char = . . . ; // trusted: result aliases input
1
2 fn first_a ( s : & string ) - > & char {
3 c_strchr ( & s [ 0 ] , 'a' ) // OK allowed only because summary ties result to arg region
4 }

RFT1 — Refinement Well‑Formedness

Example
0 let x : int = 5 ;
1 let y : { z : & int where initialized ( z ) } = { z : & x } ; // OK predicate references lifetime‑relevant property

RFT2 — Discharge at Use Sites

Example
0 fn show ( p : { z : & int where not_escaped ( z ) } ) - > void {
1 print ( p . z ) ; // OK! checker discharges `not_escaped` from current loan facts
2 }

inter‑Procedural Summary (Elision)

Example
0 fn get_first ( a : & [ int ] ) - > & int {
1 return & a [ 0 ] ;
2 }
3
4 // elaborated internally: for<ρ> fn(&ρ [int]) -> &ρ int
5 // caller instantiates ρ from its argument; return tied to same ρ

Structs with Reference Fields (Implicit Regions)

Example
0 struct Node ( T ) { data : & T } // elaborates to Node<ρ, T>
1
2 fn demo ( ) - > void {
3 let x : int = 1 ;
4 var n : Node ( int ) = Node { data : & x } ; // OK n: Node<ρx, int>
5 }
6
7 fn bad ( ) - > void {
8 var n : Node ( int ) ;
9
10 {
11 let x : int = 1 ;
12 n = Node { data : & x } ; // Not OK n outlives x ⇒ region check fails
13 }
14 }

Async Phase 1 — No Unique Loans Across Await

Example
0 async fn step ( mut v : & int ) - > void {
1 let m = v ; // borrow unique via parameter
2 await tick ( ) ; // Not OK, unique loan across await in phase 1
3 }

Closures — Capture Classification

void { move x }; // by‑value move ⇒ FnOnce" lang="aela" title="Example" id="de7b5ce457c5">
Example
0 var n : int = 0 ;
1 let c1 = fn ( ) - > void { print ( & n ) ; } ; // shared borrow capture ⇒ Fn
2 let c2 = fn ( ) - > void { n = n + 1 ; } ; // unique borrow capture ⇒ FnMut
3 let x : string = "hi" ;
4 let c3 = fn ( ) - > void { move x } ; // by‑value move ⇒ FnOnce

C/C++ Harmony

Most languages' safty stops at the FFI boundary. But by automating the creation of safe FFI boundaries and embedding more of the C/C++ code's contracts directly into Aela's type system. Instead of just marking a boundary as unsafe and leaving the safety burden entirely on the developer, Aela can actively assist in verifying the C/C++ side of the interaction.

Automatically Generate "Diplomatic" Wrappers

Instead of manually writing write unsafe blocks and wrapper functions (This is tedious and error-prone). Aela can automate this.

Aela Compile can parse C/C++ header files (.h, .hpp) and automatically generatee the FFI bindings and a safe, idiomatic Aela wrapper. It's much more sophisticated than a simple binding generator.

Contract Inference

The tool can analyze C++ code for clues about contracts.

For instance, it can interpret a gsl::not_null or _Nonnull annotation as a non-nullable reference in Aela, automatically adding the necessary runtime checks.

Resource Management

If a C function create_foo() returns a pointer that must be freed with destroy_foo() , the generator can automatically create a smart pointer or RAII object in Aela that calls destroy_foo() on scope exit. This eliminates a huge class of resource leak bugs.

Error Handling

It can translate C-style error codes e.g., return -1; or errno = EINVAL; into Aela's native error-handling mechanism, like Result types or exceptions.

This moves the burden from the developer having to manually ensure safety to the too providing a verifiably safe starting point.

A Type System for C/C++ Interop

Aela's type system can understand C/C++'s quirks better. It encodes invariants about C pointers and memory directly into the types.

Sized Pointers

Instead of a raw pointer, Aela has types like Pointer(T, size_t N), which represent a pointer to a buffer of N elements. This allows the compiler to enforce bounds checking at the FFI boundary.

Nullability and Ownership

Explicitly differentiate between Pointer(T) (nullable) and Reference(T) (non-nullable). And types that encode ownership semantics like OwnedPointer(T) (must be freed) vs. BorrowedPointer(T) (must not be freed).

Tainted Data

Data coming from C/C++ is considered "tainted" by the type system. It needs to be explicitly validated (e.g., checking a string for valid UTF-8, ensuring a value is within an expected range) before it can be used in the safe context.

Integrate Static and Dynamic Analysis

Since Aela is written in C, it can integrate powerful C/C++ analysis tools directly into the build process.

Clang's Analyzers

Aela uses libraries from the Clang/LLVM project to perform static analysis on the C/C++ code. During compilation, it automatically invokes analyzers to check for things like null pointer dereferences, use-after-free, or buffer overflows in the C code being called, and flag a warning or error if a potential issue is found.

Boundary Sanitization

A "debug mode" for FFI that injects runtime checks at the boundary.

When your code calls a C function, it could automatically add canaries or check buffer boundaries. When C code calls back into Aela, it can validate incoming pointers and data. This is similar to running with AddressSanitizer (ASan), but it's focused specifically on the FFI-boundary risks.

By taking these steps, Aela doesn't just stop its safety guarantees at the boundary. It actively polices that boundary, making brownfield integration significantly safer and more robust than the manual, high-discipline approach required by other languages.

FFI

The Foreign Function Interface (FFI) provides a mechanism for Aela code to call functions written in other programming languages, specifically C. This allows you to leverage existing C libraries, write performance-critical code in a lower-level language, or interact directly with the underlying operating system.

The core of Aela's FFI is the ffi definition, which declares a external C functions and their Aela type signatures or varibales and their types. The Aela compiler and runtime use these declarations to handle the "marshalling" of data—the process of converting data between Aela's internal representations and the C Application Binary Interface (ABI).

Declaring an FFI type

You declare a C function or C variable using the ffi keyword.

Example
0 ffi foo = fn ( string ) - > void ;
1 ffi bar = u32 ;

ABI Contract

A stable C ABI ( ae_c_abi.h ) defines the contract. It specifies the C-side string representation: typedef struct { char* ptr; int64_t len; } AeString;

Compiler Type Mapping

The Aela compiler's types.c maps the language's string type to an LLVM struct with an identical memory layout: %aela.string = type { i8*, i64 } .

Passing Convention

Strings are passed to C functions BY VALUE.

  • Aela code generates: call void @c_function(%aela.string %my_string) .
  • C code receives: void c_function(AeString my_string) .

Safety & Ownership

  • This pass-by-value convention is a "defensive" design.
  • The C function gets a copy of the string descriptor, preventing it
  • from modifying the original string's length or pointer in Aela's
  • memory.
  • Aela's runtime retains ownership of the underlying character buffer
  • ( char* ). The AeString struct is just a temporary, non-owning view.
Example
0 ffi = ; . . .

: The exact name of the function as it is defined in the C source code.

: The Aela type signature for the C function. This signature is the crucial contract that tells the Aela compiler how to call the C function correctly.

Example

Let's look at the stdio example from the standard library:

Example
0 ffi ae_stdout_write = fn ( string ) - > void ;

This code does the following:

It declares that there is an external C function named ae_stdout_write.

It specifies that from the Aela side, this function should be treated as one that accepts a single Aela string and returns void.

To call this function, you use the standard module access syntax:

Example
0 ae_stdout_write ( "Hello from C ! " ) ;

The Aela-C ABI and Data Marshalling When an FFI call occurs, the Aela compiler generates "glue" code to translate Aela types into types that C understands. This mapping follows a specific Application Binary Interface (ABI).

Primitive Types

Most Aela primitive types map directly to their C equivalents.

Aela Type C Type
i8, u8 int8_t, uint8_t
i16, u16 int16_t, uint16_t
i32, u32 int32_t, uint32_t
i64, u64 int64_t, uint64_t
f32 float
f64 double
bool bool (or \_Bool)
char uint32_t (UTF-32)
void void

Strings

The Aela string is a "fat pointer" struct containing a pointer to the data and a length. C, however, typically works with null-terminated char* strings.

Aela to C: When you pass an Aela string to an FFI function, the compiler automatically extracts the internal ptr and passes it as a const char* to the C function. The string data is guaranteed to be null-terminated, so standard C string functions can operate on it safely.

Aela's Internal runtime representation
0 struct string {
1 ptr : ptr , // Pointer to UTF-8 data
2 len : i64 // Length of the string
3 }

FFI Call:

The Aela Code
0 ae_stdout_write ( "Hello" ) ;

The C function receives a standard C string.

The C Implementation
0 void ae_stdout_write ( const char * message ) {
1 printf ( " % s" , message ) ;
2 }

Structs, Arrays, and Closures (Complex Types)

Complex aggregate types like structs, arrays, and closures cannot be passed directly by value to C functions. The ABI for these types is simple: you pass a pointer.

Aela to C: When passing a complex type, Aela passes a pointer to the object's memory layout. Your C code receives an opaque pointer (void\*) to this data. It is your responsibility in C to know the memory layout of the Aela type and cast the pointer accordingly to access its fields.

This is an advanced use case and requires careful handling to avoid memory corruption. You must ensure that the struct definition in your C code exactly matches the memory layout of the Aela struct.

Often you end up with an opaque strct in Aela. These can not have methods or properties.

An Opaque Struct
0 struct StringBuilder ;
1 ``
2
3 ## Variadic Functions (...args)
4
5 Variadic arguments are not directly passed through the FFI boundary . The . . . args
6 feature is part of the Aela language and its calling convention , not the C ABI .
7
8 As seen in the io . print example , you must handle variadic arguments within your
9 Aela code and call the FFI function with a concrete , non - variadic signature .
10
11 `` `example
12 // The public-facing Aela function is variadic.
13
14 export fn print ( formatString : string , . . . args ) - > void {
15 stdio : : ae_stdout_write ( std : : format ( formatString , . . . args ) ) ;
16 }

This design provides a safe and clear boundary. The complex, type-safe variadic handling happens within the Aela runtime, while the FFI call itself remains a simple, direct translation of the string argument to a char*.

Linking C Code To make your C functions available to the Aela compiler, you must compile them into an object file (.o) or a library (.a, .so, .dylib) and include it during the final linking step.

The Aela driver will eventually provide flags to specify these external object files. For now, you would typically use a command like clang to link the Aela-generated object file with your C object file.

  1. Compile your Aela code aec your_program.ae -o your_program.o
  1. Compile your C code clang -c my_ffi_functions.c -o my_ffi_functions.o
  1. Link them together clang your_program.o my_ffi_functions.o -o
  2. final_executable

This process creates the final executable where the Aela runtime can find and call your C functions.

Memory & Mutability Model

1. Default Allocation Semantics

  • All values start on the stack by default.
  • Stack allocations are:
  • Fast
  • Scoped
  • Owned directly by the binding.
Example
0 let foo : Foo = { num : 0 } ; // Foo is stack-allocated

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.

Example
0 let x : & T = new { . . . val } ; // Allocate on the heap
1 let x : & T = new shared { . . . val } ; // Single-threaded, mutable
2 let x : & T = new shared atomic { . . . val } ; // Thread-safe atomic
3 var x : & T = new weak { . . . val } ; // Mutable weak reference

Behavior

  • 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)
  • There is no `mut` keyword
  • Mutability is not encoded in types, structs, or fields

Example

Example
0 let a : & Foo = new shared { . . . foo } ; // Immutable heap reference
1 var b : & Foo = new shared { . . . foo } ; // Mutable heap reference

Attempting to mutate a let -bound reference results in a compile-time error .

4. Weak References & Cycle Prevention [NOT IMPLEMENTED]

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'

Example
0 if let strong_ref = weak_ref {
1 // This block only runs if the object is still alive.
2 // 'strong_ref' is a new, temporary strong reference of type &Foo.
3 strong_ref . do_something ( ) ;
4 }

5. Copying Stack Values

  • Heap allocation requires copying the stack value:
Example
0 new shared { . . . foo } // `{ ...foo }` performs a field-wise copy
  • 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.
a copy of the number 10 is passed. // bar(stack_str); -> a copy of the {ptr, i64} struct is passed." lang="example" title="- Stack Allocated" id="d4bdebbe552a">
- Stack Allocated
0 let stack_int : i32 = 10 ; // The variable 'stack_int' IS the number 10.
1 let stack_str : string = "hello" ; // The variable 'stack_str' IS the {ptr, i64} struct.
2
3 // When calling a function:
4 // foo(stack_int); -> a copy of the number 10 is passed.
5 // bar(stack_str); -> a copy of the {ptr, i64} struct is passed.

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.
- Heap allocated via new
0 let heap_int : i32 = new 42 ; // 'heap_int' is a POINTER to a box containing 42.
1 let heap_obj : MyStruct = new { } ; // 'heap_obj' is a POINTER to a box containing a MyStruct.
2 let heap_arr : u8 [ ] = new [ 1 , 2 , 3 ] ; // 'heap_arr' is a POINTER to a box for the array data.
3
4 // When calling a function:
5 // foo(heap_int); -> a copy of the POINTER is passed. The heap data is not touched.

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. The env_ptr points 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.
- Special Case
0 let captured_var = new "text" ;
1
2 let my_closure = fn ( ) {
3 print ( captured_var ) ; // Captures the pointer 'captured_var'.
4 } ;
5
6 // 'my_closure' is a {func_ptr, env_ptr} struct.
7 // 'env_ptr' points to a heap box which contains a copy of the 'captured_var' pointer.

GUI

Aela provides a cross platform GUI library that works on MacOS, iOS, Windows, Linux, & Android.

Install

Install the module into your dependencies.
0 aec install stablestate / ui
Your index.json will then have the dependency.
0 {
1 "name" : "my - program" ,
2 "version" : "0 . 1 . 0" ,
3 "entry" : "main . ae" ,
4 "output" : "build / app" ,
5 "include" : [ "src /**/ * . ae" ] ,
6 "exclude" : [ "tests / * * " ] ,
7 "dependencies" : [
8 {
9 "name" : "ui" ,
10 "url" : "https : / / github . com / stablestate / ui"
11 }
12 ]
13 }

Example

int { fn App () -> &View { let onclick: EventCallback = fn (event: &Event) -> void { // do something }; return new View { children: [ WebView { url: "https://news.ycombinator.com" }, Button { label: "save" }, Button { label: "cancel", onclick } ], onclick }; } let app: &View = App(); let loop: &io::AeEventLoop = io::getLoop(); initLoop(loop); let windowOptions: &WindowOptions = new { title: "Aela UI Test", width: 800.0, height: 600.0 }; let window: &Window = new Window(windowOptions); window.show(); window.render(app); runLoop(); return 0; }" lang="ae" title="Getting started" id="5286b485edc61">
Getting started
0 import {
1 initLoop ,
2 runLoop ,
3 WindowOptions ,
4 Window
5 } from "ui" ;
6
7 async fn main ( args : string [ ] ) - > int {
8 fn App ( ) - > & View {
9 let onclick : EventCallback = fn ( event : & Event ) - > void {
10 // do something
11 } ;
12
13 return new View {
14 children : [
15 WebView { url : "https : / / news . ycombinator . com" } ,
16 Button { label : "save" } ,
17 Button { label : "cancel" , onclick }
18 ] ,
19 onclick
20 } ;
21 }
22
23 let app : & View = App ( ) ;
24
25 let loop : & io : : AeEventLoop = io : : getLoop ( ) ;
26 initLoop ( loop ) ;
27
28 let windowOptions : & WindowOptions = new {
29 title : "Aela UI Test" ,
30 width : 800 . 0 ,
31 height : 600 . 0
32 } ;
33
34 let window : & Window = new Window ( windowOptions ) ;
35
36 window . show ( ) ;
37 window . render ( app ) ;
38
39 runLoop ( ) ;
40
41 return 0 ;
42 }

Why 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.

Formal Grammar Spec

Grammar
0 ( * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = )
1 ( Aela Language Grammar 0 . 0 . 6 )
2 ( Finalized : 2025 - 09 - 06 )
3 ( = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = * )
4
5 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
6 ( PROGRAM )
7 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
8
9 Program : : = { TopLevelDeclaration } EOF
10
11 TopLevelDeclaration : : =
12 ImportStatement
13 | ReExportDeclaration
14 | [ KW_EXPORT ] (
15 FfiDeclaration
16 | VarDeclaration
17 | FunctionDeclaration
18 | StructDeclaration
19 | ImplBlock
20 | EnumDeclaration
21 | TypeAliasDeclaration
22 | SystemDeclaration
23 )
24
25 TypeAliasDeclaration : : = KW_TYPE IDENTIFIER '=' Type ';'
26
27 ReExportDeclaration : : =
28 KW_EXPORT ( NamedImport | IDENTIFIER )
29 KW_FROM STRING_LITERAL ';'
30
31 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
32 ( IMPORTS )
33 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
34
35 ImportStatement : : =
36 KW_IMPORT ( NamedImport | IDENTIFIER )
37 KW_FROM STRING_LITERAL ';'
38
39 NamedImport : : = '{' [ ImportSpecifier { ',' ImportSpecifier } [ ',' ] ] '}'
40 ImportSpecifier : : = IDENTIFIER [ ':' IDENTIFIER ]
41
42 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
43 ( FFI ( Foreign Function Interface ) )
44 ( - Contracts are compile - time enforced to be UB - free )
45 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
46
47 FfiDeclaration : : = KW_FFI IDENTIFIER '=' Type ';'
48
49 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
50 ( DECLARATIONS )
51 ( - var is mutable , let is immutable )
52 ( - aliases are borrow - checked by the analyzer )
53 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
54
55 VarDeclaration : : = ( KW_VAR | KW_LET ) IDENTIFIER ':' Type
56 [ '=' Expression ] ';'
57
58 StructDeclaration : : = KW_STRUCT IDENTIFIER [ GenericParameters ] ( '{' { StructFieldDeclaration } '}' | ';' )
59
60 StructFieldDeclaration : : =
61 ( IDENTIFIER ':' Type ';' )
62 | ( '...' IDENTIFIER ';' )
63
64 FnModifiers : : =
65 [ ( KW_THREAD [ KW_PURE ] ) // thread; pure+thread allowed
66 | ( KW_PURE [ KW_THREAD ] ) // pure; optional thread
67 | KW_ASYNC // async alone (no pure)
68 ]
69
70 ImplBlock : : = KW_IMPL Type [ GenericParameters ] '{' { FunctionDeclaration | InvariantDeclaration } '}'
71
72 FunctionDeclaration : : =
73 FnModifiers KW_FN IDENTIFIER [ GenericParameters ]
74 '(' [ FunctionParameters ] ')' '->' Type ( Block | ';' )
75
76 EnumDeclaration : : = KW_ENUM IDENTIFIER [ GenericParameters ]
77 '{' [ EnumVariant { ',' EnumVariant } [ ',' ] ] '}'
78
79 EnumVariant : : = IDENTIFIER [ '(' Type { ',' Type } ')' ]
80
81 GenericParameters : : = '(' IDENTIFIER { ',' IDENTIFIER } ')'
82
83 SystemDeclaration : : = KW_SYSTEM IDENTIFIER '{'
84 { ActionDeclaration
85 | InvariantDeclaration
86 | PropertyDeclaration
87 }
88 '}'
89
90 ActionDeclaration : : = KW_ACTION IDENTIFIER
91 '(' [ FunctionParameters ] ')'
92 [ RequiresClause ]
93 [ EnsuresClause ]
94 Block
95
96 RequiresClause : : = KW_REQUIRES Expression
97 EnsuresClause : : = KW_ENSURES Expression
98
99 InvariantDeclaration : : = KW_INVARIANT IDENTIFIER ':' Expression
100 PropertyDeclaration : : = KW_PROPERTY IDENTIFIER ':' TemporalExpression
101
102 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
103 ( TYPES )
104 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
105
106 Type : : = [ '&' ] PostfixType
107
108 PostfixType : : = SimpleType { ArrayTypeModifier | TypeApplication | '?' }
109
110 MapType : : = KW_MAP '(' Type ',' Type ')'
111
112 SimpleType : : = PrimitiveType
113 | KW_VOID
114 | FunctionTypeSignature
115 | PathExpression
116 | MapType
117 | RefinementType
118 | '(' Type ')'
119
120 FunctionTypeSignature : : =
121 FnModifiers KW_FN '(' [ FunctionTypeParameters ] ')' '->' Type
122
123 RefinementType : : = '{' IDENTIFIER ':' Type KW_WHERE Expression '}'
124
125 PrimitiveType : : = KW_U8 | KW_I8 | KW_U16 | KW_I16 | KW_U32 | KW_I32
126 | KW_U64 | KW_I64 | KW_F32 | KW_F64 | KW_BOOL
127 | KW_CHAR | KW_STRING | KW_INT
128
129 TypeApplication : : = '(' [ Type { ',' Type } ] ')'
130
131 FunctionParameters : : = Parameter { ',' Parameter }
132 Parameter : : = [ '...' ] [ KW_MUT ] IDENTIFIER ':' Type
133 FunctionTypeParameters : : = FunctionTypeParameter { ',' FunctionTypeParameter }
134 FunctionTypeParameter : : = [ KW_MUT ] Type
135 ArrayTypeModifier : : = '[' [ Expression ] ']'
136
137 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
138 ( COMMENTS )
139 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
140
141 ( * A single - line comment starts with // and continues to the end of the line )
142 SingleLineComment : : = '//' { ~('\n' | '\r') }
143
144 ( * A multi - line comment starts with /* and ends with */ )
145 MultiLineComment : : = '/*' { . } '*/'
146
147 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
148 ( STATEMENTS ( Unambiguous ) )
149 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
150
151 Statement : : = MatchedStatement | UnmatchedStatement
152
153 MatchedStatement : : =
154 KW_IF '(' Expression ')' MatchedStatement KW_ELSE MatchedStatement
155 | KW_IF KW_LET Pattern '=' Expression Block KW_ELSE MatchedStatement
156 | Block
157 | KW_RETURN [ Expression ] ';'
158 | BreakStatement
159 | ContinueStatement
160 | WhileStatement
161 | ForStatement
162 | MatchStatement
163 | ExpressionStatement
164 | VarDeclaration
165 | FunctionDeclaration
166 | ';'
167
168 UnmatchedStatement : : =
169 KW_IF '(' Expression ')' Statement
170 | KW_IF '(' Expression ')' MatchedStatement KW_ELSE UnmatchedStatement
171 | KW_IF KW_LET Pattern '=' Expression Block
172 | KW_IF KW_LET Pattern '=' Expression Block KW_ELSE UnmatchedStatement
173
174 Block : : = '{' { Statement } '}'
175 ExpressionStatement : : = Expression ';'
176 BreakStatement : : = KW_BREAK ';'
177 ContinueStatement : : = KW_CONTINUE ';'
178
179 WhileStatement : : = KW_WHILE '(' Expression ')' Statement
180
181 ForStatement : : = KW_FOR '(' ForDeclaratorList KW_IN Expression ')' Statement
182 ForDeclaratorList : : = ForDeclarator { ',' ForDeclarator }
183 ForDeclarator : : = ( KW_LET | KW_VAR ) IDENTIFIER ':' Type
184
185 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
186 ( MATCH ( Mandatory Exhaustive ) )
187 ( - Expression form for atomic initialization . )
188 ( - Statement form for control flow . )
189 ( - Guards , @ - bindings , and nesting are disallowed . )
190 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
191
192 MatchStatement : : = KW_MATCH '(' Expression ')' '{'
193 [ MatchStmtArm { ',' MatchStmtArm } [ ',' ] ]
194 '}'
195
196 MatchStmtArm : : = MatchArmPattern '=>' ( Block | ';' )
197
198 MatchArmPattern : : = Pattern { '|' Pattern }
199
200 Pattern : : =
201 LiteralPattern
202 | IDENTIFIER // binding
203 | '_' // wildcard
204 | PathExpression [ '(' [ PatternList ] ')' ] // unit/tuple-variant
205 | TuplePattern
206 | StructPattern
207
208 TuplePattern : : = '(' [ PatternList [ ',' ] ] ')'
209
210 StructPattern : : = '{' [ StructFieldPat { ',' StructFieldPat } [ ',' ] ] '}'
211 StructFieldPat : : = IDENTIFIER ( ':' Pattern ) ? | '...' IDENTIFIER
212
213 PatternList : : = Pattern { ',' Pattern }
214
215 RangePattern : : =
216 INT_LITERAL ( '..' | '..=' ) INT_LITERAL
217 | CHAR_LITERAL ( '..' | '..=' ) CHAR_LITERAL
218
219 LiteralPattern : : = INT_LITERAL | STRING_LITERAL | CHAR_LITERAL | KW_TRUE | KW_FALSE | RangePattern
220
221 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
222 ( EXPRESSIONS ( Pratt Parser Aligned ) )
223 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
224
225 Expression : : = AssignmentExpression
226
227 AssignmentExpression : : = CoalescingExpression [ '=' AssignmentExpression ]
228
229 CoalescingExpression : : = LogicalOrExpression { '??' LogicalOrExpression }
230
231 LogicalOrExpression : : = LogicalAndExpression { '||' LogicalAndExpression }
232
233 LogicalAndExpression : : = BitwiseOrExpression { '&&' BitwiseOrExpression }
234
235 BitwiseOrExpression : : = BitwiseXorExpression { '|' BitwiseXorExpression }
236
237 BitwiseXorExpression : : = BitwiseAndExpression { '^' BitwiseAndExpression }
238
239 BitwiseAndExpression : : = ShiftExpression { '&' ShiftExpression }
240
241 ShiftExpression : : = EqualityExpression { ( '<<' | '>>' ) EqualityExpression }
242
243 EqualityExpression : : = ComparisonExpression { ( '==' | '!=' ) ComparisonExpression }
244
245 ComparisonExpression : : = AdditiveExpression { ( '<' | '<=' | '>' | '>=' ) AdditiveExpression }
246
247 AdditiveExpression : : = MultiplicativeExpression { ( '+' | '-' ) MultiplicativeExpression }
248
249 MultiplicativeExpression : : = CastExpression { ( '*' | '/' | '%' ) CastExpression }
250
251 CastExpression : : = UnaryExpression { KW_AS Type }
252
253 UnaryExpression : : =
254 ( KW_AWAIT | '-' | '!' | '&' | '~' ) UnaryExpression
255 | PostfixExpression
256
257 PostfixExpression : : =
258 PrimaryExpression {
259 '(' [ ArgumentList ] ')'
260 | '[' Expression ']'
261 | ( '.' | '?.' ) IDENTIFIER
262 }
263
264 PrimaryExpression : : =
265 PathExpression
266 | Literal
267 | '(' Expression ')'
268 | ArrayLiteral
269 | NamedStructLiteral
270 | AnonymousStructLiteral
271 | FunctionExpression
272 | NewExpression
273 | MatchExpression
274
275 MatchExpression : : = KW_MATCH '(' Expression ')' '{'
276 [ MatchExprArm { ',' MatchExprArm } [ ',' ] ]
277 '}'
278
279 MatchExprArm : : = MatchArmPattern '=>' Expression
280
281 PathExpression : : = IDENTIFIER { '::' IDENTIFIER }
282
283 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
284 ( TEMPORAL EXPRESSIONS )
285 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
286
287 TemporalExpression : : =
288 KW_ALWAYS Expression
289 | KW_EVENTUALLY Expression
290 | KW_NEXT Expression
291 | Expression KW_UNTIL Expression
292 | Expression KW_RELEASE Expression
293 | KW_FORALL IDENTIFIER KW_IN Expression ':' Expression
294 | KW_EXISTS IDENTIFIER KW_IN Expression ':' Expression
295 | Expression
296
297 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
298 ( LITERALS & HELPER RULES )
299 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
300
301 Literal : : =
302 INT_LITERAL | FLOAT_LITERAL | STRING_LITERAL | STRING_MULTILINE
303 | CHAR_LITERAL | KW_TRUE | KW_FALSE
304
305 ArrayLiteral : : = '[' [ ArgumentList ] ']'
306 NamedStructLiteral : : = PathExpression StructLiteralBody
307 AnonymousStructLiteral : : = StructLiteralBody
308 StructLiteralBody : : = '{' [ StructElement { ',' StructElement } [ ',' ] ] '}'
309 StructElement : : = ( IDENTIFIER ':' Expression ) | IDENTIFIER | '...' Expression
310
311 ArgumentList : : = CallArgument { ',' CallArgument }
312 CallArgument : : = [ '...' ] [ KW_MUT ] Expression
313
314 FunctionExpression : : = FnModifiers KW_FN '(' [ FunctionParameters ] ')' [ '->' Type ] Block
315
316 NewExpression : : = KW_NEW PrimaryExpression
317
318 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
319 ( TERMINALS ( TOKENS ) )
320 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
321
322 IDENTIFIER
323 INT_LITERAL , FLOAT_LITERAL , STRING_LITERAL , STRING_MULTILINE , CHAR_LITERAL
324
325 ( * Keywords : * )
326 KW_LET , KW_VAR , KW_FN , KW_IF , KW_IN , KW_ELSE , KW_WHILE , KW_FOR ,
327 KW_RETURN , KW_BREAK , KW_CONTINUE , KW_WHERE ,
328 KW_ASYNC , KW_AWAIT , KW_AS , KW_STRUCT , KW_IMPL , KW_THREAD ,
329 KW_ENUM , KW_MATCH , KW_TYPE , KW_VOID , KW_INT ,
330 KW_U8 , KW_I8 , KW_U16 , KW_I16 , KW_U32 , KW_I32 , KW_U64 , KW_I64 ,
331 KW_F32 , KW_F64 , KW_BOOL , KW_CHAR , KW_STRING , KW_TRUE , KW_FALSE ,
332 KW_IMPORT , KW_EXPORT , KW_FROM , KW_FFI , KW_MAP ,
333
334 ( * No shared mutability without atomics ! * )
335 KW_NEW , KW_SHARED , KW_ATOMIC , KW_WEAK , KW_MUT ,
336
337 KW_SYSTEM , KW_ACTION , KW_REQUIRES , KW_ENSURES ,
338 KW_INVARIANT , KW_PROPERTY ,
339 KW_FORALL , KW_EXISTS , KW_ALWAYS , KW_EVENTUALLY , KW_NEXT , KW_UNTIL ,
340 KW_RELEASE , KW_PURE
341
342 ( * Operators and Delimiters : Arithmetic Wraps )
343 '=' , '+' , '-' , '*' , '/' , '%' , '&' , '==' , '!=' , '<' , '<=' , '>' , '>=' ,
344 '!' , '&&' , '||' , '|' , '^' , '~' , '<<' , '>>' , '(' , ')' , '{' , '}' , '[' , ']' ,
345 ',' , ';' , '.' , ':' , '::' , '?' , '?.' , '??' , '...' , '..=' , '..' , '->' , '_' , '=>'
346
347 EOF

Get Started

Aela is a software platform for creating formally verifiable, memory safe, and highly reliable applications. What's included in the compiler:

  • Works in any editor
  • Provides a built in linter, formatter, and LSP.
  • A local, offline-first agent that understands the compiler and your codebase and can talk to your other AI services.
  • Supports JIT module (that's hot reloading for compiled programs, aka: edit and continue)

Install the compiler.

Example
0 sudo sh - c 'curl -fsSL https://stablestate.ai/$CUSTOMER_ID | bash'

In a new directory, create a new project using the following command.

Example
0 aec init

This will create some default files.

Example
0 .
1 ├── index . json
2 └── src
3 └── main . ae

Edit the index.json file to name your project.

Example
0 {
1 "name" : "aela - tests" ,
2 "version" : "0 . 1 . 0" ,
3 "entry" : "main . ae" ,
4 "output" : "build / main" ,
5 "include" : [ "src /**/ * . ae" ] ,
6 "exclude" : [ "tests / * * " ] ,
7 "dependencies" : [
8 {
9 "name" : "ui" ,
10 "url" : " . . / lib / ui"
11 }
12 ]
13 }

Next you’ll edit the main.ae file

int { io::print("Hello, Aela!"); return 0; }" lang="rust" title="Example" id="8580699c4d9ff">
Example
0 // Aela Hello World
1
2 import io from "io" ;
3
4 fn main ( args : string [ ] ) - > int {
5 io : : print ( "Hello , Aela ! " ) ;
6 return 0 ;
7 }

To build your project, run the following command.

Example
0 aec build

You’ll see a new directory with the compiled program that you can run.

- new files
0 .
1 ├── build
2 │ └── main
3 ├── index . json
4 └── src
5 └── main . ae

Compiler Modes

aec build

This is your traditional, one-shot Ahead-Of-Time (AOT) compiler command.

What Compiles your entire project from source into a final, optimized executable binary.
How It invokes the compiler engine, which runs all formal verifications, performs release-level optimizations (which can be slow), and links everything together. It's a non-interactive process designed for final output.
Why Running in a Continuous Integration (CI) pipeline or when you're ready to create a production version of your application.

aec run

This is a convenience command for development.

What Builds and immediately executes your program.
How It's a simple wrapper that first performs an aec build (likely with fewer optimizations to be faster than a release build) and then runs the resulting binary.
Why Quickly testing a command-line application's behavior without needing a full watch session.

aec watch

This command exposes the engine's interactive mode directly to the command line.

What Starts the persistent, incremental engine to monitor files and provide continuous feedback in the terminal.
How It activates the same underlying engine that the LSP uses. On every change, it re-verifies your code and can be configured to take an action, such as re-running your test suite (aec watch --exec test) or restarting your application via the JIT.
Why For developers who prefer working in the terminal, it enables a very fast Test-Driven Development (TDD) loop without the overhead of an IDE.

aec package

This is a higher-level workflow and distribution tool.

What Bundles your project into a distributable format.
How It would first run aec build --release to create the optimized executable. Then, it would gather other assets—like documentation, licenses, and configuration files—and package them into a compressed archive (like a .tar.gz) for publishing to a registry or for distribution.
Why Publishing a new version of your library or application for others to use.

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 wont 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

- /path/to/my-app/index.json
0 {
1 "name" : "my - app" ,
2 "version" : "0 . 1 . 0" ,
3 "entry" : "src / main . ae"
4 }

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.json to the
  • dependency's root directory.

Example

- index.json for a project that uses a UI library
0 {
1 "name" : "my - gui - app" ,
2 "version" : "1 . 0 . 0" ,
3 "entry" : "src / app . ae" ,
4 "dependencies" : {
5 "ui" : " . . / libs / aela - ui" ,
6 "database" : " . . / libs / aela - db"
7 }
8 }

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

- ../libs/aela-ui/src/window.ae
0 // This struct can be imported by other modules.
1 export struct WindowOptions {
2 title : string ;
3 width : int ;
4 height : int ;
5 }
6
7 // This function is private to the window.ae module.
8 fn internal_helper ( ) - > void {
9 // ...
10 }

Re-exports

- re-export pattern
0 import { Thing1 , Thing2 } from "things . ae" ;
1
2 export Thing1 ;
3 export Thing2 ;

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

";" lang="" title="Example" id="ab5bd6fe58171">
Example
0 import from " " ;

Example

void { // Access symbols using the `::` namespace operator. let opts: &ui::WindowOptions = new ui::WindowOptions(); helpers::do_something(); }" lang="example" title="- src/app.ae" id="49a0501874e3d">
- src/app.ae
0 // Import the "ui" package, which was defined in index.json.
1 import ui from "ui" ;
2
3 // Import a local utility file using a relative path.
4 import helpers from " . / helpers . ae" ;
5
6 fn main ( ) - > void {
7 // Access symbols using the `::` namespace operator.
8 let opts : & ui : : WindowOptions = new ui : : WindowOptions ( ) ;
9 helpers : : do_something ( ) ;
10 }

Named Imports

This allows you to import specific functions or types directly into the current scope, without needing a namespace qualifier.

Syntax

";" lang="" title="Example" id="2068a58526a41">
Example
0 import { , : } from " " ;

Example EXAMPLE

void { // `Window` can be used directly. let win: &Window = new Window(); // `WindowOptions` is available under the alias `Options`. let opts: &Options = new Options(); }" lang="example" title="- src/app.ae" id="5ed0ea6e30563">
- src/app.ae
0 import { Window , WindowOptions : Options } from "ui" ;
1
2 fn main ( ) - > void {
3 // `Window` can be used directly.
4 let win : & Window = new Window ( ) ;
5
6 // `WindowOptions` is available under the alias `Options`.
7 let opts : & Options = new Options ( ) ;
8 }

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

- index.json for an app with native UI code on macOS
0 {
1 "name" : "aela - native - app" ,
2 "version" : "0 . 1 . 0" ,
3 "entry" : "src / main . ae" ,
4 "sources" : {
5 "darwin" : [ "src / native / apple . mm" ]
6 } ,
7 "link" : {
8 "platform" : {
9 "darwin" : [
10 " - framework" , "Foundation" ,
11 " - framework" , "AppKit" ,
12 " - framework" , "ObjectiveC"
13 ]
14 }
15 }
16 }

This configuration tells the Aela compiler:

  1. On macOS, compile the src/native/apple.mm Objective-C++ file.
  2. When linking the final executable, add flags to link against the
  3. Foundation, AppKit, and ObjectiveC system frameworks.

Type System Features

Refinement Types

Refinement types allow you to create a subtype from an existing type by adding constraints that are checked at compile time. This is useful for creating more precise and bug-resistant types.

Example
0 type NewTypeName = { value_name : BaseType where condition_expression } ;
value_name A name for the value, used within the where clause.
BaseType The type you are constraining (e.g., i32, string).
condition_expression A boolean expression that must be true for any value of this new type.

Example

You can define a type that only allows positive integers, preventing invalid values at compile time.

- Positive Integer
0 // Definition
1 type PositiveInt = { n : i32 where n > 0 } ;
2
3 // Usage
4 fn set_age ( age : PositiveInt ) {
5 // ... function body
6 }
7
8 set_age ( 10 ) ; // OK
9 set_age ( - 5 ) ; // Compile-time error!

Example: Non-Empty String

Refinements can use functions or properties in their conditions.

Example
0 // Definition
1 type NonEmptyString = { s : string where s . length ( ) > 0 } ;
2
3 // Usage
4 fn print_greeting ( name : NonEmptyString ) {
5 // ...
6 }

The primary benefit is turning potential runtime errors (like invalid arguments) into compile-time errors.

Dependent Types

Dependent types allow a type's definition to depend on a value, not just another type. This lets you encode properties like the size of a collection directly into its type.

Defining Dependent Structs

You define a dependent struct by including value parameters in parentheses () in its declaration.

Syntax

struct TypeName(param_name: ParamType, ...);

Example

Here, the Vect type depends on a length len and a type T.

Length-Indexed Vector
0 // Definition of a vector that knows its length.
1 struct Vect ( len : nat , T : Type ) {
2 // ... implementation details
3 }
4
5 // Usage in a variable declaration.
6 let names : Vect ( 3 , string ) = [ "Alice" , "Bob" , "Charlie" ] ;
7 let empty : Vect ( 0 , i32 ) = [ ] ;

Using Dependent Types in Functions

Function signatures can use these value dependencies to enforce relationships between parameters, making impossible states unrepresentable.

Example

This function to get the first element of a vector can only be called on a non-empty vector.

Safe head function
0 // The type signature guarantees vec has a length of at least 1.
1 fn safe_head ( len : nat , T : Type , vec : Vect ( len + 1 , T ) ) - > T {
2 return vec [ 0 ] ;
3 }
4
5 let first_name : string = safe_head ( 2 , string , names ) ; // OK
6
7 // The following line would cause a compile-time error because the
8 // compiler can see that the type Vect(0, i32) does not match
9 // the required type Vect(n + 1, T).
10 let error = safe_head ( - 1 , i32 , empty ) ;

This feature allows you to build extremely safe APIs by making the compiler aware of properties that are usually only checked at runtime.

Operators

Precedence Operator(s) Description Associativity
1 (Lowest) = Assignment Right-to-left
2 ?? Optional Coalescing Left-to-right
3 || Logical OR Left-to-right
4 && Logical AND Left-to-right
5 | Bitwise OR Left-to-right
6 ^ Bitwise XOR Left-to-right
7 & Bitwise AND Left-to-right
8 == , != Equality / Inequality Left-to-right
9 < , > , <= , >= Comparison Left-to-right
10 << , >> Bitwise Shift Left-to-right
11 + , - Addition / Subtraction Left-to-right
12 * , / , % Multiplication / Division / Modulo Left-to-right
13 ! , - , ~ , & (prefix), await Unary (Logical NOT, Negation, Bitwise NOT, Address-of, Await) Right-to-left
14 (Highest) () , [] , . , ?. , as Function Call, Index Access, Member Access, Type Cast Left-to-right

Flow Control

This document covers all flow control constructs in Aela, including conditionals, loops, and matching.

1. if / else

Syntax

Example
0 if ( )
1 [ else ]

Description

Standard conditional branching.

  • The condition must be an expression evaluating to bool .
  • Both then_branch and else_branch must be statements , usually blocks.

Examples

Example
0 if ( x > 0 ) {
1 print ( "Positive" ) ;
2 } else {
3 print ( "Non - positive" ) ;
4 }
5
6 if ( flag ) doSomething ( ) ;

2. while Loop

Syntax

Example
0 while ( )

Description

Loops as long as the condition evaluates to true .

Example

Example
0 while ( i < 10 ) {
1 i = i + 1 ;
2 }

3. for Loop

Syntax

Example
0 for ( in )

Description

Iterates over a collection or generator. Declarations must bind variables with types.

Example

Example
0 for ( let i : Int in 0 . . 10 ) {
1 print ( " { } " , i ) ;
2 }
3
4 for ( var x : String , var y : String in lines ) {
5 print ( " { } { } " , x , y ) ;
6 }

4. match Expression

Syntax

Example
0 match ( ) {
1 = > ,
2 . . .
3 _ = >
4 }

Description

Exhaustive pattern matching. Each match arm uses a pattern and a block.

  • _ is the wildcard pattern (required if not all cases are covered).
  • Patterns can be:
  • Literals: 1 , "foo" , 'c' , true , false
  • Identifiers: binds the value
  • Constructor patterns: Some(x) , Err(e)

Example

{ print("One"); }, _ => { print("Other"); } }" lang="aela" title="Example" id="36ee863b38fd8">
Example
0 match ( value ) {
1 0 = > { print ( "Zero" ) ; } ,
2 1 = > { print ( "One" ) ; } ,
3 _ = > { print ( "Other" ) ; }
4 }

5. return

Syntax

Example
0 return [ ] ;

Description

Exits a function immediately with an optional return value.

Examples

Example
0 return ;
1 return x + 1 ;

6. break

Syntax

Example
0 break ;

Description

Terminates the nearest enclosing loop.

7. continue

Syntax

Example
0 continue ;

Description

Skips to the next iteration of the nearest enclosing loop.

8. Blocks and Statement Composition

Syntax

Example
0 {
1 ;
2 . . .
3 }

Description

A block groups multiple statements into a single compound statement. Used for control flow bodies.

Example

Example
0 {
1 let x : Int = 1 ;
2 let y : Int = x + 2 ;
3 print ( y ) ;
4 }

9. Expression Statements

Syntax

Example
0 ;

Description

Evaluates an expression for side effects. Common for function calls or assignments.

Example

Example
0 doSomething ( ) ;
1 x = x + 1 ;

Optionals

Optionals provide a safe and explicit way to handle values that may or may not be present. Instead of using special values like null or -1 which can lead to runtime errors, Aela uses the Option type to wrap a potential value. The compiler will then enforce checks to ensure you handle the "empty" case safely.

Declaring an Optional Type

You can declare a variable or field as optional using two equivalent syntaxes:

  1. The `?` Suffix (Recommended) : This is the preferred, idiomatic syntax.
  2. It's a concise way to mark a type as optional.
Example
0 // A variable that might hold a string
1 let name : string ? ;
2
3 // A struct with optional fields
4 struct Profile {
5 age : u32 ? ;
6 bio : string ? ;
7 }
  1. The `Option(T)` Syntax : This is the formal, nominal type. The T?
  2. syntax is simply sugar for this. It can be useful in complex, nested type
  3. signatures for clarity.
Example
0 // This is equivalent to `let name: string?`
1 let name : Option ( string ) ;

Creating Optional Values

An optional variable can be in one of two states: it either contains a value, or it's empty. You use the Some and None keywords to create these states.

None : The Empty State

The None keyword represents the absence of a value. You can assign it to any optional variable, and the compiler will infer the correct type from the context.

Example
0 let age : u32 ? = None ;
1
2 let user : User = {
3 // The profile field is optional
4 profile : None
5 } ;
6 Some ( value ) : The Value - Holding State

To create an optional that contains a value, you wrap the value with the Some constructor.

Example
0 // Create an optional u32 containing the value 30
1 let age : u32 ? = Some ( 30 ) ;
2
3 let user : User = {
4 profile : Some ( {
5 email : "some@example . com" ,
6 age : Some ( 30 )
7 } )
8 } ;

The Optional-Coalescing Operator (??) (For Defaults)

This is the best way to unwrap an optional by providing a fallback value to use if the optional is None. The term "coalesce" means to merge or come together; this operator coalesces the optional's potential value and the default value into a single, guaranteed, non-optional result.

Example
0 // Get the user's email, or use a default if it's None.
1 // `email_address` will be a regular `string`, not a `string?`.
2 let email_address : string = user2 . profile ? . email ? ? "no - email - provided@domain . com" ;
3
4 print ( "Contacting user at : { } " , email_address ) ;

Using Optional Values

Aela provides mechanisms to safely work with optional values, preventing you from accidentally using an empty value as if it contained something.

Optional Chaining (?.)

The primary way to access members of an optional struct is with the optional chaining operator, ?.. If the optional is None, the entire expression short-circuits and evaluates to None. If it contains a value, the member access proceeds.

The result of an optional chain is always another optional.

Example
0 struct Profile {
1 email : string ;
2 }
3
4 struct User {
5 profile : Profile ? ;
6 }
7
8 fn main ( ) - > int {
9 let user1 : User = { profile : Some ( { email : "test@example . com" } ) } ;
10 let user2 : User = { profile : None } ;
11
12 // email1 will be an `Option(string)` containing Some("test@example.com")
13 let email1 : string ? = user1 . profile ? . email ;
14
15 // email2 will be an `Option(string)` containing None
16 let email2 : string ? = user2 . profile ? . email ;
17
18 return 0 ;
19 }

Explicit Checking (Match Statement)

Use match statements to explicitly handle the Some and None cases, allowing you to unwrap the value and perform more complex logic.

io::print("The name is: {}", value), None => io::print("No name was provided."), }" lang="" title="Example" id="c36380aec7d35">
Example
0 let name : string ? = Some ( "Aela" ) ;
1
2 match name {
3 Some ( value ) = > io : : print ( "The name is : { } " , value ) ,
4 None = > io : : print ( "No name was provided . " ) ,
5 }

Mutability

Aela enforces safety and clarity by requiring that any function intending to modify data must be explicitly marked. This prevents accidental changes and makes code easier to reason about. This is achieved through the mut keyword.

The Principle: Safe by Default

In Aela, all function parameters are immutable (read-only) by default. When you pass a variable to a function, you are providing a read-only view of it.

Example
0 fn read_runner ( r : & Runner ) {
1 // This is OK.
2 io : : print ( "Points : { } " , r . point ) ;
3
4 // This would be a COMPILE-TIME ERROR.
5 // r.point = 5;
6 }

Granting Permission to Mutate

To allow a function to modify a parameter, you must use the mut keyword in two places:

  1. The Function Definition: To declare that the function requires mutable
  2. access.
  3. The Call Site: To explicitly acknowledge that you are passing a variable
  4. to be changed.

This two-part system makes mutation a clear and intentional act.

In the Function Definition

Prefix the parameter you want to make mutable with mut . This is the function's "contract," stating its intent to modify the argument.

Example
0 fn reset_runner ( mut r : & Runner ) {
1 // This is now allowed because the parameter `r` is marked as `mut`.
2 r . point = 0 ;
3 r . passed = 0 ;
4 r . failed = 0 ;
5 }

At the Call Site

When you call a function that expects a mutable parameter, you must also prefix the argument with mut . This confirms you understand the variable will be modified.

Example
0 fn main ( ) {
1 // The variable itself must be mutable, declared with 'var'.
2 var my_runner = Runner . new ( ) ;
3
4 // The 'mut' keyword is required here to pass 'my_runner'
5 // to a function that expects a mutable argument.
6 reset_runner ( mut my_runner ) ;
7 }

The compiler will produce an error if you try to pass a mutable argument without the mut keyword, or if you try to pass an immutable ( let ) variable to a function that expects a mutable one. This ensures there are no surprises about where your data can be changed.

Errors

Out-of-the-box errors are simple, the verifier runs the check borrows and the life-times of variables and properties.

An error where mut keyword should have been used
0 Analyzer Error : Cannot assign to field 'point' because 'self' is immutable .
1 - - > / Users / paolofragomeni / projects / aela / lib / test . ae : 16 : 10
2
3 15 | fn ok ( self : & Self , cond : bool , desc : string ) - > bool {
4 16 - > self . point = self . point + 1 ;
5 | ^
6 17 |

Structs, Impl Blocks, and Memory Layout

struct Declarations: The Data Blueprint

A struct defines a composite data type. Its sole purpose is to describe the memory layout of a collection of named fields. Structs contain ONLY data members.

Syntax

Example
0 struct {
1 : ;
2 : ;
3 . . .
4 }

Example

Defines a type named 'Packet' that holds a sequence number, a size, and a single-byte flag.

Example
0 struct Packet {
1 sequence : u32 ;
2 size : u16 ;
3 is_urgent : u8 ;
4 }

impl Blocks: Attaching Behavior

An impl (implementation) block associates functions with an existing struct type. These functions are called methods. The impl block does NOT alter the struct's memory layout or size.

Example
0 impl {
1 // constructor (optional, special method)
2 fn constructor ( self : & Self , . . . ) - > Self { . . . }
3
4 // methods
5 fn ( self : & Self , . . . ) - > { . . . }
6 }

Details

  • The constructor is a special function that initializes the
  • struct's memory. It is called when using the new keyword.
  • Methods are regular functions that receive a reference to an
  • instance of the struct as their first parameter, named self .
  • Self (capital 'S') is a type alias for the struct being implemented.
  • Multiple impl blocks can exist for the same struct. The compiler
  • merges them.

Example

Example
0 impl Packet {
1 fn constructor ( self : & Self , seq : u32 ) - > Self {
2 self . sequence = seq ;
3 self . size = 0 ;
4 self . is_urgent = 0 ;
5 }
6
7 fn mark_urgent ( self : & Self ) - > void {
8 self . is_urgent = 1 ;
9 }
10 }

Memory Layout and Padding

Aela adopts C-style struct memory layout rules, including padding and alignment, to ensure efficient memory access and ABI compatibility.

  1. Sequential Layout: Fields are laid out in memory in the exact
  2. order they are declared in the struct definition.
  1. Alignment: Each field is aligned to a memory address that is a
  2. multiple of its own size (or the platform's word size for larger
  3. types). The compiler inserts unused "padding" bytes to enforce this.
  1. Struct Padding: The total size of the struct itself is padded to be a
  2. multiple of the alignment of its largest member. This ensures that
  3. in an array of structs, every element is properly aligned.

Rules:

Example
0 struct Packet {
1 sequence : u32 ; // 4 bytes
2 size : u16 ; // 2 bytes
3 is_urgent : u8 ; // 1 byte
4 }

Visual Layout (on a typical 64-bit system):

Byte Offset Content
0 sequence (Byte 0)
1 sequence (Byte 1)
2 sequence (Byte 2)
3 sequence (Byte 3) ← 4‑byte
Byte Offset Content
4 size (Byte 0)
5 size (Byte 1) ← 2‑byte
Byte Offset Content
6 is_urgent (Byte 0)
Byte Offset Content
7 PADDING (1 byte) ← struct padded to a multiple of 4 bytes (max)

TOTAL SIZE: 8 bytes

Heap vs. Stack Allocation

Aela supports both heap and stack allocation for structs, giving the programmer control over memory management and performance.

Stack allocation (Default for local variables):

  • How: A struct is allocated on the stack by declaring a variable of
  • the struct type and initializing it with a struct literal. The new
  • keyword is NOT used.
  • Lifetime: The memory is valid only within the scope where it is
  • declared (e.g., inside a function). It is automatically reclaimed
  • when the scope is exited.
  • Performance: Extremely fast. Allocation and deallocation are nearly
  • instant, involving only minor adjustments to the stack pointer.
Example
0 let my_packet : Packet = Packet {
1 sequence : 200 ,
2 size : 128 ,
3 is_urgent : 1
4 } ;

Heap Allocation (Explicit):

  • How: A struct is allocated on the heap using the new keyword, which
  • returns a reference ( & ) to the object.
  • Lifetime: The memory persists until it is no longer referenced. Its
  • lifetime is managed by the runtime's reference counter, not tied to a
  • specific scope.
  • Performance: Slower than stack allocation. Involves a call to the
  • system's memory allocator ( malloc ) and requires runtime overhead for
  • reference counting.
- Explicit Heap Allocation
0 let my_packet_ref : & Packet = new Packet ( 201 ) ;

When to use which:

  • STACK: Use for most local, temporary data. It's the idiomatic and
  • most performant choice for data that does not need to outlive the
  • function in which it was created.
  • HEAP: Use when a struct instance must be shared or returned from a
  • function and needs to have a lifetime independent of any single
  • scope. Also used for very large structs to avoid overflowing the stack.

Opaque Structs

Safety & Undefined Behavior (UB)

The primary benefit of opaque structs is preventing a whole class of undefined behavior by strengthening type safety at the language boundary.

How Safety is Increased

Eliminates Type Confusion: Before, you might have used a generic type like `u64` or `&void` to represent a C handle. The compiler had no way to know that a `u64` from `database_connect()` was different from a `u64` from `file_open()`. You could accidentally pass a database handle to a file function, leading to memory corruption or crashes. Now, `&DatabaseHandle` and `&FileHandle` are distinct, incompatible types *. The Aela compiler will issue a compile-time error if you try to misuse them, completely eliminating this risk.

Prevents Invalid Operations in Aela: * By disallowing member access and instantiation, we prevent Aela code from making assumptions about the C data structure. Aela code cannot accidentally:

Read from or write to a field that doesn't exist or has a different offset (`my_handle.field`). Create a struct of the wrong size on the stack ( let handle: StringBuilder ). * Perform pointer arithmetic on the handle. The only thing Aela code can do is treat the handle as an opaque value to be passed back to the C library, which is the only safe way to interact with it.

For Users of Opaque Structs

Your documentation should include:

  1. Purpose and Syntax: Explain that opaque structs are for safely handling foreign pointers/handles. Show the syntax:
Example
0 // in lib/mylib.ae
1 export struct MyFFIHandle ;
  1. Rules of Engagement: Clearly state the allowed and disallowed operations we implemented.

Allowed: Passing to/from FFI functions, assigning to other variables of the same type, comparing for equality. Disallowed: Member access ( . ), instantiation ( new ), and dereferencing. Always use a reference ( &MyFFIHandle ).

  1. A Mandatory Safety Section on Lifetimes: This section must be prominent. It should explain the dangling pointer risk and establish a clear best practice.

When working with opaque handles, you are responsible for managing their memory. Most C libraries provide functions for creating and destroying these objects. You must call the destruction function to prevent memory leaks and undefined behavior.

&StringBuilder; ffi ae_runtime_sb_append: fn(&StringBuilder, string); ffi ae_runtime_sb_destroy: fn(&StringBuilder); // <-- The cleanup function fn main() -> int { let sb = ae_runtime_sb_new(); ae_runtime_sb_append(sb, "hello"); // CRITICAL: You must call destroy when you are done. ae_runtime_sb_destroy(sb); // Using `sb` after this point is UNDEFINED BEHAVIOR. // ae_runtime_sb_append(sb, " world"); // <-- DANGEROUS! return 0; }" lang="aela" title="Example: Managing Lifetimes" id="0b432d411f723">
Example: Managing Lifetimes
0 `` `aela
1 import { StringBuilder } from " . / runtime . ae" ;
2
3 // FFI Declarations for a C string builder
4 ffi ae_runtime_sb_new : fn ( ) - > & StringBuilder ;
5 ffi ae_runtime_sb_append : fn ( & StringBuilder , string ) ;
6 ffi ae_runtime_sb_destroy : fn ( & StringBuilder ) ; // <-- The cleanup function
7
8 fn main ( ) - > int {
9 let sb = ae_runtime_sb_new ( ) ;
10 ae_runtime_sb_append ( sb , "hello" ) ;
11
12 // CRITICAL: You must call destroy when you are done.
13 ae_runtime_sb_destroy ( sb ) ;
14
15 // Using `sb` after this point is UNDEFINED BEHAVIOR.
16 // ae_runtime_sb_append(sb, " world"); // <-- DANGEROUS!
17
18 return 0 ;
19 }

Interfaces

This document specifies the design and behavior of Aela's system for polymorphism, which is based on interface, struct, and impl...as... declarations.

Overview

Aela's polymorphism is designed to be explicit, safe, and familiar. It allows developers to write flexible code that can operate on different data types in a uniform way, a concept known as dynamic dispatch. This is achieved by separating a contract's definition (the interface) from its implementation (the struct and impl block).

Example
0 interface Element {
1 fn onclick ( event : & Event ) - > void ;
2 }
3
4 struct Button {
5 handle : i64 ;
6 }
7
8 impl Button as Element {
9 fn constructor ( self : & Self , someArg1 : string ) {
10 // fired when new is used
11 }
12 fn init ( self : & Self , someArg1 : string ) {
13 // fired when ever a struct is initialized.
14 }
15 fn onclick ( self : & Self , event : & Event ) - > void {
16 // fired when called directly (statically or dynamically)
17 }
18 }
19
20 impl Button as Element {
21 fn ontoch ( self : & self , event : & Event ) - > void {
22 }
23 }

The core philosophy is:

Interfaces define abstract contracts or capabilities.

Structs define concrete data structures.

impl...as... blocks prove that a concrete struct satisfies an abstract interface.

Components

The interface Declaration

An interface defines a set of method signatures that a concrete type must implement to conform to the contract.

Example
0 interface {
1 fn ( ) - > ;
2 // ... more method signatures
3 }

Rules:

An interface block can only contain method signatures. It cannot contain any data fields.

Method signatures within an interface must not have a body. They must end with a semicolon ;.

The self parameter in an interface method must be of a reference type (e.g., &self).

Example
0 interface Serializable {
1 fn serialize ( & self ) - > string ;
2 }

The struct Declaration

A struct defines a concrete data type. Its role is unchanged.

Example
0 struct {
1 : ;
2 // ... more data fields
3 }

Rules:

A struct can only contain data fields. Method implementations are defined separately in impl blocks.

Example
0 struct User {
1 id : int ;
2 username : string ;
3 }

The impl...as... Declaration

This block connects a concrete struct to an interface, proving that the struct fulfills the contract.

Example
0 impl as {
1 // Implementations for all methods required by the interface
2 fn ( ) - > {
3 // ... method body ...
4 }
5 }

Rules:

The impl block must provide a concrete implementation for every method defined in the .

The signature of each implemented method must be compatible with the corresponding signature in the interface.

A single struct may implement multiple interfaces by using separate impl...as... blocks for each one.

Example
0 impl User as Serializable {
1 fn serialize ( & self ) - > string {
2 // Implementation of the serialize method for the User struct
3 return std : : format ( " { { \"id\" : { } , \"username\" : \" { } \" } } " , self . id , self . username ) ;
4 }
5 }

Interface Types

A variable can be declared with an interface type by using a reference. This creates a "trait object" or "fat pointer" that can hold any concrete type that implements the interface.

Syntax: &

Behavior: A variable of type & is a fat pointer containing two components:

A pointer to the instance data (e.g., a &User).

A pointer to the v-table for the specific (Struct, Interface) implementation.

Example
0 let objects : & Serializable [ ] = [
1 & User { id : 1 , username : "aela" } ,
2 & Document { title : "spec . md" }
3 ] ;
4
5 for ( let obj : & Serializable in objects ) {
6 // This call is dynamically dispatched using the v-table.
7 io : : print ( obj . serialize ( ) ) ;
8 }

Formal Verification

This document specifies the design and behavior of Aela's compile-time verification system , which ensures the correctness of a program through formal specifications — namely, invariant and property declarations embedded in impl blocks.

Overview

Aela enables developers to write mathematically precise specifications that describe the expected state and behavior of a program, which the compiler formally verifies at compile time. These specifications are not runtime code — they do not execute, incur no runtime cost, and exist solely to ensure program correctness before code generation.

There are two key constructs:

`invariant`: A safety condition that must always hold before and after each function in an `impl` block. property : A liveness or temporal condition that must hold across all valid execution traces of a type’s behavior.

These declarations allow Aela to verify complex asynchronous and stateful systems using SMT solvers or model checking , without requiring users to learn a separate formal language.

Example
0 struct BankAccount {
1 balance : i32 ;
2 overdraft_limit : u32 ;
3 }
4
5 impl BankAccount {
6 // Always ensure the balance never drops below the allowed overdraft.
7 invariant balanceIsAboveLimit = balance > = - overdraft_limit
8
9 // Eventually, the system must bring the account back to a non-negative balance.
10 property eventuallyInBalance = eventually ( balance > = 0 )
11
12 fn recoverNow ( ) {
13 requires balance < 0
14 ensures balance = = 0
15
16 balance = 0 ;
17 }
18 }

Components

The invariant Declaration

An invariant specifies a condition that must hold before and after every function in an impl block.

Example
0 invariant =

Rules:

Invariants must be side-effect-free boolean expressions. They may reference any field defined in the corresponding struct . The compiler verifies that every function in the `impl` block preserves the invariant. If the compiler cannot prove an invariant holds across all paths, compilation fails.

Example
0 struct Counter { count : int }
1
2 impl Counter {
3 invariant nonNegative = count > = 0
4
5 fn increment ( ) {
6 ensures count = = old ( count ) + 1
7 count = count + 1 ;
8 }
9 }

In this example, the invariant guarantees that the count is never negative. The verifier ensures this remains true even after increment() executes.


The property Declaration

A property expresses a temporal guarantee — such as liveness or ordering — across the program’s behavior.

Example
0 property =

Temporal expressions may include:

`always`: The condition must hold at all times. eventually : The condition must hold at some point in the future. `until`, `release`: Conditions over time and ordering. forall , exists : Quantifiers over a domain (e.g., tasks, states).

Rules:

Properties do not affect control flow or behavior. They are used by the compiler to prove guarantees about possible program traces . * Property violations produce counterexample traces at compile time.

Example
0 struct Queue {
1 items : Array ( int ) ;
2 }
3
4 impl Queue {
5 invariant lengthNonNegative = std : : length ( items ) > = 0
6
7 // Ensure that any task awaiting the queue will eventually see data.
8 property eventuallyReceivesItem = forall task in Tasks {
9 eventually ( ! std : : is_empty ( items ) )
10 }
11
12 async fn dequeue ( ) - > int {
13 while std : : is_empty ( items ) {
14 await wait_for_item ( ) ;
15 }
16 return std : : pop ( items ) ;
17 }
18
19 fn enqueue ( value : int ) {
20 std : : push ( items , value ) ;
21 notify_waiters ( ) ;
22 }
23 }

Specification Behavior

Construct Scope Verified When Runtime Cost
invariant Per-impl block Before and after each fn / async fn None
property Per-impl block Over all valid execution traces None

`invariant` is used for safety ("nothing bad happens"). property is used for liveness ("something good eventually happens").

The compiler treats both as compile-time-only declarations that participate in verification, not execution.


Old State Reference: old(expr)

The old keyword allows a function’s ensures clause to reference the value of an expression before the function was called.

Example
0 fn increment ( ) {
1 ensures count = = old ( count ) + 1
2 count = count + 1 ;
3 }

The compiler ensures that the post-state ( count ) relates correctly to the pre-state ( old(count) ).


Quantifiers and Temporal Blocks

Quantifiers can be used in properties to express conditions across many elements or tasks.

Example
0 forall in {
1
2 }

Compiler Interactions

- Interacting with the compiler
0 ⚠️ [ formal : invariant ] Detected 1 invariant violation during compilation .
1
2 Invariant :
3 balanceIsAboveLimit = balance > = - overdraft_limit
4
5 Violated in :
6 impl BankAccount → fn emergencyReset ( )
7
8 ╭────┬───────────────────────────────────────────────
9 ╭─│ 12 │
10 │ │ 13 │ balance = - 999 ;
11 │ │ │ ^ ^ ^ ^ ^ violates invariant after execution
12 │ │ 14 │
13 │ ╰────┴───────────────────────────────────────────────
14
15 │ 💡 The compiler attempted to prove that `balance >= -overdraft_limit`
16 │ holds before and after each function in `impl BankAccount` .
17
18 │ But after calling `emergencyReset()` , balance = - 999 ,
19 │ and overdraft_limit = 0 ⇒ invariant fails : - 999 > = 0 ❌
20
21 ╰──────────────────────────────────────────────────────────
22
23 Actions :
24
25 1 . 🔍 Explain why this invariant is failing
26 2 . ✏️ Show me the relevant values and trace
27 3 . 🧪 Temporarily disable this invariant ( not recommended )
28 4 . 🧼 Open `emergencyReset` to fix it
29 5 . ❌ Remove the invariant entirely
30 6 . ❓ Ask any arbitrary question ( chat )
31
32 > I guess I don't really understand invariants
33
34 No problem , let's take a look at how they work .
35 The first thing we . . .

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 that p is borrowed, with kind ∈ {shared, unique} .
  • Program Point q : A location in the control-flow graph (CFG).
  • Alive(L, q) : Predicate meaning loan L is still valid at point q (lexically-free semantics).
  • Operations :
  • reads(p, q) : a read of place p at q .
  • writes(p, q) : a write to place p at q .
  • moves(p, q) : a move from place p at q .
  • Aliases(x, p, q) : x holds a reference to p at q (logical predicate, tracked via loan origins).
  • Escape(r, q) : Reference r escapes its region at q (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]) -> &i32 elaborates to for<ρ> 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 marker A .

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: self implicitly labeled Self , but can also be explicitly labeled: self: &Self as S .
  • Diagnostics: Role-based: “return value tied to param a (label A) but a does not live long enough.” Quick fixes: “Add as A to param.”
&Self T { return &self.0; } fn other(self: &Self as S, other: &Self as O) -> &O T { return &other.0; } } fn demo_methods() { let p1: Pair(int) = (1, 2); let p2: Pair(int) = (3, 4); let r1 = p1.first(); let r2 = p1.other(&p2); print(r1); print(r2); } // 4) Higher-order function: label flows through function type fn map_head(xs: &Vec(T) as A, f: Fn(&A T) -> U) -> &A U { return f(&xs[0]); } fn demo_hof() { let nums: Vec(int) = [1, 2, 3]; let head_ref = map_head(&nums, fn(x: &int) -> &int { return x; }); print(head_ref); } // 5) Multiple inputs require label: `as A` disambiguates the return fn pick(a: &T as A, b: &T) -> &A T { // body could choose either; label tells the checker the result is tied to `a` return a; } // 6) Optional: Outlives relation (advanced) fn choose_longer(a: &T as X, b: &T as Y) -> &X T where Y : X { return a; } // 7) Struct constructor with label reuse struct Window(T) { head: &A T, tail: &B T } fn mk_window(xs: &Vec(T) as A, ys: &Vec(T) as B) -> Window(T) { return Window { head: &xs[0], tail: &ys[0] }; } fn demo_window() { let xs: Vec(int) = [1,2,3]; let ys: Vec(int) = [4,5,6]; let w = mk_window(&xs, &ys); print(w.head); print(w.tail); } // 8) Diagnostics-friendly failure (commented): // fn bad_pick(a: &T, b: &T) -> &T { // if cond { return a; } else { return b; } // // Error: ambiguous return lifetime. Add `as A` to a (or b) and return `&A T`. // }" lang="aela" title="Example" id="bd62fb2cf27e9">
Example
0 // 1) Disambiguate which input a returned reference is tied to
1 fn first_of_two ( a : & T as A , b : & T ) - > & A T {
2 return a ;
3 }
4
5 fn demo_first_of_two ( ) {
6 let x : int = 10 ;
7 let y : int = 20 ;
8 let rx = & x ;
9 let ry = & y ;
10
11 let r = first_of_two ( rx , ry ) ;
12 print ( r ) ;
13 }
14
15 // 2) Struct field referencing an argument via label
16 struct View ( T ) { data : & A T }
17
18 fn make_view ( x : & T as A ) - > View ( T ) {
19 return View { data : x } ;
20 }
21
22 fn demo_view ( ) {
23 let s : string = "hello" ;
24 let v = make_view ( & s ) ;
25 print ( v . data ) ;
26 }
27
28 // 3) Methods: implicit Self label + explicit labels to disambiguate
29 impl Pair ( T ) {
30 fn first ( self : & Self ) - > & Self T {
31 return & self . 0 ;
32 }
33
34 fn other ( self : & Self as S , other : & Self as O ) - > & O T {
35 return & other . 0 ;
36 }
37 }
38
39 fn demo_methods ( ) {
40 let p1 : Pair ( int ) = ( 1 , 2 ) ;
41 let p2 : Pair ( int ) = ( 3 , 4 ) ;
42
43 let r1 = p1 . first ( ) ;
44 let r2 = p1 . other ( & p2 ) ;
45 print ( r1 ) ;
46 print ( r2 ) ;
47 }
48
49 // 4) Higher-order function: label flows through function type
50 fn map_head ( xs : & Vec ( T ) as A , f : Fn ( & A T ) - > U ) - > & A U {
51 return f ( & xs [ 0 ] ) ;
52 }
53
54 fn demo_hof ( ) {
55 let nums : Vec ( int ) = [ 1 , 2 , 3 ] ;
56 let head_ref = map_head ( & nums , fn ( x : & int ) - > & int { return x ; } ) ;
57 print ( head_ref ) ;
58 }
59
60 // 5) Multiple inputs require label: `as A` disambiguates the return
61 fn pick ( a : & T as A , b : & T ) - > & A T {
62 // body could choose either; label tells the checker the result is tied to `a`
63 return a ;
64 }
65
66 // 6) Optional: Outlives relation (advanced)
67 fn choose_longer ( a : & T as X , b : & T as Y ) - > & X T where Y : X {
68 return a ;
69 }
70
71 // 7) Struct constructor with label reuse
72 struct Window ( T ) { head : & A T , tail : & B T }
73
74 fn mk_window ( xs : & Vec ( T ) as A , ys : & Vec ( T ) as B ) - > Window ( T ) {
75 return Window { head : & xs [ 0 ] , tail : & ys [ 0 ] } ;
76 }
77
78 fn demo_window ( ) {
79 let xs : Vec ( int ) = [ 1 , 2 , 3 ] ;
80 let ys : Vec ( int ) = [ 4 , 5 , 6 ] ;
81 let w = mk_window ( & xs , & ys ) ;
82 print ( w . head ) ;
83 print ( w . tail ) ;
84 }
85
86 // 8) Diagnostics-friendly failure (commented):
87 // fn bad_pick(a: &T, b: &T) -> &T {
88 // if cond { return a; } else { return b; }
89 // // Error: ambiguous return lifetime. Add `as A` to a (or b) and return `&A T`.
90 // }

Lifetimes in Data Structures

  • Reference fields implicitly carry region parameters. struct Node { data: &T } elaborates to struct Node<ρ, T> { data: &ρ T } .
  • Construction instantiates ρ from the argument’s region. Error if struct outlives the referenced data.

Place Overlap (I3)

  • p overlaps p .
  • p overlaps p.f . Distinct fields disjoint.
  • arr[i] vs arr[j] : disjoint if indices are distinct compile-time constants; else conservative overlap.
  • Library intrinsics like split_at_mut provide 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 await unless the origin outlives the entire future. Practically: locals cannot cross await ; only borrows from captured fields of the async task can.
  • Phase 2: Desugar async to 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

Example
0 let s : string = "hi" ;
1 let r = & s ; // borrow starts
2 print ( r ) ; // last use of r
3 var t = s ; // OK allowed: r’s region ended at last use (lexically-free)

L1 — Loan Creation

Example
0 var x : int = 0 ;
1 let r = & x ; // L(x, shared)
2 let m = & mut x ; // Not OK conflict later under A1, but creation itself establishes L(x, unique)

L2 — Loan Propagation

Example
0 fn id_ref ( p : & int ) - > & int { p }
1 let x : int = 1 ;
2 let r1 = & x ; // L(x, shared)
3 let r2 = id_ref ( r1 ) ; // loan propagates to r2 until last use
4 print ( r2 ) ; // OK!

A1 — Unique Exclusivity

Example
0 var v : int = 0 ;
1 let m = & mut v ; // L(v, unique)
2 let r = & v ; // Not OK! A1: unique excludes any concurrent shared borrow

A2 — Shared Read‑Only

Example
0 var n : int = 0 ;
1 let a = & n ; // L(n, shared)
2 let b = & n ; // another shared loan
3 print ( * a + * b ) ; // OK! reads allowed
4 n = 5 ; // Not OK! A2: write while shared loans alive

I1 — Write Invalidates

Example
0 var s : string = "hi" ;
1 let r = & s ; // L(s, shared)
2 print ( r ) ;
3 s . push ( " ! " ) ; // Not OK! I1: write to s while shared loan alive (if r not yet last‑used)

I2 — Move Invalidates

Example
0 var s : string = "a" ;
1 let r = & s ; // L(s, shared)
2 var t = s ; // Not OK! I2: move invalidates loans; r would dangle

I3 — Overlap/Fields

Example
0 struct P { x : int , y : int }
1 var p : P = P { x : 1 , y : 2 } ;
2 let mx = & mut p . x ; // L(p.x, unique)
3 let my = & mut p . y ; // OK, disjoint fields: allowed
4
5 var arr : [ int ; 3 ] = [ 0 , 1 , 2 ] ;
6 let a = & mut arr [ 0 ] ;
7 let b = & mut arr [ 1 ] ; // not OK, if indices distinct constants
8 let i = 0 ; let j = read_index ( ) ;
9 let c = & mut arr [ i ] ;
10 let d = & mut arr [ j ] ; // Not OK conservatively, may overlap unless proven disjoint (use split helpers)

U1 — Use Requires Alive

Example
0 let s : string = "hi" ;
1 let r = & s ; // L(s, shared)
2 print ( r ) ; // OK! use while alive
3 // after last use r ends; any further use would be not OK (dead borrow)

U2 — No After‑Free

Example
0 fn bad ( ) {
1 let s : string = "x" ;
2 let r = & s ;
3 drop ( s ) ; // base place dead
4 print ( r ) ; // Not OK: U2: use after base death
5 }

RB1 — Shared from Unique

Example
0 var v : int = 0 ;
1 let mu = & mut v ; // L(v, unique)
2
3 // Shared reborrow of the same place (auto-deref from &mut int to int)
4 let rs : & int = & mu ; // creates L(v, shared)
5
6 print ( rs ) ;
7
8 // Write through the unique borrow (auto-deref on assignment)
9 mu = 3 ; // must end shared loan before writing via `mu`

RB2 — Unique from Unique (optional v1)

Example
0 var v : int = 0 ;
1 let mu1 = & mut v ;
2 let mu2 = & mut mu1 ; // unique reborrow of v; allowed only if no overlapping use via mu1

E1 — No Escaping Borrows

Example
0 fn leak_ref ( ) - > & int {
1 let x : int = 1 ;
2 return & x ; // Not OK! Escapes to caller; Not OK, dies at function end
3 }
4
5 fn head ( s : & string ) - > & char {
6 return & s [ 0 ] ;
7 } // OK! Summary ties return to arg

M1 — Binding Mutability

Example
0 let x : int = 0 ;
1 let rx = & mut x ; // Not OK: M1: &mut requires mutable base
2 var y : int = 0 ;
3 let ry = & mut y ; // OK

M2 — No Shared Mutability Without Atomics

Example
0 shared var n : int = 0 ; // shared location
1 let r1 = & n ; let r2 = & n ; // shared borrows
2 n = n + 1 ; // Not OK concurrent write without atomic discipline
3 atomic var a : int = 0 ; // with atomic, writes are allowed by policy

T1 — Temporaries

Example
0 print ( & ( make_string ( ) ) ) ; // OK temporary lives through call; reference dies at last use
1 let r = & ( make_string ( ) ) ;
2 print ( r ) ; // Not OK if r would outlive the temporary’s last use

T2 — Branches/Loops/Match

Example
0 let s : string = "x" ;
1 let r = & s ;
2 if cond { print ( r ) ; } else { print ( r ) ; }
3 // r is used on both paths; live set at join = intersection ⇒ still alive until after the if
4 var t = s ; // not OK must occur after r’s last use on all paths

F1 — FFI Preconditions

Example
0 ffi puts : ( & char ) - > int = . . . ;
1 let s : string = "hi" ;
2 let r = & s [ 0 ] ;
3 puts ( r ) ; // OK if FFI summary: does not retain; Not OK if it may retain (escape)

F2 — FFI Postconditions

Example
0 ffi c_strchr : ( & char , int ) - > & char = . . . ; // trusted: result aliases input
1
2 fn first_a ( s : & string ) - > & char {
3 c_strchr ( & s [ 0 ] , 'a' ) // OK allowed only because summary ties result to arg region
4 }

RFT1 — Refinement Well‑Formedness

Example
0 let x : int = 5 ;
1 let y : { z : & int where initialized ( z ) } = { z : & x } ; // OK predicate references lifetime‑relevant property

RFT2 — Discharge at Use Sites

Example
0 fn show ( p : { z : & int where not_escaped ( z ) } ) - > void {
1 print ( p . z ) ; // OK! checker discharges `not_escaped` from current loan facts
2 }

inter‑Procedural Summary (Elision)

Example
0 fn get_first ( a : & [ int ] ) - > & int {
1 return & a [ 0 ] ;
2 }
3
4 // elaborated internally: for<ρ> fn(&ρ [int]) -> &ρ int
5 // caller instantiates ρ from its argument; return tied to same ρ

Structs with Reference Fields (Implicit Regions)

Example
0 struct Node ( T ) { data : & T } // elaborates to Node<ρ, T>
1
2 fn demo ( ) - > void {
3 let x : int = 1 ;
4 var n : Node ( int ) = Node { data : & x } ; // OK n: Node<ρx, int>
5 }
6
7 fn bad ( ) - > void {
8 var n : Node ( int ) ;
9
10 {
11 let x : int = 1 ;
12 n = Node { data : & x } ; // Not OK n outlives x ⇒ region check fails
13 }
14 }

Async Phase 1 — No Unique Loans Across Await

Example
0 async fn step ( mut v : & int ) - > void {
1 let m = v ; // borrow unique via parameter
2 await tick ( ) ; // Not OK, unique loan across await in phase 1
3 }

Closures — Capture Classification

void { move x }; // by‑value move ⇒ FnOnce" lang="aela" title="Example" id="de7b5ce457c5">
Example
0 var n : int = 0 ;
1 let c1 = fn ( ) - > void { print ( & n ) ; } ; // shared borrow capture ⇒ Fn
2 let c2 = fn ( ) - > void { n = n + 1 ; } ; // unique borrow capture ⇒ FnMut
3 let x : string = "hi" ;
4 let c3 = fn ( ) - > void { move x } ; // by‑value move ⇒ FnOnce

C/C++ Harmony

Most languages' safty stops at the FFI boundary. But by automating the creation of safe FFI boundaries and embedding more of the C/C++ code's contracts directly into Aela's type system. Instead of just marking a boundary as unsafe and leaving the safety burden entirely on the developer, Aela can actively assist in verifying the C/C++ side of the interaction.

Automatically Generate "Diplomatic" Wrappers

Instead of manually writing write unsafe blocks and wrapper functions (This is tedious and error-prone). Aela can automate this.

Aela Compile can parse C/C++ header files (.h, .hpp) and automatically generatee the FFI bindings and a safe, idiomatic Aela wrapper. It's much more sophisticated than a simple binding generator.

Contract Inference

The tool can analyze C++ code for clues about contracts.

For instance, it can interpret a gsl::not_null or _Nonnull annotation as a non-nullable reference in Aela, automatically adding the necessary runtime checks.

Resource Management

If a C function create_foo() returns a pointer that must be freed with destroy_foo() , the generator can automatically create a smart pointer or RAII object in Aela that calls destroy_foo() on scope exit. This eliminates a huge class of resource leak bugs.

Error Handling

It can translate C-style error codes e.g., return -1; or errno = EINVAL; into Aela's native error-handling mechanism, like Result types or exceptions.

This moves the burden from the developer having to manually ensure safety to the too providing a verifiably safe starting point.

A Type System for C/C++ Interop

Aela's type system can understand C/C++'s quirks better. It encodes invariants about C pointers and memory directly into the types.

Sized Pointers

Instead of a raw pointer, Aela has types like Pointer(T, size_t N), which represent a pointer to a buffer of N elements. This allows the compiler to enforce bounds checking at the FFI boundary.

Nullability and Ownership

Explicitly differentiate between Pointer(T) (nullable) and Reference(T) (non-nullable). And types that encode ownership semantics like OwnedPointer(T) (must be freed) vs. BorrowedPointer(T) (must not be freed).

Tainted Data

Data coming from C/C++ is considered "tainted" by the type system. It needs to be explicitly validated (e.g., checking a string for valid UTF-8, ensuring a value is within an expected range) before it can be used in the safe context.

Integrate Static and Dynamic Analysis

Since Aela is written in C, it can integrate powerful C/C++ analysis tools directly into the build process.

Clang's Analyzers

Aela uses libraries from the Clang/LLVM project to perform static analysis on the C/C++ code. During compilation, it automatically invokes analyzers to check for things like null pointer dereferences, use-after-free, or buffer overflows in the C code being called, and flag a warning or error if a potential issue is found.

Boundary Sanitization

A "debug mode" for FFI that injects runtime checks at the boundary.

When your code calls a C function, it could automatically add canaries or check buffer boundaries. When C code calls back into Aela, it can validate incoming pointers and data. This is similar to running with AddressSanitizer (ASan), but it's focused specifically on the FFI-boundary risks.

By taking these steps, Aela doesn't just stop its safety guarantees at the boundary. It actively polices that boundary, making brownfield integration significantly safer and more robust than the manual, high-discipline approach required by other languages.

FFI

The Foreign Function Interface (FFI) provides a mechanism for Aela code to call functions written in other programming languages, specifically C. This allows you to leverage existing C libraries, write performance-critical code in a lower-level language, or interact directly with the underlying operating system.

The core of Aela's FFI is the ffi definition, which declares a external C functions and their Aela type signatures or varibales and their types. The Aela compiler and runtime use these declarations to handle the "marshalling" of data—the process of converting data between Aela's internal representations and the C Application Binary Interface (ABI).

Declaring an FFI type

You declare a C function or C variable using the ffi keyword.

Example
0 ffi foo = fn ( string ) - > void ;
1 ffi bar = u32 ;

ABI Contract

A stable C ABI ( ae_c_abi.h ) defines the contract. It specifies the C-side string representation: typedef struct { char* ptr; int64_t len; } AeString;

Compiler Type Mapping

The Aela compiler's types.c maps the language's string type to an LLVM struct with an identical memory layout: %aela.string = type { i8*, i64 } .

Passing Convention

Strings are passed to C functions BY VALUE.

  • Aela code generates: call void @c_function(%aela.string %my_string) .
  • C code receives: void c_function(AeString my_string) .

Safety & Ownership

  • This pass-by-value convention is a "defensive" design.
  • The C function gets a copy of the string descriptor, preventing it
  • from modifying the original string's length or pointer in Aela's
  • memory.
  • Aela's runtime retains ownership of the underlying character buffer
  • ( char* ). The AeString struct is just a temporary, non-owning view.
Example
0 ffi = ; . . .

: The exact name of the function as it is defined in the C source code.

: The Aela type signature for the C function. This signature is the crucial contract that tells the Aela compiler how to call the C function correctly.

Example

Let's look at the stdio example from the standard library:

Example
0 ffi ae_stdout_write = fn ( string ) - > void ;

This code does the following:

It declares that there is an external C function named ae_stdout_write.

It specifies that from the Aela side, this function should be treated as one that accepts a single Aela string and returns void.

To call this function, you use the standard module access syntax:

Example
0 ae_stdout_write ( "Hello from C ! " ) ;

The Aela-C ABI and Data Marshalling When an FFI call occurs, the Aela compiler generates "glue" code to translate Aela types into types that C understands. This mapping follows a specific Application Binary Interface (ABI).

Primitive Types

Most Aela primitive types map directly to their C equivalents.

Aela Type C Type
i8, u8 int8_t, uint8_t
i16, u16 int16_t, uint16_t
i32, u32 int32_t, uint32_t
i64, u64 int64_t, uint64_t
f32 float
f64 double
bool bool (or \_Bool)
char uint32_t (UTF-32)
void void

Strings

The Aela string is a "fat pointer" struct containing a pointer to the data and a length. C, however, typically works with null-terminated char* strings.

Aela to C: When you pass an Aela string to an FFI function, the compiler automatically extracts the internal ptr and passes it as a const char* to the C function. The string data is guaranteed to be null-terminated, so standard C string functions can operate on it safely.

Aela's Internal runtime representation
0 struct string {
1 ptr : ptr , // Pointer to UTF-8 data
2 len : i64 // Length of the string
3 }

FFI Call:

The Aela Code
0 ae_stdout_write ( "Hello" ) ;

The C function receives a standard C string.

The C Implementation
0 void ae_stdout_write ( const char * message ) {
1 printf ( " % s" , message ) ;
2 }

Structs, Arrays, and Closures (Complex Types)

Complex aggregate types like structs, arrays, and closures cannot be passed directly by value to C functions. The ABI for these types is simple: you pass a pointer.

Aela to C: When passing a complex type, Aela passes a pointer to the object's memory layout. Your C code receives an opaque pointer (void\*) to this data. It is your responsibility in C to know the memory layout of the Aela type and cast the pointer accordingly to access its fields.

This is an advanced use case and requires careful handling to avoid memory corruption. You must ensure that the struct definition in your C code exactly matches the memory layout of the Aela struct.

Often you end up with an opaque strct in Aela. These can not have methods or properties.

An Opaque Struct
0 struct StringBuilder ;
1 ``
2
3 ## Variadic Functions (...args)
4
5 Variadic arguments are not directly passed through the FFI boundary . The . . . args
6 feature is part of the Aela language and its calling convention , not the C ABI .
7
8 As seen in the io . print example , you must handle variadic arguments within your
9 Aela code and call the FFI function with a concrete , non - variadic signature .
10
11 `` `example
12 // The public-facing Aela function is variadic.
13
14 export fn print ( formatString : string , . . . args ) - > void {
15 stdio : : ae_stdout_write ( std : : format ( formatString , . . . args ) ) ;
16 }

This design provides a safe and clear boundary. The complex, type-safe variadic handling happens within the Aela runtime, while the FFI call itself remains a simple, direct translation of the string argument to a char*.

Linking C Code To make your C functions available to the Aela compiler, you must compile them into an object file (.o) or a library (.a, .so, .dylib) and include it during the final linking step.

The Aela driver will eventually provide flags to specify these external object files. For now, you would typically use a command like clang to link the Aela-generated object file with your C object file.

  1. Compile your Aela code aec your_program.ae -o your_program.o
  1. Compile your C code clang -c my_ffi_functions.c -o my_ffi_functions.o
  1. Link them together clang your_program.o my_ffi_functions.o -o
  2. final_executable

This process creates the final executable where the Aela runtime can find and call your C functions.

Memory & Mutability Model

1. Default Allocation Semantics

  • All values start on the stack by default.
  • Stack allocations are:
  • Fast
  • Scoped
  • Owned directly by the binding.
Example
0 let foo : Foo = { num : 0 } ; // Foo is stack-allocated

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.

Example
0 let x : & T = new { . . . val } ; // Allocate on the heap
1 let x : & T = new shared { . . . val } ; // Single-threaded, mutable
2 let x : & T = new shared atomic { . . . val } ; // Thread-safe atomic
3 var x : & T = new weak { . . . val } ; // Mutable weak reference

Behavior

  • 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)
  • There is no `mut` keyword
  • Mutability is not encoded in types, structs, or fields

Example

Example
0 let a : & Foo = new shared { . . . foo } ; // Immutable heap reference
1 var b : & Foo = new shared { . . . foo } ; // Mutable heap reference

Attempting to mutate a let -bound reference results in a compile-time error .

4. Weak References & Cycle Prevention [NOT IMPLEMENTED]

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'

Example
0 if let strong_ref = weak_ref {
1 // This block only runs if the object is still alive.
2 // 'strong_ref' is a new, temporary strong reference of type &Foo.
3 strong_ref . do_something ( ) ;
4 }

5. Copying Stack Values

  • Heap allocation requires copying the stack value:
Example
0 new shared { . . . foo } // `{ ...foo }` performs a field-wise copy
  • 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.
a copy of the number 10 is passed. // bar(stack_str); -> a copy of the {ptr, i64} struct is passed." lang="example" title="- Stack Allocated" id="d4bdebbe552a">
- Stack Allocated
0 let stack_int : i32 = 10 ; // The variable 'stack_int' IS the number 10.
1 let stack_str : string = "hello" ; // The variable 'stack_str' IS the {ptr, i64} struct.
2
3 // When calling a function:
4 // foo(stack_int); -> a copy of the number 10 is passed.
5 // bar(stack_str); -> a copy of the {ptr, i64} struct is passed.

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.
- Heap allocated via new
0 let heap_int : i32 = new 42 ; // 'heap_int' is a POINTER to a box containing 42.
1 let heap_obj : MyStruct = new { } ; // 'heap_obj' is a POINTER to a box containing a MyStruct.
2 let heap_arr : u8 [ ] = new [ 1 , 2 , 3 ] ; // 'heap_arr' is a POINTER to a box for the array data.
3
4 // When calling a function:
5 // foo(heap_int); -> a copy of the POINTER is passed. The heap data is not touched.

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. The env_ptr points 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.
- Special Case
0 let captured_var = new "text" ;
1
2 let my_closure = fn ( ) {
3 print ( captured_var ) ; // Captures the pointer 'captured_var'.
4 } ;
5
6 // 'my_closure' is a {func_ptr, env_ptr} struct.
7 // 'env_ptr' points to a heap box which contains a copy of the 'captured_var' pointer.

GUI

Aela provides a cross platform GUI library that works on MacOS, iOS, Windows, Linux, & Android.

Install

Install the module into your dependencies.
0 aec install stablestate / ui
Your index.json will then have the dependency.
0 {
1 "name" : "my - program" ,
2 "version" : "0 . 1 . 0" ,
3 "entry" : "main . ae" ,
4 "output" : "build / app" ,
5 "include" : [ "src /**/ * . ae" ] ,
6 "exclude" : [ "tests / * * " ] ,
7 "dependencies" : [
8 {
9 "name" : "ui" ,
10 "url" : "https : / / github . com / stablestate / ui"
11 }
12 ]
13 }

Example

int { fn App () -> &View { let onclick: EventCallback = fn (event: &Event) -> void { // do something }; return new View { children: [ WebView { url: "https://news.ycombinator.com" }, Button { label: "save" }, Button { label: "cancel", onclick } ], onclick }; } let app: &View = App(); let loop: &io::AeEventLoop = io::getLoop(); initLoop(loop); let windowOptions: &WindowOptions = new { title: "Aela UI Test", width: 800.0, height: 600.0 }; let window: &Window = new Window(windowOptions); window.show(); window.render(app); runLoop(); return 0; }" lang="ae" title="Getting started" id="5286b485edc61">
Getting started
0 import {
1 initLoop ,
2 runLoop ,
3 WindowOptions ,
4 Window
5 } from "ui" ;
6
7 async fn main ( args : string [ ] ) - > int {
8 fn App ( ) - > & View {
9 let onclick : EventCallback = fn ( event : & Event ) - > void {
10 // do something
11 } ;
12
13 return new View {
14 children : [
15 WebView { url : "https : / / news . ycombinator . com" } ,
16 Button { label : "save" } ,
17 Button { label : "cancel" , onclick }
18 ] ,
19 onclick
20 } ;
21 }
22
23 let app : & View = App ( ) ;
24
25 let loop : & io : : AeEventLoop = io : : getLoop ( ) ;
26 initLoop ( loop ) ;
27
28 let windowOptions : & WindowOptions = new {
29 title : "Aela UI Test" ,
30 width : 800 . 0 ,
31 height : 600 . 0
32 } ;
33
34 let window : & Window = new Window ( windowOptions ) ;
35
36 window . show ( ) ;
37 window . render ( app ) ;
38
39 runLoop ( ) ;
40
41 return 0 ;
42 }

Why 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.

Formal Grammar Spec

Grammar
0 ( * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = )
1 ( Aela Language Grammar 0 . 0 . 6 )
2 ( Finalized : 2025 - 09 - 06 )
3 ( = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = * )
4
5 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
6 ( PROGRAM )
7 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
8
9 Program : : = { TopLevelDeclaration } EOF
10
11 TopLevelDeclaration : : =
12 ImportStatement
13 | ReExportDeclaration
14 | [ KW_EXPORT ] (
15 FfiDeclaration
16 | VarDeclaration
17 | FunctionDeclaration
18 | StructDeclaration
19 | ImplBlock
20 | EnumDeclaration
21 | TypeAliasDeclaration
22 | SystemDeclaration
23 )
24
25 TypeAliasDeclaration : : = KW_TYPE IDENTIFIER '=' Type ';'
26
27 ReExportDeclaration : : =
28 KW_EXPORT ( NamedImport | IDENTIFIER )
29 KW_FROM STRING_LITERAL ';'
30
31 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
32 ( IMPORTS )
33 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
34
35 ImportStatement : : =
36 KW_IMPORT ( NamedImport | IDENTIFIER )
37 KW_FROM STRING_LITERAL ';'
38
39 NamedImport : : = '{' [ ImportSpecifier { ',' ImportSpecifier } [ ',' ] ] '}'
40 ImportSpecifier : : = IDENTIFIER [ ':' IDENTIFIER ]
41
42 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
43 ( FFI ( Foreign Function Interface ) )
44 ( - Contracts are compile - time enforced to be UB - free )
45 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
46
47 FfiDeclaration : : = KW_FFI IDENTIFIER '=' Type ';'
48
49 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
50 ( DECLARATIONS )
51 ( - var is mutable , let is immutable )
52 ( - aliases are borrow - checked by the analyzer )
53 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
54
55 VarDeclaration : : = ( KW_VAR | KW_LET ) IDENTIFIER ':' Type
56 [ '=' Expression ] ';'
57
58 StructDeclaration : : = KW_STRUCT IDENTIFIER [ GenericParameters ] ( '{' { StructFieldDeclaration } '}' | ';' )
59
60 StructFieldDeclaration : : =
61 ( IDENTIFIER ':' Type ';' )
62 | ( '...' IDENTIFIER ';' )
63
64 FnModifiers : : =
65 [ ( KW_THREAD [ KW_PURE ] ) // thread; pure+thread allowed
66 | ( KW_PURE [ KW_THREAD ] ) // pure; optional thread
67 | KW_ASYNC // async alone (no pure)
68 ]
69
70 ImplBlock : : = KW_IMPL Type [ GenericParameters ] '{' { FunctionDeclaration | InvariantDeclaration } '}'
71
72 FunctionDeclaration : : =
73 FnModifiers KW_FN IDENTIFIER [ GenericParameters ]
74 '(' [ FunctionParameters ] ')' '->' Type ( Block | ';' )
75
76 EnumDeclaration : : = KW_ENUM IDENTIFIER [ GenericParameters ]
77 '{' [ EnumVariant { ',' EnumVariant } [ ',' ] ] '}'
78
79 EnumVariant : : = IDENTIFIER [ '(' Type { ',' Type } ')' ]
80
81 GenericParameters : : = '(' IDENTIFIER { ',' IDENTIFIER } ')'
82
83 SystemDeclaration : : = KW_SYSTEM IDENTIFIER '{'
84 { ActionDeclaration
85 | InvariantDeclaration
86 | PropertyDeclaration
87 }
88 '}'
89
90 ActionDeclaration : : = KW_ACTION IDENTIFIER
91 '(' [ FunctionParameters ] ')'
92 [ RequiresClause ]
93 [ EnsuresClause ]
94 Block
95
96 RequiresClause : : = KW_REQUIRES Expression
97 EnsuresClause : : = KW_ENSURES Expression
98
99 InvariantDeclaration : : = KW_INVARIANT IDENTIFIER ':' Expression
100 PropertyDeclaration : : = KW_PROPERTY IDENTIFIER ':' TemporalExpression
101
102 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
103 ( TYPES )
104 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
105
106 Type : : = [ '&' ] PostfixType
107
108 PostfixType : : = SimpleType { ArrayTypeModifier | TypeApplication | '?' }
109
110 MapType : : = KW_MAP '(' Type ',' Type ')'
111
112 SimpleType : : = PrimitiveType
113 | KW_VOID
114 | FunctionTypeSignature
115 | PathExpression
116 | MapType
117 | RefinementType
118 | '(' Type ')'
119
120 FunctionTypeSignature : : =
121 FnModifiers KW_FN '(' [ FunctionTypeParameters ] ')' '->' Type
122
123 RefinementType : : = '{' IDENTIFIER ':' Type KW_WHERE Expression '}'
124
125 PrimitiveType : : = KW_U8 | KW_I8 | KW_U16 | KW_I16 | KW_U32 | KW_I32
126 | KW_U64 | KW_I64 | KW_F32 | KW_F64 | KW_BOOL
127 | KW_CHAR | KW_STRING | KW_INT
128
129 TypeApplication : : = '(' [ Type { ',' Type } ] ')'
130
131 FunctionParameters : : = Parameter { ',' Parameter }
132 Parameter : : = [ '...' ] [ KW_MUT ] IDENTIFIER ':' Type
133 FunctionTypeParameters : : = FunctionTypeParameter { ',' FunctionTypeParameter }
134 FunctionTypeParameter : : = [ KW_MUT ] Type
135 ArrayTypeModifier : : = '[' [ Expression ] ']'
136
137 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
138 ( COMMENTS )
139 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
140
141 ( * A single - line comment starts with // and continues to the end of the line )
142 SingleLineComment : : = '//' { ~('\n' | '\r') }
143
144 ( * A multi - line comment starts with /* and ends with */ )
145 MultiLineComment : : = '/*' { . } '*/'
146
147 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
148 ( STATEMENTS ( Unambiguous ) )
149 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
150
151 Statement : : = MatchedStatement | UnmatchedStatement
152
153 MatchedStatement : : =
154 KW_IF '(' Expression ')' MatchedStatement KW_ELSE MatchedStatement
155 | KW_IF KW_LET Pattern '=' Expression Block KW_ELSE MatchedStatement
156 | Block
157 | KW_RETURN [ Expression ] ';'
158 | BreakStatement
159 | ContinueStatement
160 | WhileStatement
161 | ForStatement
162 | MatchStatement
163 | ExpressionStatement
164 | VarDeclaration
165 | FunctionDeclaration
166 | ';'
167
168 UnmatchedStatement : : =
169 KW_IF '(' Expression ')' Statement
170 | KW_IF '(' Expression ')' MatchedStatement KW_ELSE UnmatchedStatement
171 | KW_IF KW_LET Pattern '=' Expression Block
172 | KW_IF KW_LET Pattern '=' Expression Block KW_ELSE UnmatchedStatement
173
174 Block : : = '{' { Statement } '}'
175 ExpressionStatement : : = Expression ';'
176 BreakStatement : : = KW_BREAK ';'
177 ContinueStatement : : = KW_CONTINUE ';'
178
179 WhileStatement : : = KW_WHILE '(' Expression ')' Statement
180
181 ForStatement : : = KW_FOR '(' ForDeclaratorList KW_IN Expression ')' Statement
182 ForDeclaratorList : : = ForDeclarator { ',' ForDeclarator }
183 ForDeclarator : : = ( KW_LET | KW_VAR ) IDENTIFIER ':' Type
184
185 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
186 ( MATCH ( Mandatory Exhaustive ) )
187 ( - Expression form for atomic initialization . )
188 ( - Statement form for control flow . )
189 ( - Guards , @ - bindings , and nesting are disallowed . )
190 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
191
192 MatchStatement : : = KW_MATCH '(' Expression ')' '{'
193 [ MatchStmtArm { ',' MatchStmtArm } [ ',' ] ]
194 '}'
195
196 MatchStmtArm : : = MatchArmPattern '=>' ( Block | ';' )
197
198 MatchArmPattern : : = Pattern { '|' Pattern }
199
200 Pattern : : =
201 LiteralPattern
202 | IDENTIFIER // binding
203 | '_' // wildcard
204 | PathExpression [ '(' [ PatternList ] ')' ] // unit/tuple-variant
205 | TuplePattern
206 | StructPattern
207
208 TuplePattern : : = '(' [ PatternList [ ',' ] ] ')'
209
210 StructPattern : : = '{' [ StructFieldPat { ',' StructFieldPat } [ ',' ] ] '}'
211 StructFieldPat : : = IDENTIFIER ( ':' Pattern ) ? | '...' IDENTIFIER
212
213 PatternList : : = Pattern { ',' Pattern }
214
215 RangePattern : : =
216 INT_LITERAL ( '..' | '..=' ) INT_LITERAL
217 | CHAR_LITERAL ( '..' | '..=' ) CHAR_LITERAL
218
219 LiteralPattern : : = INT_LITERAL | STRING_LITERAL | CHAR_LITERAL | KW_TRUE | KW_FALSE | RangePattern
220
221 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
222 ( EXPRESSIONS ( Pratt Parser Aligned ) )
223 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
224
225 Expression : : = AssignmentExpression
226
227 AssignmentExpression : : = CoalescingExpression [ '=' AssignmentExpression ]
228
229 CoalescingExpression : : = LogicalOrExpression { '??' LogicalOrExpression }
230
231 LogicalOrExpression : : = LogicalAndExpression { '||' LogicalAndExpression }
232
233 LogicalAndExpression : : = BitwiseOrExpression { '&&' BitwiseOrExpression }
234
235 BitwiseOrExpression : : = BitwiseXorExpression { '|' BitwiseXorExpression }
236
237 BitwiseXorExpression : : = BitwiseAndExpression { '^' BitwiseAndExpression }
238
239 BitwiseAndExpression : : = ShiftExpression { '&' ShiftExpression }
240
241 ShiftExpression : : = EqualityExpression { ( '<<' | '>>' ) EqualityExpression }
242
243 EqualityExpression : : = ComparisonExpression { ( '==' | '!=' ) ComparisonExpression }
244
245 ComparisonExpression : : = AdditiveExpression { ( '<' | '<=' | '>' | '>=' ) AdditiveExpression }
246
247 AdditiveExpression : : = MultiplicativeExpression { ( '+' | '-' ) MultiplicativeExpression }
248
249 MultiplicativeExpression : : = CastExpression { ( '*' | '/' | '%' ) CastExpression }
250
251 CastExpression : : = UnaryExpression { KW_AS Type }
252
253 UnaryExpression : : =
254 ( KW_AWAIT | '-' | '!' | '&' | '~' ) UnaryExpression
255 | PostfixExpression
256
257 PostfixExpression : : =
258 PrimaryExpression {
259 '(' [ ArgumentList ] ')'
260 | '[' Expression ']'
261 | ( '.' | '?.' ) IDENTIFIER
262 }
263
264 PrimaryExpression : : =
265 PathExpression
266 | Literal
267 | '(' Expression ')'
268 | ArrayLiteral
269 | NamedStructLiteral
270 | AnonymousStructLiteral
271 | FunctionExpression
272 | NewExpression
273 | MatchExpression
274
275 MatchExpression : : = KW_MATCH '(' Expression ')' '{'
276 [ MatchExprArm { ',' MatchExprArm } [ ',' ] ]
277 '}'
278
279 MatchExprArm : : = MatchArmPattern '=>' Expression
280
281 PathExpression : : = IDENTIFIER { '::' IDENTIFIER }
282
283 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
284 ( TEMPORAL EXPRESSIONS )
285 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
286
287 TemporalExpression : : =
288 KW_ALWAYS Expression
289 | KW_EVENTUALLY Expression
290 | KW_NEXT Expression
291 | Expression KW_UNTIL Expression
292 | Expression KW_RELEASE Expression
293 | KW_FORALL IDENTIFIER KW_IN Expression ':' Expression
294 | KW_EXISTS IDENTIFIER KW_IN Expression ':' Expression
295 | Expression
296
297 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
298 ( LITERALS & HELPER RULES )
299 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
300
301 Literal : : =
302 INT_LITERAL | FLOAT_LITERAL | STRING_LITERAL | STRING_MULTILINE
303 | CHAR_LITERAL | KW_TRUE | KW_FALSE
304
305 ArrayLiteral : : = '[' [ ArgumentList ] ']'
306 NamedStructLiteral : : = PathExpression StructLiteralBody
307 AnonymousStructLiteral : : = StructLiteralBody
308 StructLiteralBody : : = '{' [ StructElement { ',' StructElement } [ ',' ] ] '}'
309 StructElement : : = ( IDENTIFIER ':' Expression ) | IDENTIFIER | '...' Expression
310
311 ArgumentList : : = CallArgument { ',' CallArgument }
312 CallArgument : : = [ '...' ] [ KW_MUT ] Expression
313
314 FunctionExpression : : = FnModifiers KW_FN '(' [ FunctionParameters ] ')' [ '->' Type ] Block
315
316 NewExpression : : = KW_NEW PrimaryExpression
317
318 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
319 ( TERMINALS ( TOKENS ) )
320 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
321
322 IDENTIFIER
323 INT_LITERAL , FLOAT_LITERAL , STRING_LITERAL , STRING_MULTILINE , CHAR_LITERAL
324
325 ( * Keywords : * )
326 KW_LET , KW_VAR , KW_FN , KW_IF , KW_IN , KW_ELSE , KW_WHILE , KW_FOR ,
327 KW_RETURN , KW_BREAK , KW_CONTINUE , KW_WHERE ,
328 KW_ASYNC , KW_AWAIT , KW_AS , KW_STRUCT , KW_IMPL , KW_THREAD ,
329 KW_ENUM , KW_MATCH , KW_TYPE , KW_VOID , KW_INT ,
330 KW_U8 , KW_I8 , KW_U16 , KW_I16 , KW_U32 , KW_I32 , KW_U64 , KW_I64 ,
331 KW_F32 , KW_F64 , KW_BOOL , KW_CHAR , KW_STRING , KW_TRUE , KW_FALSE ,
332 KW_IMPORT , KW_EXPORT , KW_FROM , KW_FFI , KW_MAP ,
333
334 ( * No shared mutability without atomics ! * )
335 KW_NEW , KW_SHARED , KW_ATOMIC , KW_WEAK , KW_MUT ,
336
337 KW_SYSTEM , KW_ACTION , KW_REQUIRES , KW_ENSURES ,
338 KW_INVARIANT , KW_PROPERTY ,
339 KW_FORALL , KW_EXISTS , KW_ALWAYS , KW_EVENTUALLY , KW_NEXT , KW_UNTIL ,
340 KW_RELEASE , KW_PURE
341
342 ( * Operators and Delimiters : Arithmetic Wraps )
343 '=' , '+' , '-' , '*' , '/' , '%' , '&' , '==' , '!=' , '<' , '<=' , '>' , '>=' ,
344 '!' , '&&' , '||' , '|' , '^' , '~' , '<<' , '>>' , '(' , ')' , '{' , '}' , '[' , ']' ,
345 ',' , ';' , '.' , ':' , '::' , '?' , '?.' , '??' , '...' , '..=' , '..' , '->' , '_' , '=>'
346
347 EOF