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="ff3661e49117c">
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 daemonize

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 This will enable you to set watches on directories and enable incremental builds, and maintain stateful sessions.
Why This is ideal or developers who prefer working in the terminal, or for anyone using AI tooling.

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.

Types

Quick Reference

Category Surface Syntax Examples Notes
Booleans bool true , false Logical values.
Integers (fixed width) u8 i8 u16 i16 u32 i32 u64 i64 let n: i32 = 42; Signed/unsigned bit‑widths.
Integer (platform) int let n: int = 1; Implementation/default integer.
Floats f32 f64 let x: f64 = 3.14; IEEE‑754.
Char char 'A' Unicode scalar.
String string "hello" Immutable text; multi‑line strings supported.
Void / Unit void fn foo () -> void {} Functions that return nothing.
Time Types Instant , Duration let i: Instant = std::now(); Specialized i64 types for time measurement. Instant is a time point; Duration is a span.
Optional T? User? , i32? null / none allowed; use match , ?. , ?? .
None (value) null / none The distinguished empty value; typed as T? .
Reference (borrow) &T &User Borrowed reference. Analyzer enforces aliasing rules.
Arrays / Slices T[] , T[N] i32[] , byte[32] Dynamic slice vs fixed length (compile‑time N ).
Maps map(K, V) map(string, i32) Built‑in map/dictionary.
Function Types fn(params) -> R pure fn(i32)->i32 Modifiers: pure , thread , async (values).
Closures (function value) let f = fn(x:i32)->i32 { return x+1 }; Captures env; typed as fn(...) -> ... .
Structs (nominal) struct Name ... then Name(...) Point , Option(T) User‑defined records; may be parameterized.
Enums (sum types) enum Name { ... } Result(T,E) Tagged variants; pattern‑matched.
Modules (qualified name) pkg::Type Namespacing; module itself has a type internally.
Futures Future(T) returned by async fn Produced by async functions; await yields T .

Postfix builders: you can apply ? , array [...] , and type application ( ... ) to base types where applicable.

Nominal & Parameterized Types

Structs and enums define named (nominal) types. They may take type parameters and, as the language evolves, const parameters . Use ordinary type application:

Example
0 struct Pair ( T , U ) { first : T ; second : U ; }
1 let p : Pair ( i32 , string ) = { first : 1 , second : "hi" } ;
2
3 enum Option ( T ) { Some ( T ) , None }
4 let x : Option ( i32 ) = Option : : Some ( 3 ) ;

For functions, Aela uses a unified parameter list with a ; split: fn id(T; x:T) -> T (compile‑time params before ; , run‑time after). See the Functions section.

Optionals & None vs. Void

  • T? means maybe a `T` . Use match , ?. , or ?? to handle absence.
  • none / null is the empty value that inhabits optional types.
  • void means no value is returned (a function that completes for effects only). It is not the same as none .
Example
0 fn find_user ( ; id : i32 ) - > User ? { /* ... */ }
1 let u = find_user ( 42 ) ? ? default_user ( ) ;

Arrays & Maps

  • Dynamic slices: T[] — size known at run time; indexing and length available via stdlib.
  • Fixed arrays: T[N] — size is part of the type; N must be compile‑time evaluable.
  • Maps: map(K, V) — associative container; keys and values are regular types.
Example
0 let bytes : u8 [ 32 ] ;
1 let names : string [ ] ;
2 let counts : map ( string , i32 ) ;

References (borrowing)

&T is a borrowed reference to T .

  • Mutable vs immutable is governed by parameter modifiers ( mut ) and aliasing rules enforced by the analyzer (no shared mutability without atomics).
  • Think of &T as a view ; the underlying ownership model is enforced by the compiler.
Example
0 fn length ( ; s : & string ) - > i32 { return std : : length ( s ) ; }

Function & Closure Types

Function types and values share the same shape: fn(params) -> Return with optional modifiers.

Example
0 // Type position
1 let f : pure fn ( i32 ) - > i32 = add_one ;
2
3 // Value (closure) position
4 let g = pure fn ( x : i32 ) - > i32 { return x + 1 ; } ;

Modifiers:

  • pure — no observable side effects; enables stronger reasoning/optimizations.
  • thread — safe to run in a separate thread (may be combined with pure ).
  • async — produces a Future(Return) ; use await to get the value.

Unified parameter list (declarations):

Example
0 fn map ( T , U ; f : fn ( T ) - > U , xs : T [ ] ) - > U [ ] { /* ... */ }

Enums (sum types)

Enums declare a closed set of variants and are exhaustively pattern‑matched.

"err" } }" lang="aela" title="Example" id="e1732e6ca2be3">
Example
0 enum Result ( T , E ) { Ok ( T ) , Err ( E ) }
1
2 fn handle ( T , E ; r : Result ( T , E ) ) - > string {
3 return match ( r ) {
4 Result : : Ok ( x ) = > "ok" ,
5 Result : : Err ( e ) = > "err"
6 }
7 }

Futures & Async

async functions return future values that represent a computation in progress. Conceptually, the type is Future(T) , and await yields T .

Example
0 async fn fetch ( ; url : string ) - > string { /* ... */ }
1
2 fn use_it ( ; ) - > void {
3 let fut = fetch ( " / data" ) ;
4 let s = await fut ; // s: string
5 }

How the Compiler Thinks About Types (High‑Level)

Internally, every type has a kind (primitive, struct, enum, map, array, function, optional, reference, etc.) and a canonical signature string (e.g., "fn(i32)->bool" ). The compiler interns types in a cache so that equality checks are fast pointer comparisons.

  • Compatibility: types_are_compatible(dst, src) determines assignment/call compatibility (more than just raw equality when coercions are allowed).
  • Optional & None: represented distinctly ( TYPE_OPTIONAL around a base, and a special TYPE_NONE ).
  • Closures: carry a function type plus captured environment.
  • Modules: form a namespace; the module itself has a type used by the compiler for resolution.

You normally don’t see these details, but they explain why types print consistently and compare cheaply.

Minimal Grammar Reminders (surface)

  • Map: map(K, V)
  • Optional: T?
  • Reference: &T
  • Array: T[expr] (fixed) or T[] (slice)
  • Function type: pure fn(T1, T2) -> R
  • Type application: Name(T, U)

For advanced material (refinements { x: T where φ } and dependent forms like Vec(T, N) ), see the companion doc.

Best Practices

  • Prefer named aliases for recurring shapes: type Port = { p: i32 where 1024 <= p && p <= 65535 };
  • Use ? sparingly; prefer sum types (e.g., Result ) when you want the caller to handle both cases explicitly.
  • Keep function types pure when possible; it improves composability.
  • Choose fixed arrays ( T[N] ) when size is intrinsic and enables better checking/optimization.

Operators

Precedence Operator(s) Description Associativity
1 (Lowest) = , += , -= , *= , /= Assignment / Compound 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

Literals

Literals are notations for representing fixed values directly in source code. Aela supports a rich set of literals for primitive and aggregate data types.


Numeric Literals

Numeric literals represent number values. They can be integers or floating-point numbers and can include type suffixes and numeric separators for readability.

Integer Literals

Integer literals represent whole numbers. They can be specified in decimal or hexadecimal format.

Decimal: Standard base-10 numbers (e.g., `123`, `42`, `1000`). Hexadecimal: Base-16 numbers, prefixed with 0x (e.g., 0xFF , 0xdeadbeef ). Numeric Separator: * The underscore _ can be used to improve readability in long numbers (e.g., 1_000_000 , 0xDE_AD_BE_EF ).

By default, an integer literal is of type i32 . You can specify a different integer type using a suffix.

Suffix Type Range
i8 8-bit signed −128 to 127
u8 8-bit unsigned 0 to 255
i16 16-bit signed −32,768 to 32,767
u16 16-bit unsigned 0 to 65,535
i32 32-bit signed −2,147,483,648 to 2,147,483,647
u32 32-bit unsigned 0 to 4,294,967,295
i64 64-bit signed −9,223,372,036,854,775,808 …
u64 64-bit unsigned 0 to 18,446,744,073,709,551,615

Example:

Example
0 let default_int = 100 ; // Type: i32
1 let large_int = 1_000_000 ; // Type: i32
2 let unsigned_val = 42u32 ; // Type: u32
3 let id = 0x1A4F ; // Type: i32
4 let big_id = 0xDE_AD_BE_EFu64 ; // Type: u64

Floating-Point Literals

Floating-point literals represent numbers with a fractional component.

Decimal Notation: `3.14`, `0.001`, `1.0` Scientific Notation: 1.5e10 ( 1.5 × 10¹⁰ ), 2.5e-3 ( 2.5 × 10⁻³ ) Numeric Separator: * _ can be used in integer or fractional parts (e.g., 1_234.567_890 )

By default, a floating-point literal is of type f64 .

Suffix Type Precision
f32 32-bit float \~7 decimal digits
f64 64-bit float \~15 decimal digits

Example:

Example
0 let pi = 3 . 14159 ; // Type: f64
1 let small_val = 1e - 6 ; // Type: f64
2 let gravity = 9 . 8f32 ; // Type: f32
3 let large_float = 1_234 . 567 ; // Type: f64

Duration Literals

Duration literals represent a span of time and are of the first-class Duration type. They are formed by an integer or floating-point literal followed by a unit suffix.

Suffix Unit Description
ns Nanoseconds The smallest unit of time
us Microseconds 1,000 nanoseconds
ms Milliseconds 1,000 microseconds
s Seconds 1,000 milliseconds
min Minutes 60 seconds
h Hours 60 minutes
d Days 24 hours

Example:

Example
0 let timeout : Duration = 250ms ;
1 let retry_interval : Duration = 3s ;
2 let frame_time : Duration = 16 . 6ms ;
3 let long_wait : Duration = 1 . 5h ;

Boolean Literals

Boolean literals represent truth values and are of type bool .

`true`: Represents logical truth. false : Represents logical falsehood.

Example:

Example
0 let is_ready : bool = true ;
1 let has_failed : bool = false ;

Character Literals

A character literal represents a single Unicode scalar value (stored as a u32 ). Enclosed in single quotes ( ' ).

Example:

Example
0 let initial : char = 'P' ;
1 let newline : char = '\n' ;
2 let escaped_quote : char = '\' ' ;

String Literals

String literals represent sequences of characters and are of type string . Aela supports two forms:

Single-Line Strings

Enclosed in double quotes ( " ). Support escape sequences:

`\n` newline \r carriage return `\t` tab \\ backslash * \" double quote

Example:

Example
0 let greeting = "Hello , World ! \n" ;

Multi-Line Strings

Enclosed in backticks (` ` ). These are *raw*: preserve all whitespace and newlines. Only \` (escaped backtick) and \\\` (escaped backslash) are special.

Example:

Example
0 let query = `
1 SELECT
2 id ,
3 name
4 FROM
5 users ;
6 ` ;

Aggregate Literals

Aggregate literals create container values like arrays and structs.

Array Literals

A comma-separated list inside [] . Elements must share a compatible type. Empty arrays require a type annotation.

Example:

Example
0 let numbers = [ 1 , 2 , 3 , 4 , 5 ] ; // inferred i32[]
1 let names : string [ ] = [ ] ; // explicit annotation required

Struct Literals

Create a struct instance with {} .

Named Struct Literal: Prefix with the struct type. Field Shorthand: Use x instead of x: x . Spread Operator: * Use ... to copy fields from another struct.

Example:

Example
0 struct Point {
1 x : i32 ;
2 y : i32 ;
3 }
4
5 let p1 = Point { x : 10 , y : 20 } ;
6
7 let x = 15 ;
8 let p2 = Point { x , y : 30 } ; // shorthand
9
10 let p3 = Point { . . . p1 , y : 40 } ; // p3 = { x: 10, y: 40 }

All Literals in Action

bool { // Numbers let int_val = 1_000; let hex_val = 0xFF; let sixty_four_bits = 12345u64; let float_val = 99.5f32; let scientific = 6.022e23; // Durations let http_timeout = 30s; let animation_frame = 16.6ms; // Booleans let is_active = true; // Characters let a = 'a'; // Strings let single = "A single line."; let multi = `A multi-line string.`; // Aggregates let ids: u64[] = [101u64, 202u64, 303u64]; let name = "Alice"; let user = User { id: 1u64, name, is_active }; t.ok(true, "Literals demonstrated"); return true; }" lang="aela" title="Example" id="678ae36ee2001">
Example
0 import { Tap } from " . . / . . / . . / lib / test . ae" ;
1
2 struct User {
3 id : u64 ;
4 name : string ;
5 is_active : bool ;
6 }
7
8 export fn all_literals_example ( t : & Tap ) - > bool {
9 // Numbers
10 let int_val = 1_000 ;
11 let hex_val = 0xFF ;
12 let sixty_four_bits = 12345u64 ;
13 let float_val = 99 . 5f32 ;
14 let scientific = 6 . 022e23 ;
15
16 // Durations
17 let http_timeout = 30s ;
18 let animation_frame = 16 . 6ms ;
19
20 // Booleans
21 let is_active = true ;
22
23 // Characters
24 let a = 'a' ;
25
26 // Strings
27 let single = "A single line . " ;
28 let multi = `A
29 multi - line
30 string . ` ;
31
32 // Aggregates
33 let ids : u64 [ ] = [ 101u64 , 202u64 , 303u64 ] ;
34 let name = "Alice" ;
35 let user = User { id : 1u64 , name , is_active } ;
36
37 t . ok ( true , "Literals demonstrated" ) ;
38 return true ;
39 }

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="a2c2d2e484d92">
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 ;

Optional

The Optional type 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="ea0d1620f4b28">
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_sb_append: fn(&StringBuilder, string); ffi ae_sb_destroy: fn(&StringBuilder); // <-- The cleanup function fn main() -> int { let sb = ae_sb_new(); ae_sb_append(sb, "hello"); // CRITICAL: You must call destroy when you are done. ae_sb_destroy(sb); // Using `sb` after this point is UNDEFINED BEHAVIOR. // ae_sb_append(sb, " world"); // <-- ERROR! return 0; }" lang="aela" title="Example: Managing Lifetimes" id="33b615ffe6335">
Example: Managing Lifetimes
0 `` `aela
1 import { StringBuilder } from " . / runtime . ae" ;
2
3 // FFI Declarations for a C string builder
4 ffi ae_sb_new : fn ( ) - > & StringBuilder ;
5 ffi ae_sb_append : fn ( & StringBuilder , string ) ;
6 ffi ae_sb_destroy : fn ( & StringBuilder ) ; // <-- The cleanup function
7
8 fn main ( ) - > int {
9 let sb = ae_sb_new ( ) ;
10 ae_sb_append ( sb , "hello" ) ;
11
12 // CRITICAL: You must call destroy when you are done.
13 ae_sb_destroy ( sb ) ;
14
15 // Using `sb` after this point is UNDEFINED BEHAVIOR.
16 // ae_sb_append(sb, " world"); // <-- ERROR!
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 }

Duration & Instant

Time-related bugs are notoriously common and usually subtle. The root cause is frequently quantity confusion: when a plain number like 10 or lastUpdated is used, its unit is ambiguous. Does it represent 10 seconds, 10 milliseconds, or 10 microseconds? The programmer's intent is lost, hidden in variable names or documentation, leading to misinterpretations and errors.

Duration a first-class type with built-in literals. This design has two major benefits:

Improved Comprehension: Code becomes self-documenting. A value like 250ms is unambiguous; it cannot be mistaken for seconds or any other unit. This clarity makes code easier to read, write, and maintain. An expression like let timeout = 1s + 500ms; is immediately understandable without needing to look up function definitions or comments.

Clarified Intent & Type Safety: By distinguishing Duration from numeric types, the compiler can enforce correctness. You cannot accidentally add a raw number to a duration (5s + 3 is a compile-time error), which prevents nonsensical operations. Function signatures become more expressive and safe, for example fn sleep(for: Duration). This forces the caller to be explicit (e.g., sleep(for: 500ms)), eliminating the possibility of passing a value with the wrong unit.

The Duration type moves the handling of time units from a convention to a language-enforced guarantee, significantly reducing a whole class of common bugs.

Literals & type

  • Literals: INT_LITERAL DurationUnit or FLOAT_LITERAL DurationUnit (e.g., 250ms , 1.5s ).
  • Type: Duration is a first-class scalar quantity (internally monotonic-time ticks; implementation detail).
  • Sign: Duration is signed . -5s is allowed via unary minus.
  • No implicit numeric conversions: Duration never implicitly converts to/from numeric types.

Unary

Form Result Notes
+d Duration no-op
-d Duration negation; overflow is checked

Binary with Duration

Expr Result Allowed? Notes
d1 + d2 Duration Yes checked overflow
d1 - d2 Duration Yes checked overflow
d1 * n Duration Yes n is integer (any int type); checked overflow
n * d1 Duration Yes symmetric
d1 / n Duration Yes n integer; trunc toward zero ; div-by-zero error
d1 / d2 F64 Yes dimensionless ratio (floating)
d1 % d2 Duration Yes remainder; d2 != 0
d1 % n No disallowed
d1 & d2 - No no bitwise ops on Duration (including ^ , << , >> )
d1 && d2 No not booleans

Float scalars

Disallowed by default: Duration * F64 , Duration / F64 Rationale: silent precision loss. Provide library helpers instead (e.g., Duration::from_seconds_f64(x) ).

Comparison

Expr Result Allowed?
d1 == d2 Bool Yes
d1 != d2 Bool Yes
d1 < d2 , <= , > , >= Bool Yes
d1 == n , d1 < n No (no cross-type compare)

Instant

Expr Result Allowed? Notes
t1 + d Instant Yes checked overflow
d + t1 Instant Yes commutes
t1 - d Instant Yes checked overflow
t1 - t2 Duration Yes difference
t1 + t2 , t1 * d No nonsensical

Casting / construction

  • Allowed: explicit constructors, e.g. Duration::from_ms(250) , Duration::seconds_f64(1.5) .
  • Disallowed: implicit casts ( (int) d , (f64) d ).

Overflow & division semantics

  • Checked arithmetic by default: + , - , * on Duration panic on overflow (or trap).
  • Provide library variants:
  • checked_add , checked_sub , checked_mulOption
  • saturating_add , saturating_sub , saturating_mul
  • Division: d / n truncates toward zero; n must be nonzero.
  • d / d returns F64 (no truncation).

Examples

Example
0 let a : Duration = 250ms + 1s ; // ok
1 let b : Duration = 2 * 500ms ; // ok (int * Duration)
2 let c : Duration = ( 5s - 1200ms ) ; // ok, can be negative
3 let r : f64 = ( 750ms / 1 . 5s ) ; // ok: Duration / Duration -> F64 == 0.5
4
5 let bad1 = 1 . 2 * 5s ; // error: float scalar not allowed
6 let bad2 = 5s + 3 ; // error: no Duration + Int
7 let bad3 = 5s < 1000 ; // error: cross-type compare
8 let bad4 = 5s & 1s ; // error: bitwise on Duration

Suffix/literal interaction (clarity)

  • 1s + 500ms is fine; units normalize.
  • 1.5s is legal as a literal; it’s converted to integral ticks (ns) with rounding toward zero during lex/const-eval. (If you prefer bankers-rounding, specify that instead.)
  • No ambiguity with range tokens: ensure lexer orders '...' , '..=' , '..' (longest first) and treats ms/min etc. as unit suffixes , not identifiers.

Arenas

Overview

Aela's has a three-part model for safe, dynamic memory management. The model is designed to provide explicit, and verifiable memory control for both hosted (OS) and freestanding (bare-metal) environments.

The model consists of:

  • An intrinsic Arena type for memory provisioning.
  • A transactional reserve statement for scoped memory reservation.
  • A context-aware new keyword for object allocation.

The implementation is based on compile-time AST tagging, ensuring zero runtime overhead and inherent safety for asynchronous and multi-threaded code.

The Arena

The Arena is a primitive type known to the compiler, used for managing a block of memory.

Syntax

An Arena is provisioned using a special form of the new expression.

Example
0 // For freestanding targets (bare-metal)
1 'let' IDENTIFIER ':' 'Arena' '=' 'new' 'static' '{' 'size' ':' ConstantExpression '}' ';'
2
3 // For hosted targets (OS)
4 'let' IDENTIFIER ':' 'Arena' '=' 'new' '{' '}' ';'

Semantics

new {} : A runtime operation for hosted environments. It calls the system allocator (e.g., malloc). This expression is fallible and should be treated as returning an Option(Arena).

new static { size: ... } : A compile-time instruction. It directs the linker to reserve a fixed-size block of memory in the final binary's static data region (e.g., .bss). This is the primary mechanism for provisioning memory on bare metal.

The reserve Statement (Transactional Reservation)

The reserve statement transactionally reserves memory from an Arena for a specific lexical scope.

Syntax

Example
0 'reserve' size_expr 'from' arena_expr Block [ 'else' Block ]

Semantics

The reserve statement attempts to acquire size_expr bytes from the given arena_expr.

If the reservation is successful, the first Block is executed.

If the reservation fails (the arena has insufficient capacity), the else Block is executed.

A successful reservation creates a special allocation context that is active for the duration of the success block and any functions called from within it.

The new Keyword (Allocation)

The new keyword creates an object instance. Its behavior is context-dependent and verified by the compiler.

Semantics

The compiler enforces three distinct behaviors for new:

Hosted Default Context: When compiling for a hosted target and not inside a reserve block, new allocates from the system heap.

Freestanding Default Context: When compiling for a bare-metal target and not inside a reserve block, a call to new is a compile-time error. This ensures no accidental heap usage on constrained devices.

reserve Context: Inside a successful reserve block, new allocates from the reserved memory. This allocation is infallible and returns a value of type T, not Option(T).

Complete Bare-Metal Example

Example
0 // 1. PROVISIONING (Compile-Time)
1 // The compiler reserves 64KB of static memory.
2 var MY_ARENA : Arena = new static { size : 65536 } ;
3
4 // This function is only called from within a `reserve` block, so `new` is safe.
5 fn create_header ( ) - > Header {
6 // This `new` call inherits the reservation context from its caller.
7 return new shared Header { } ;
8 }
9
10 fn create_packet ( ) - > Option ( Packet ) {
11 // 2. RESERVATION (Transactional Check)
12 reserve 2048b from MY_ARENA {
13 // This block is entered only if the reservation succeeds.
14
15 // 3. ALLOCATION (Infallible)
16 // `new` is now infallible and allocates from MY_ARENA.
17 let packet = new shared Packet { } ;
18 packet . header = create_header ( ) ;
19
20 return Some ( packet ) ;
21 } else {
22 // The reservation failed; handle the error.
23 return None ;
24 }
25 }

Buffers

Introduction

Buffer(T) is a fundamental intrinsic type that provides a low-level, direct interface to a contiguous block of allocated memory (from where depending on if you do or don't use a reserve block). It is the primitive that higher-level, safe collection types like Vec(T) and String are built.

As an intrinsic , the compiler has special knowledge of Buffer(T) , allowing it to enforce powerful compile-time guarantees about memory ownership and borrowing. It's important to understand that Buffer(T) is intentionally designed as an unsafe primitive . Its core operations do not perform runtime bounds checking, providing a zero-overhead foundation for performance-critical code and the standard library. Your code can make it safe

Core Concepts

Representation

A Buffer(T) is a "fat pointer" containing two fields:

  1. A raw pointer to the start of the memory block.
  2. The capacity of the buffer (the total number of elements it can hold).

A Buffer(T) only tracks its total capacity. It does not track how many elements are currently initialized or in use (its length ). This responsibility is left to higher-level abstractions.

Ownership

The Buffer(T) value is the unique owner of the memory it controls. The compiler's verifier enforces this ownership model strictly:

  • When a Buffer(T) is moved, ownership is transferred. The original variable can no longer be used.
  • When a Buffer(T) variable goes out of scope, its memory is automatically deallocated.
  • The std::buffer::drop intrinsic can be used to explicitly deallocate the memory, consuming the buffer variable.

This model guarantees at compile time that the buffer's memory is freed exactly once, eliminating memory leaks and double-free errors.

The Intrinsic API

The following functions provide the raw manipulation capabilities for Buffer(T) .

std::buffer::alloc

Signature std::buffer::alloc(capacity: int, elem_size: int) -> Buffer(T)
Description Allocates an uninitialized buffer on the heap. The element type T is inferred from the context.

std::buffer::write

Signature std::buffer::write(mut buf: Buffer(T), index: int, value: T)
Description Writes a value into the buffer at a given index. This is an unsafe operation and does not perform bounds checking.

std::buffer::read

Signature std::buffer::read(buf: &Buffer(T), index: int) -> T
Description Reads the value from the buffer at a given index. This is an unsafe operation and does not perform bounds checking.

std::buffer::capacity

Signature std::buffer::capacity(buf: &Buffer(T)) -> int
Description Returns the total number of elements the buffer can hold. This operation is always safe.

std::buffer::drop

Signature std::buffer::drop(buf: Buffer(T))
Description Explicitly deallocates the buffer's memory. The verifier prevents any subsequent use of the buf variable.

std::buffer::view

Signature std::buffer::view(buf: &Buffer(T), start: int, len: int) -> &T[]
Description Creates an immutable slice ( &T[] ) that borrows a portion of the buffer's data. This is an unsafe operation as it does not check if the range is in bounds.

std::buffer::slice

Signature std::buffer::slice(mut buf: Buffer(T), start: int, len: int) -> T[]
Description Creates a mutable slice ( T[] ) that mutably borrows a portion of the buffer's data. This is an unsafe operation as it does not check if the range is in bounds.

The Safety Model: A Layered Approach

The safety of Buffer(T) and its ecosystem is best understood as a series of layers, where stronger guarantees are built upon more primitive ones.

Layer 1: The Unsafe Buffer(T) Primitive

The intrinsic functions themselves form the base layer. They are designed to be as close to the machine as possible. std::buffer::write compiles to a single store instruction, and std::buffer::read to a single load . They do not have bounds checks because they are meant to be the absolute zero-cost building blocks. This layer is primarily intended for the authors of the standard library and other highly-optimized, low-level code.

Layer 2: Compile-Time Safety via the Verifier

The compiler's verifier (or "borrow checker") provides the next layer of safety, and it does so with zero runtime cost . It enforces:

  • Ownership & Lifetimes : Guarantees that a Buffer is dropped exactly once and that any view or slice cannot outlive the Buffer it borrows from.
  • Aliasing Rules : Prevents data races by ensuring that you cannot have a mutable borrow ( T[] ) at the same time as any other borrow of the same data.

These checks happen entirely at compile time.

Layer 3: Provable Safety via Refinement Types

This is the highest level of safety, allowing for the creation of truly safe abstractions on top of the unsafe Buffer primitive. The language allows types to be "refined" with predicates that the compiler must prove.

A safe Vec(T) type in the standard library would not expose the unsafe read / write intrinsics. Instead, it would provide methods whose signatures use refinement types to enforce correctness:

Example
0 // Hypothetical safe API for a Vec(T) built on Buffer(T)
1 fn Vec . get ( & self , index : { i : int where i > = 0 & & i < self.size() }) -> & T {
2 // The compiler has already proven the index is valid, so we can
3 // safely call the unsafe intrinsic with no additional runtime check.
4 return std : : buffer : : view ( & self . buffer , index , 1 ) [ 0 ] ;
5 }

This system provides two powerful benefits:

  1. Compile-Time Proof : If you call my_vec.get(5) and the compiler can prove the vector's length is greater than 5, the safety is guaranteed and the generated code is just a direct memory access. The safety check has zero runtime cost.
  1. Compiler-Enforced Runtime Checks : If the compiler cannot prove the index is safe (e.g., it comes from user input), it will issue a compile-time error. This forces the programmer to add an explicit if check, which provides the compiler with the proof it needs inside the if block.
Example
0 let i = get_user_input ( ) ;
1 if ( i > = 0 & & i < my_vec . size ( ) ) {
2 // This is now valid. The compiler accepts the call because
3 // the 'if' condition satisfies the refinement type's predicate.
4 let element = my_vec . get ( i ) ;
5 }

This layered approach is the essence of a zero-cost abstraction: safety is guaranteed by the compiler wherever possible, and runtime costs are only incurred when logically necessary and are made explicit in the program's control flow.

Concurrency

Aela's concurrency is built on two orthogonal keywords, async and thread , that modify function declarations. These provide a clear, explicit syntax for defining concurrent work. The runtime manages a thread pool to execute these tasks, enabling both I/O-bound concurrency and CPU-bound parallelism that work in concert.

Core Keywords: async and thread

It is crucial to understand that `async` and `thread` are two separate modifiers with distinct meanings. Even though they are designed to work in a way that feels cohesive.

async

Marks a function as pausable . An async function can use the await keyword to non-blockingly wait for I/O or other long-running operations. It primarily relates to concurrency .

The decision to have a langauge-native async engine provides significant advantages for compile-time determinism.

thread

Marks a function as a parallel task . Calling a thread fn is a special operation that is non-blocking. It immediately submits the function to the runtime's thread pool for execution and returns a Task handle. It primarily relates to parallelism .

These keywords can be combined to define tasks with different behaviors:

Combination Meaning Primary Use Case
fn A regular, blocking function. Standard synchronous logic.
async fn A pausable, awaitable function. I/O-bound work on the current thread.
thread fn A function that runs in parallel in another thread. It cannot use await . CPU-intensive, blocking computations that should not stall the main thread.
async thread fn A pausable function that runs in parallel in a thread. A self-contained parallel task that performs its own I/O (e.g., a network client).

Defining and Spawning Tasks

Threaded tasks are ideal for the parallel processing of CPU intensive workloads.

Example
0 // This function is defined as a parallel task.
1 thread fn process_data ( source : DataSource ) - > Report {
2 return generate_report ( data ) ;
3 }

But sometimes a threaded task to compartmentalize work. For example, you might want some async processing to happen in another thread, separately from a UI thread.

The async keyword is optional and is used if the task needs to await I/O. The function's return type defines the final value returned when the task is complete.

Example
0 // This function is defined as a parallel task that is also async.
1 async thread fn process_data ( source : DataSource ) - > Report {
2 let data = await source . read ( ) ;
3 // ...
4 return generate_report ( data ) ;
5 }

Calling a function marked thread is a non-blocking operation. It starts the task in the background and immediately returns a Thread handle.

Example
0 // This call is non-blocking and returns a handle.
1 let handle : Thread ( Report ) = process_data ( my_source ) ;
2
3 // Await the handle to get the final result.
4 let report : Report = await handle . join ( ) ;

Structured Parallelism: thread { ... } Block

The thread block is used to run a group of tasks in parallel and wait for all of them to complete before continuing.

Example
0 async fn get_dashboard_data ( ) - > Dashboard {
1 var user : User ;
2 var orders : [ Order ] ;
3
4 // This block runs its internal async calls in parallel.
5 thread {
6 let user_task = fetch_user_profile ( 123 ) ;
7 let orders_task = fetch_recent_orders ( 123 ) ;
8
9 // The block implicitly awaits both before exiting.
10 user = await user_task ;
11 orders = await orders_task ;
12 }
13
14 // This line is only reached after both tasks are complete.
15 return Dashboard ( user , orders ) ;
16 }

Channels

Channels are used for streaming communication between threads. The type Channel(T) is a valid type according to the TypeApplication grammar rule.

Example
0 // Create a channel that transports string values.
1 let my_channel : Channel ( string ) = new { } ;

Streams

This pattern pauses the current function to process each message from a channel sequentially.

Example
0 // The `for await` loop consumes the receiver.
1 for await ( let message : string in my_channel . receiver ) {
2 process_message ( message ) ;
3 }

Events

This registers a handler and immediately returns, allowing the current function to continue its work.

Example
0 // `on_receive` takes a function expression as an argument.
1 my_channel . receiver . listen ( ( message : string ) - > void {
2 io : : print ( "Event received : { } " , message ) ;
3 } ) ;
4
5 // ... code here continues to run without blocking ...

Integrated Vehicle Safety vs. Aftermarket Parts

Think of it like the safety systems in a car.

A Library-Based Ecosystem (like Rust's): This is like buying a car chassis and then adding safety features yourself. You buy airbags from one company, an anti-lock braking system from another, and traction control from a third. They might be excellent components, but they weren't designed to work together as a single, cohesive system.

A Built-in Scheduler (Aela's model): This is like buying a modern car where the manufacturer has designed the airbags, ABS, traction control, and crumple zones to work together as a single, integrated safety system. It's tested as a whole and provides guarantees that the individual parts can't.

Here are the specific safety wins this integration provides.

  1. Compile-Time Data-Race Prevention

Because the scheduler is part of the language, the compiler has a deep understanding of what it means to "cross a thread boundary." It can enforce Send and Sync rules at compile-time. This means it's a syntax error to try to send non-thread-safe data into a thread fn or across a channel, completely eliminating an entire class of data-race bugs before the program can even run.

  1. A Single, Safe Bridge for Blocking Code

As we discussed, blocking in an async context is a huge foot-gun. A built-in runtime provides one, official, and well-defined function for handling this: std::task::run_blocking(). This prevents a scenario where different libraries provide their own, subtly different (and potentially unsafe) ways of handling blocking calls, which would lead to confusion and bugs.

  1. Guaranteed Structured Concurrency

The thread { ... } block is a major safety feature. Because it's a language construct powered by the built-in scheduler, the compiler can absolutely guarantee that all child tasks are completed before the parent function is allowed to continue. This prevents "leaked" tasks and makes error handling robust and predictable. In a library, this guarantee might be weaker or easier to accidentally bypass.

  1. Predictable Task Lifecycles

With a built-in scheduler, the behavior of a Task handle is predictable across the entire ecosystem. The rule that a handle will detach on drop is a language-level guarantee. This prevents situations where one library's handle might join on drop while another's aborts, leading to surprising behavior and resource leaks.

In short, a built-in scheduler allows Aela to treat concurrency as a core feature, subject to the same rigorous, compile-time safety analysis as the rest of the language.

Pool Control & Oversubscription

The Aela runtime is built on a work-stealing thread pool that defaults to using the number of logical CPU cores available (std::thread::available_parallelism()).

Pool Size & Pinning: The pool size can be overridden at startup via an environment variable (e.g., AELA_THREAD_COUNT=4). Fine-grained control like core pinning and thread priorities is considered a low-level OS feature and is not exposed through the high-level async/thread API. For expert use cases, a separate std::os::thread module could provide these unsafe, platform-specific controls.

On tiny targets without an OS, the runtime can be compiled in a "pool disabled" mode, using a single-threaded cooperative scheduler.

Oversubscription: The runtime's global task queue is bounded (e.g., a default capacity of 256 tasks). Calling a thread fn is cheap, but if the task queue is full, the call itself becomes an async operation. It will pause the calling function until space becomes available in the queue. This provides natural back-pressure and prevents developers from overwhelming the system.

Example
0 async fn io_bound_work1 ( ) - > int { // This already works
1 await doSomething ( ) ; // This already works
2 return 0 ;
3 }
4
5 thread fn compute_bound_work1 ( c : & Channel ( int ) ) - > int {
6 c . send ( 42 ) ; // some new data is ready
7 return 0 ; // the thread is finished
8 }
9
10 async fn main ( ) {
11 let c : Channel ( int ) = new shared { } ;
12
13 c . receive = fn ( i : int ) - > {
14 io : : print ( " { } " , i ) ;
15 } ;
16
17 let h : int = await compute_bound_work1 ( c ) ; // Actually returns `Task(int)` and resolves it
18 let i : int = await io_bound_work1 ( ) ; // Actually returns `Future(int)` and resolves it
19
20 let a : int [ ] = await std : : all ( [
21 compute_bound_work2 ( ) , // dispatched to the scheduler
22 compute_bound_work2 ( ) , // dispatched to the scheduler
23 io_bound_work2 ( ) // dispatched to the eventloop
24 ] ) ;
25
26 let h2 : Task ( int ) = compute_bound_work1 ( c ) ; // returns `Task(int)` lazy, not executing.
27
28 let i2 : int = await h2 ; // starts executing (ie, `await compute_bound_work1();`)
29 }
Example
0 enum RecvJob {
1 Msg ( Job ) ,
2 Cancelled ,
3 Timeout
4 }
5
6 async fn next_recv ( jobs : & Channel ( Job ) ) - > RecvJob {
7 let j : Job = await jobs . recv ( ) ;
8 return RecvJob : : Msg ( j ) ;
9 }
10
11 async fn on_cancel ( tok : & CancellationToken ) - > RecvJob {
12 await tok . cancelled ( ) ;
13 return RecvJob : : Cancelled ;
14 }
15
16 async fn after ( ms : Int ) - > RecvJob {
17 await timer . after_ms ( ms ) ;
18 return RecvJob : : Timeout ;
19 }
20
21 thread fn handle ( job : Job ) - > int {
22 // something interesting and cpu intensive
23 return 0 ;
24 }
25
26 async fn run_worker ( jobs : & Channel ( Job ) , tok : & CancellationToken ) - > int {
27 while ( true ) {
28
29 // `await select` atomically starts all provided cold awaitables, yields
30 // the first one to complete (as a tagged case for its arm), and cancels the rest.
31
32 let evt : RecvJob = await std : : select ( [
33 next_recv ( jobs ) ,
34 on_cancel ( tok ) ,
35 after ( 250 )
36 ] ) ;
37
38 match ( evt ) {
39 RecvJob : : Msg ( job ) = > { await handle ( job ) ; }
40 RecvJob : : Cancelled = > { break ; }
41 RecvJob : : Timeout = > { io : : print ( "tick" ) ; }
42 }
43 }
44 return 0 ;
45 }

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

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.

Formal Grammar Spec

' ReturnType RefinementType ::= '{' IDENTIFIER ':' Type KW_WHERE Expression '}' PrimitiveType ::= KW_U8 | KW_I8 | KW_U16 | KW_I16 | KW_U32 | KW_I32 | KW_U64 | KW_I64 | KW_F32 | KW_F64 | KW_BOOL | KW_CHAR | KW_STRING | KW_INT | KW_ARENA TypeArguments ::= '(' [ Type { ',' Type } ] ')' CompileTimeParameters ::= CompileTimeParameter { ',' CompileTimeParameter } CompileTimeParameter ::= IDENTIFIER | IDENTIFIER ':' Type RunTimeParameters ::= Parameter { ',' Parameter } FunctionParameters ::= RunTimeParameters | CompileTimeParameters [ ';' [ RunTimeParameters ] ] Parameter ::= [ '...' ] [ KW_MUT ] IDENTIFIER ':' Type FunctionTypeParameters ::= FunctionTypeParameter { ',' FunctionTypeParameter } FunctionTypeParameter ::= [ KW_MUT ] Type ArrayTypeModifier ::= '[' [ Expression ] ']' (* -------------------------------------------------------- ) ( COMMENTS ) ( -------------------------------------------------------- *) (* A single-line comment starts with // and continues to the end of the line ) SingleLineComment ::= '//' { ~('\n' | '\r') } (* A multi-line comment starts with /* and ends with */ ) MultiLineComment ::= '/*' { . } '*/' (* -------------------------------------------------------- ) ( STATEMENTS (Unambiguous) ) ( -------------------------------------------------------- *) Statement ::= MatchedStatement | UnmatchedStatement MatchedStatement ::= KW_IF '(' Expression ')' MatchedStatement KW_ELSE MatchedStatement | KW_IF KW_LET Pattern '=' Expression Block KW_ELSE MatchedStatement | Block | KW_RETURN [ Expression ] ';' | RaiseStatement | BreakStatement | ContinueStatement | WhileStatement | ForStatement | MatchStatement | ReserveStatement | ExpressionStatement | VarDeclaration | FunctionDeclaration | ';' UnmatchedStatement ::= KW_IF '(' Expression ')' Statement | KW_IF '(' Expression ')' MatchedStatement KW_ELSE UnmatchedStatement | KW_IF KW_LET Pattern '=' Expression Block | KW_IF KW_LET Pattern '=' Expression Block KW_ELSE UnmatchedStatement Block ::= '{' { Statement } '}' ExpressionStatement ::= Expression ';' BreakStatement ::= KW_BREAK ';' ContinueStatement ::= KW_CONTINUE ';' WhileStatement ::= KW_WHILE '(' Expression ')' Statement ForStatement ::= KW_FOR '(' ForDeclaratorList KW_IN Expression ')' Statement ForDeclaratorList ::= ForDeclarator { ',' ForDeclarator } ForDeclarator ::= ( KW_LET | KW_VAR ) IDENTIFIER ':' Type RaiseStatement ::= KW_RAISE Expression ';' (* -------------------------------------------------------- ) ( MATCH (Mandatory Exhaustive) ) ( - Expression form for atomic initialization. ) ( - Statement form for control flow. ) ( - Guards, @-bindings, and nesting are disallowed. ) ( -------------------------------------------------------- *) MatchStatement ::= KW_MATCH '(' Expression ')' '{' [ MatchStmtArm { ',' MatchStmtArm } [ ',' ] ] '}' MatchStmtArm ::= MatchArmPattern '=>' ( Block | ';' ) MatchArmPattern ::= Pattern { '|' Pattern } Pattern ::= LiteralPattern | IDENTIFIER // binding | '_' // wildcard | PathExpression [ '(' [ PatternList ] ')' ] // unit/tuple-variant | TuplePattern | StructPattern TuplePattern ::= '(' [ PatternList [ ',' ] ] ')' StructPattern ::= '{' [ StructFieldPat { ',' StructFieldPat } [ ',' ] ] '}' StructFieldPat ::= IDENTIFIER ( ':' Pattern )? | '...' IDENTIFIER PatternList ::= Pattern { ',' Pattern } RangePattern ::= INT_LITERAL ('..' | '..=') INT_LITERAL | CHAR_LITERAL ('..' | '..=') CHAR_LITERAL LiteralPattern ::= INT_LITERAL | STRING_LITERAL | CHAR_LITERAL | KW_TRUE | KW_FALSE | RangePattern (* -------------------------------------------------------- ) ( EXPRESSIONS (Pratt Parser Aligned) ) ( -------------------------------------------------------- *) Expression ::= AssignmentExpression AssignmentExpression ::= CoalescingExpression | AssignmentTarget AssignmentOperator AssignmentExpression (* keep syntax permissive; lvalue-ness is a semantic check *) AssignmentTarget ::= CoalescingExpression AssignmentOperator ::= '=' | '+=' | '-=' | '*=' | '/=' CoalescingExpression ::= LogicalOrExpression { '??' LogicalOrExpression } LogicalOrExpression ::= LogicalAndExpression { '||' LogicalAndExpression } LogicalAndExpression ::= BitwiseOrExpression { '&&' BitwiseOrExpression } BitwiseOrExpression ::= BitwiseXorExpression { '|' BitwiseXorExpression } BitwiseXorExpression ::= BitwiseAndExpression { '^' BitwiseAndExpression } BitwiseAndExpression ::= ShiftExpression { '&' ShiftExpression } ShiftExpression ::= EqualityExpression { ( '<<' | '>>' ) EqualityExpression } EqualityExpression ::= ComparisonExpression { ( '==' | '!=' ) ComparisonExpression } ComparisonExpression ::= AdditiveExpression { ( '<' | '<=' | '>' | '>=' ) AdditiveExpression } AdditiveExpression ::= MultiplicativeExpression { ( '+' | '-' ) MultiplicativeExpression } MultiplicativeExpression ::= CastExpression { ( '*' | '/' | '%' ) CastExpression } CastExpression ::= UnaryExpression { KW_AS Type } UnaryExpression ::= ( KW_AWAIT | '-' | '!' | '&' | '~' ) UnaryExpression | PostfixExpression PostfixExpression ::= PrimaryExpression { '(' [ ArgumentList ] ')' | '[' Expression ']' | ( '.' | '?.' ) IDENTIFIER } PrimaryExpression ::= PathExpression | Literal | '(' Expression ')' | ArrayLiteral | NamedStructLiteral | AnonymousStructLiteral | FunctionExpression | NewExpression | MatchExpression MatchExpression ::= KW_MATCH '(' Expression ')' '{' [ MatchExprArm { ',' MatchExprArm } [ ',' ] ] '}' MatchExprArm ::= MatchArmPattern '=>' Expression PathExpression ::= IDENTIFIER { '::' IDENTIFIER } (* -------------------------------------------------------- ) ( TEMPORAL EXPRESSIONS ) ( -------------------------------------------------------- *) TemporalExpression ::= KW_ALWAYS Expression | KW_EVENTUALLY Expression | KW_NEXT Expression | Expression KW_UNTIL Expression | Expression KW_RELEASE Expression | KW_FORALL IDENTIFIER KW_IN Expression ':' Expression | KW_EXISTS IDENTIFIER KW_IN Expression ':' Expression | Expression (* -------------------------------------------------------- ) ( LITERALS & HELPER RULES ) ( -------------------------------------------------------- *) Literal ::= INT_LITERAL | FLOAT_LITERAL | STRING_LITERAL | STRING_MULTILINE | CHAR_LITERAL | DurationLiteral | KW_TRUE | KW_FALSE ArrayLiteral ::= '[' [ ArgumentList ] ']' NamedStructLiteral ::= PathExpression StructLiteralBody AnonymousStructLiteral ::= StructLiteralBody StructLiteralBody ::= '{' [ StructElement { ',' StructElement } [ ',' ] ] '}' StructElement ::= ( IDENTIFIER ':' Expression ) | IDENTIFIER | '...' Expression ArgumentList ::= CallArgument { ',' CallArgument } CallArgument ::= [ '...' ] [ KW_MUT ] Expression FunctionExpression ::= FnModifiers KW_FN '(' [ FunctionParameters ] ')' '->' ReturnType FunctionBodyWithReturn (* -------------------------------------------------------- ) ( Automatic Dereference: All values returned by `new`, with ) ( or without modifiers, are reference types and are ) ( automatically dereferenced when used in expression and ) ( member access contexts. Users do not need to explicitly ) ( write *x to access the underlying value; the compiler ) ( inserts dereferences implicitly. ) ( -------------------------------------------------------- *) NewExpression ::= KW_NEW [ AllocationModifiers ] AllocationBody AllocationModifiers ::= KW_SHARED [ KW_ATOMIC ] | KW_STATIC [ KW_ATOMIC ] | KW_WEAK AllocationBody ::= PrimaryExpression | StructLiteralBody ReserveStatement ::= KW_RESERVE Expression KW_FROM Expression Block [ KW_ELSE Block ] (* -------------------------------------------------------- ) ( TERMINALS (TOKENS) ) ( -------------------------------------------------------- *) IDENTIFIER INT_LITERAL, FLOAT_LITERAL, STRING_LITERAL, STRING_MULTILINE, CHAR_LITERAL (* Keywords: *) KW_LET, KW_VAR, KW_FN, KW_IF, KW_IN, KW_ELSE, KW_WHILE, KW_FOR, KW_RETURN, KW_BREAK, KW_CONTINUE, KW_WHERE, KW_ASYNC, KW_AWAIT, KW_AS, KW_STRUCT, KW_IMPL, KW_THREAD, KW_PURE KW_ENUM, KW_MATCH, KW_TYPE, KW_VOID, KW_INT, KW_ARENA, KW_U8, KW_I8, KW_U16, KW_I16, KW_U32, KW_I32, KW_U64, KW_I64, KW_F32, KW_F64, KW_BOOL, KW_CHAR, KW_STRING, KW_TRUE, KW_FALSE, KW_IMPORT, KW_EXPORT, KW_FROM, KW_FFI, KW_MAP, KW_DURATION, KW_INSTANT, (* No shared mutability without atomics! *) KW_NEW, KW_RESERVE, KW_SHARED, KW_ATOMIC, KW_WEAK, KW_STATIC, KW_MUT, KW_SYSTEM, KW_ACTION, KW_REQUIRES, KW_ENSURES, KW_INVARIANT, KW_PROPERTY, KW_FORALL, KW_EXISTS, KW_ALWAYS, KW_EVENTUALLY, KW_NEXT, KW_UNTIL, KW_RELEASE, KW_FAULT, KW_RAISE (* Operators and Delimiters: Arithmetic Wraps ) '=', '+=', '-=', '*=', '/=', '+', '-', '*', '/', '%', '&', '==', '!=', '<', '<=', '>', '>=', '!', '&&', '||', '|', '^', '~', '<<', '>>', '(', ')', '{', '}', '[', ']', ',', ';', '.', ':', '::', '?', '?.', '??', '...', '..=', '..', '->', '_', '=>' EOF " title="Grammar" id="8107cf2fa09a3">
Grammar
0 ( * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = )
1 ( Aela Language Grammar 0 . 0 . 8 )
2 ( Finalized : 2025 - 10 - 12 )
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 | FaultDeclaration
24 )
25
26 TypeAliasDeclaration : : = KW_TYPE IDENTIFIER '=' Type ';'
27
28 ReExportDeclaration : : =
29 KW_EXPORT ( NamedImport | IDENTIFIER )
30 KW_FROM STRING_LITERAL ';'
31
32 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
33 ( IMPORTS )
34 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
35
36 ImportStatement : : =
37 KW_IMPORT ( NamedImport | IDENTIFIER )
38 KW_FROM STRING_LITERAL ';'
39
40 NamedImport : : = '{' [ ImportSpecifier { ',' ImportSpecifier } [ ',' ] ] '}'
41 ImportSpecifier : : = IDENTIFIER [ ':' IDENTIFIER ]
42
43 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
44 ( FFI ( Foreign Function Interface ) )
45 ( - Contracts are compile - time enforced to be UB - free )
46 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
47
48 FfiDeclaration : : = KW_FFI IDENTIFIER '=' Type ';'
49
50 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
51 ( DECLARATIONS )
52 ( - var is mutable , let is immutable )
53 ( - aliases are borrow - checked by the analyzer )
54 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
55
56 VarDeclaration : : = ( KW_VAR | KW_LET ) IDENTIFIER ':' Type
57 [ '=' Expression ] ';'
58
59 StructDeclaration : : = KW_STRUCT IDENTIFIER '(' [ FunctionParameters ] ')' . . .
60
61 StructFieldDeclaration : : =
62 ( IDENTIFIER ':' Type )
63 | ( '...' IDENTIFIER )
64
65 VarDeclaration : : = ( KW_VAR | KW_LET ) IDENTIFIER ':' Type
66 [ '=' Expression ] ';'
67
68 FnModifiers : : =
69 [ ( KW_THREAD [ KW_PURE ] ) // thread; pure+thread allowed
70 | ( KW_PURE [ KW_THREAD ] ) // pure; optional thread
71 | KW_ASYNC // async alone (no pure)
72 ]
73
74 ImplBlock : : = KW_IMPL Type '{' { FunctionDeclaration | InvariantDeclaration } '}'
75
76
77 FunctionDeclaration : : =
78   FnModifiers KW_FN IDENTIFIER
79   '(' [ FunctionParameters ] ')' '->' ReturnType
80   ( ';' | FunctionBodyWithReturn )
81
82 FunctionBodyWithReturn : : =
83 '{' { Statement } ReturnStatement '}'
84
85 ReturnStatement : : = KW_RETURN [ Expression ] ';'
86
87 EnumDeclaration : : = KW_ENUM IDENTIFIER '(' [ FunctionParameters ] ')' . . .
88
89 TypeArguments : : = '(' [ TypeOrConst { ',' TypeOrConst } ] ')'
90 TypeOrConst : : = Type | ConstExpression
91
92 SystemDeclaration : : = KW_SYSTEM IDENTIFIER '{'
93 { ActionDeclaration
94 | InvariantDeclaration
95 | PropertyDeclaration
96 }
97 '}'
98
99 ActionDeclaration : : = KW_ACTION IDENTIFIER
100 '(' [ FunctionParameters ] ')'
101 [ RequiresClause ]
102 [ EnsuresClause ]
103 Block
104
105 RequiresClause : : = KW_REQUIRES Expression
106 EnsuresClause : : = KW_ENSURES Expression
107
108 InvariantDeclaration : : = KW_INVARIANT IDENTIFIER ':' Expression
109 PropertyDeclaration : : = KW_PROPERTY IDENTIFIER ':' TemporalExpression
110
111 FaultDeclaration : : = KW_FAULT IDENTIFIER '(' [ FunctionParameters ] ')' ';'
112
113 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
114 ( TYPES )
115 ( - - - - - )
116 ( The `(...)` syntax following a type identifier is used )
117 ( for type - level parameters , which can include both )
118 ( types AND values , to support Dependent Types . This )
119 ( differs from the generics syntax in languages like )
120 ( Rust or C + + , which typically use `<...>` for type - only )
121 ( parameters . )
122 ( )
123 ( Aela does not add built in properties or methods , instead )
124 ( it uses std : : length ( v ) , std : : size ( v ) , or standard library )
125 ( functions ie `import { vec } from "core/vector.ae";` )
126 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
127
128 Type : : = [ '&' ] PostfixType
129
130 PostfixType : : = SimpleType { ArrayTypeModifier | TypeArguments | '?' }
131
132 ReturnType : : = Type [ '|' FaultTypeList ]
133 FaultTypeList : : = FaultType { '|' FaultType }
134 FaultType : : = PathExpression
135
136 MapType : : = KW_MAP '(' Type ',' Type ')'
137
138 SimpleType : : = PrimitiveType
139 | KW_VOID
140 | FunctionTypeSignature
141 | PathExpression
142 | MapType
143 | RefinementType
144 | '(' Type ')'
145
146 FunctionTypeSignature : : =
147 FnModifiers KW_FN '(' [ FunctionTypeParameters ] ')' '->' ReturnType
148
149 RefinementType : : = '{' IDENTIFIER ':' Type KW_WHERE Expression '}'
150
151 PrimitiveType : : = KW_U8 | KW_I8 | KW_U16 | KW_I16 | KW_U32 | KW_I32
152 | KW_U64 | KW_I64 | KW_F32 | KW_F64 | KW_BOOL
153 | KW_CHAR | KW_STRING | KW_INT | KW_ARENA
154
155 TypeArguments : : = '(' [ Type { ',' Type } ] ')'
156
157 CompileTimeParameters : : = CompileTimeParameter { ',' CompileTimeParameter }
158 CompileTimeParameter : : = IDENTIFIER | IDENTIFIER ':' Type
159 RunTimeParameters : : = Parameter { ',' Parameter }
160
161 FunctionParameters : : =
162 RunTimeParameters
163 | CompileTimeParameters [ ';' [ RunTimeParameters ] ]
164
165 Parameter : : = [ '...' ] [ KW_MUT ] IDENTIFIER ':' Type
166 FunctionTypeParameters : : = FunctionTypeParameter { ',' FunctionTypeParameter }
167 FunctionTypeParameter : : = [ KW_MUT ] Type
168 ArrayTypeModifier : : = '[' [ Expression ] ']'
169
170 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
171 ( COMMENTS )
172 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
173
174 ( * A single - line comment starts with // and continues to the end of the line )
175 SingleLineComment : : = '//' { ~('\n' | '\r') }
176
177 ( * A multi - line comment starts with /* and ends with */ )
178 MultiLineComment : : = '/*' { . } '*/'
179
180 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
181 ( STATEMENTS ( Unambiguous ) )
182 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
183
184 Statement : : = MatchedStatement | UnmatchedStatement
185
186 MatchedStatement : : =
187 KW_IF '(' Expression ')' MatchedStatement KW_ELSE MatchedStatement
188 | KW_IF KW_LET Pattern '=' Expression Block KW_ELSE MatchedStatement
189 | Block
190 | KW_RETURN [ Expression ] ';'
191 | RaiseStatement
192 | BreakStatement
193 | ContinueStatement
194 | WhileStatement
195 | ForStatement
196 | MatchStatement
197 | ReserveStatement
198 | ExpressionStatement
199 | VarDeclaration
200 | FunctionDeclaration
201 | ';'
202
203 UnmatchedStatement : : =
204 KW_IF '(' Expression ')' Statement
205 | KW_IF '(' Expression ')' MatchedStatement KW_ELSE UnmatchedStatement
206 | KW_IF KW_LET Pattern '=' Expression Block
207 | KW_IF KW_LET Pattern '=' Expression Block KW_ELSE UnmatchedStatement
208
209 Block : : = '{' { Statement } '}'
210 ExpressionStatement : : = Expression ';'
211 BreakStatement : : = KW_BREAK ';'
212 ContinueStatement : : = KW_CONTINUE ';'
213
214 WhileStatement : : = KW_WHILE '(' Expression ')' Statement
215
216 ForStatement : : = KW_FOR '(' ForDeclaratorList KW_IN Expression ')' Statement
217 ForDeclaratorList : : = ForDeclarator { ',' ForDeclarator }
218 ForDeclarator : : = ( KW_LET | KW_VAR ) IDENTIFIER ':' Type
219
220 RaiseStatement : : = KW_RAISE Expression ';'
221
222 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
223 ( MATCH ( Mandatory Exhaustive ) )
224 ( - Expression form for atomic initialization . )
225 ( - Statement form for control flow . )
226 ( - Guards , @ - bindings , and nesting are disallowed . )
227 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
228
229 MatchStatement : : = KW_MATCH '(' Expression ')' '{'
230 [ MatchStmtArm { ',' MatchStmtArm } [ ',' ] ]
231 '}'
232
233 MatchStmtArm : : = MatchArmPattern '=>' ( Block | ';' )
234
235 MatchArmPattern : : = Pattern { '|' Pattern }
236
237 Pattern : : =
238 LiteralPattern
239 | IDENTIFIER // binding
240 | '_' // wildcard
241 | PathExpression [ '(' [ PatternList ] ')' ] // unit/tuple-variant
242 | TuplePattern
243 | StructPattern
244
245 TuplePattern : : = '(' [ PatternList [ ',' ] ] ')'
246
247 StructPattern : : = '{' [ StructFieldPat { ',' StructFieldPat } [ ',' ] ] '}'
248 StructFieldPat : : = IDENTIFIER ( ':' Pattern ) ? | '...' IDENTIFIER
249
250 PatternList : : = Pattern { ',' Pattern }
251
252 RangePattern : : =
253 INT_LITERAL ( '..' | '..=' ) INT_LITERAL
254 | CHAR_LITERAL ( '..' | '..=' ) CHAR_LITERAL
255
256 LiteralPattern : : = INT_LITERAL | STRING_LITERAL | CHAR_LITERAL | KW_TRUE | KW_FALSE | RangePattern
257
258 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
259 ( EXPRESSIONS ( Pratt Parser Aligned ) )
260 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
261
262 Expression : : = AssignmentExpression
263
264 AssignmentExpression : : =
265 CoalescingExpression
266 | AssignmentTarget AssignmentOperator AssignmentExpression
267
268 ( * keep syntax permissive ; lvalue - ness is a semantic check * )
269 AssignmentTarget : : = CoalescingExpression
270
271 AssignmentOperator : : = '=' | '+=' | '-=' | '*=' | '/='
272
273 CoalescingExpression : : = LogicalOrExpression { '??' LogicalOrExpression }
274
275 LogicalOrExpression : : = LogicalAndExpression { '||' LogicalAndExpression }
276
277 LogicalAndExpression : : = BitwiseOrExpression { '&&' BitwiseOrExpression }
278
279 BitwiseOrExpression : : = BitwiseXorExpression { '|' BitwiseXorExpression }
280
281 BitwiseXorExpression : : = BitwiseAndExpression { '^' BitwiseAndExpression }
282
283 BitwiseAndExpression : : = ShiftExpression { '&' ShiftExpression }
284
285 ShiftExpression : : = EqualityExpression { ( '<<' | '>>' ) EqualityExpression }
286
287 EqualityExpression : : = ComparisonExpression { ( '==' | '!=' ) ComparisonExpression }
288
289 ComparisonExpression : : = AdditiveExpression { ( '<' | '<=' | '>' | '>=' ) AdditiveExpression }
290
291 AdditiveExpression : : = MultiplicativeExpression { ( '+' | '-' ) MultiplicativeExpression }
292
293 MultiplicativeExpression : : = CastExpression { ( '*' | '/' | '%' ) CastExpression }
294
295 CastExpression : : = UnaryExpression { KW_AS Type }
296
297 UnaryExpression : : =
298 ( KW_AWAIT | '-' | '!' | '&' | '~' ) UnaryExpression
299 | PostfixExpression
300
301 PostfixExpression : : =
302 PrimaryExpression {
303 '(' [ ArgumentList ] ')'
304 | '[' Expression ']'
305 | ( '.' | '?.' ) IDENTIFIER
306 }
307
308 PrimaryExpression : : =
309 PathExpression
310 | Literal
311 | '(' Expression ')'
312 | ArrayLiteral
313 | NamedStructLiteral
314 | AnonymousStructLiteral
315 | FunctionExpression
316 | NewExpression
317 | MatchExpression
318
319 MatchExpression : : = KW_MATCH '(' Expression ')' '{'
320 [ MatchExprArm { ',' MatchExprArm } [ ',' ] ]
321 '}'
322
323 MatchExprArm : : = MatchArmPattern '=>' Expression
324
325 PathExpression : : = IDENTIFIER { '::' IDENTIFIER }
326
327 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
328 ( TEMPORAL EXPRESSIONS )
329 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
330
331 TemporalExpression : : =
332 KW_ALWAYS Expression
333 | KW_EVENTUALLY Expression
334 | KW_NEXT Expression
335 | Expression KW_UNTIL Expression
336 | Expression KW_RELEASE Expression
337 | KW_FORALL IDENTIFIER KW_IN Expression ':' Expression
338 | KW_EXISTS IDENTIFIER KW_IN Expression ':' Expression
339 | Expression
340
341 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
342 ( LITERALS & HELPER RULES )
343 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
344
345 Literal : : =
346 INT_LITERAL | FLOAT_LITERAL | STRING_LITERAL | STRING_MULTILINE
347 | CHAR_LITERAL | DurationLiteral | KW_TRUE | KW_FALSE
348
349 ArrayLiteral : : = '[' [ ArgumentList ] ']'
350 NamedStructLiteral : : = PathExpression StructLiteralBody
351 AnonymousStructLiteral : : = StructLiteralBody
352 StructLiteralBody : : = '{' [ StructElement { ',' StructElement } [ ',' ] ] '}'
353 StructElement : : = ( IDENTIFIER ':' Expression ) | IDENTIFIER | '...' Expression
354
355 ArgumentList : : = CallArgument { ',' CallArgument }
356 CallArgument : : = [ '...' ] [ KW_MUT ] Expression
357
358 FunctionExpression : : = FnModifiers KW_FN '(' [ FunctionParameters ] ')' '->' ReturnType FunctionBodyWithReturn
359
360 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
361 ( Automatic Dereference : All values returned by `new` , with )
362 ( or without modifiers , are reference types and are )
363 ( automatically dereferenced when used in expression and )
364 ( member access contexts . Users do not need to explicitly )
365 ( write * x to access the underlying value ; the compiler )
366 ( inserts dereferences implicitly . )
367 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
368
369 NewExpression : : =
370 KW_NEW [ AllocationModifiers ] AllocationBody
371
372 AllocationModifiers : : =
373 KW_SHARED [ KW_ATOMIC ]
374 | KW_STATIC [ KW_ATOMIC ]
375 | KW_WEAK
376
377 AllocationBody : : =
378 PrimaryExpression
379 | StructLiteralBody
380
381 ReserveStatement : : =
382 KW_RESERVE Expression KW_FROM Expression Block [ KW_ELSE Block ]
383
384 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
385 ( TERMINALS ( TOKENS ) )
386 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
387
388 IDENTIFIER
389 INT_LITERAL , FLOAT_LITERAL , STRING_LITERAL , STRING_MULTILINE , CHAR_LITERAL
390
391 ( * Keywords : * )
392 KW_LET , KW_VAR , KW_FN , KW_IF , KW_IN , KW_ELSE , KW_WHILE , KW_FOR ,
393 KW_RETURN , KW_BREAK , KW_CONTINUE , KW_WHERE ,
394 KW_ASYNC , KW_AWAIT , KW_AS , KW_STRUCT , KW_IMPL , KW_THREAD , KW_PURE
395 KW_ENUM , KW_MATCH , KW_TYPE , KW_VOID , KW_INT , KW_ARENA ,
396 KW_U8 , KW_I8 , KW_U16 , KW_I16 , KW_U32 , KW_I32 , KW_U64 , KW_I64 ,
397 KW_F32 , KW_F64 , KW_BOOL , KW_CHAR , KW_STRING , KW_TRUE , KW_FALSE ,
398 KW_IMPORT , KW_EXPORT , KW_FROM , KW_FFI , KW_MAP ,
399 KW_DURATION , KW_INSTANT ,
400
401 ( * No shared mutability without atomics ! * )
402 KW_NEW , KW_RESERVE , KW_SHARED , KW_ATOMIC , KW_WEAK , KW_STATIC , KW_MUT ,
403
404 KW_SYSTEM , KW_ACTION , KW_REQUIRES , KW_ENSURES ,
405 KW_INVARIANT , KW_PROPERTY , KW_FORALL , KW_EXISTS , KW_ALWAYS , KW_EVENTUALLY , KW_NEXT , KW_UNTIL ,
406 KW_RELEASE , KW_FAULT , KW_RAISE
407
408 ( * Operators and Delimiters : Arithmetic Wraps )
409 '=' , '+=' , '-=' , '*=' , '/=' , '+' , '-' , '*' , '/' , '%' , '&' , '==' , '!=' , '<' , '<=' , '>' , '>=' ,
410 '!' , '&&' , '||' , '|' , '^' , '~' , '<<' , '>>' , '(' , ')' , '{' , '}' , '[' , ']' ,
411 ',' , ';' , '.' , ':' , '::' , '?' , '?.' , '??' , '...' , '..=' , '..' , '->' , '_' , '=>'
412
413 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="ff3661e49117c">
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 daemonize

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 This will enable you to set watches on directories and enable incremental builds, and maintain stateful sessions.
Why This is ideal or developers who prefer working in the terminal, or for anyone using AI tooling.

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.

Types

Quick Reference

Category Surface Syntax Examples Notes
Booleans bool true , false Logical values.
Integers (fixed width) u8 i8 u16 i16 u32 i32 u64 i64 let n: i32 = 42; Signed/unsigned bit‑widths.
Integer (platform) int let n: int = 1; Implementation/default integer.
Floats f32 f64 let x: f64 = 3.14; IEEE‑754.
Char char 'A' Unicode scalar.
String string "hello" Immutable text; multi‑line strings supported.
Void / Unit void fn foo () -> void {} Functions that return nothing.
Time Types Instant , Duration let i: Instant = std::now(); Specialized i64 types for time measurement. Instant is a time point; Duration is a span.
Optional T? User? , i32? null / none allowed; use match , ?. , ?? .
None (value) null / none The distinguished empty value; typed as T? .
Reference (borrow) &T &User Borrowed reference. Analyzer enforces aliasing rules.
Arrays / Slices T[] , T[N] i32[] , byte[32] Dynamic slice vs fixed length (compile‑time N ).
Maps map(K, V) map(string, i32) Built‑in map/dictionary.
Function Types fn(params) -> R pure fn(i32)->i32 Modifiers: pure , thread , async (values).
Closures (function value) let f = fn(x:i32)->i32 { return x+1 }; Captures env; typed as fn(...) -> ... .
Structs (nominal) struct Name ... then Name(...) Point , Option(T) User‑defined records; may be parameterized.
Enums (sum types) enum Name { ... } Result(T,E) Tagged variants; pattern‑matched.
Modules (qualified name) pkg::Type Namespacing; module itself has a type internally.
Futures Future(T) returned by async fn Produced by async functions; await yields T .

Postfix builders: you can apply ? , array [...] , and type application ( ... ) to base types where applicable.

Nominal & Parameterized Types

Structs and enums define named (nominal) types. They may take type parameters and, as the language evolves, const parameters . Use ordinary type application:

Example
0 struct Pair ( T , U ) { first : T ; second : U ; }
1 let p : Pair ( i32 , string ) = { first : 1 , second : "hi" } ;
2
3 enum Option ( T ) { Some ( T ) , None }
4 let x : Option ( i32 ) = Option : : Some ( 3 ) ;

For functions, Aela uses a unified parameter list with a ; split: fn id(T; x:T) -> T (compile‑time params before ; , run‑time after). See the Functions section.

Optionals & None vs. Void

  • T? means maybe a `T` . Use match , ?. , or ?? to handle absence.
  • none / null is the empty value that inhabits optional types.
  • void means no value is returned (a function that completes for effects only). It is not the same as none .
Example
0 fn find_user ( ; id : i32 ) - > User ? { /* ... */ }
1 let u = find_user ( 42 ) ? ? default_user ( ) ;

Arrays & Maps

  • Dynamic slices: T[] — size known at run time; indexing and length available via stdlib.
  • Fixed arrays: T[N] — size is part of the type; N must be compile‑time evaluable.
  • Maps: map(K, V) — associative container; keys and values are regular types.
Example
0 let bytes : u8 [ 32 ] ;
1 let names : string [ ] ;
2 let counts : map ( string , i32 ) ;

References (borrowing)

&T is a borrowed reference to T .

  • Mutable vs immutable is governed by parameter modifiers ( mut ) and aliasing rules enforced by the analyzer (no shared mutability without atomics).
  • Think of &T as a view ; the underlying ownership model is enforced by the compiler.
Example
0 fn length ( ; s : & string ) - > i32 { return std : : length ( s ) ; }

Function & Closure Types

Function types and values share the same shape: fn(params) -> Return with optional modifiers.

Example
0 // Type position
1 let f : pure fn ( i32 ) - > i32 = add_one ;
2
3 // Value (closure) position
4 let g = pure fn ( x : i32 ) - > i32 { return x + 1 ; } ;

Modifiers:

  • pure — no observable side effects; enables stronger reasoning/optimizations.
  • thread — safe to run in a separate thread (may be combined with pure ).
  • async — produces a Future(Return) ; use await to get the value.

Unified parameter list (declarations):

Example
0 fn map ( T , U ; f : fn ( T ) - > U , xs : T [ ] ) - > U [ ] { /* ... */ }

Enums (sum types)

Enums declare a closed set of variants and are exhaustively pattern‑matched.

"err" } }" lang="aela" title="Example" id="e1732e6ca2be3">
Example
0 enum Result ( T , E ) { Ok ( T ) , Err ( E ) }
1
2 fn handle ( T , E ; r : Result ( T , E ) ) - > string {
3 return match ( r ) {
4 Result : : Ok ( x ) = > "ok" ,
5 Result : : Err ( e ) = > "err"
6 }
7 }

Futures & Async

async functions return future values that represent a computation in progress. Conceptually, the type is Future(T) , and await yields T .

Example
0 async fn fetch ( ; url : string ) - > string { /* ... */ }
1
2 fn use_it ( ; ) - > void {
3 let fut = fetch ( " / data" ) ;
4 let s = await fut ; // s: string
5 }

How the Compiler Thinks About Types (High‑Level)

Internally, every type has a kind (primitive, struct, enum, map, array, function, optional, reference, etc.) and a canonical signature string (e.g., "fn(i32)->bool" ). The compiler interns types in a cache so that equality checks are fast pointer comparisons.

  • Compatibility: types_are_compatible(dst, src) determines assignment/call compatibility (more than just raw equality when coercions are allowed).
  • Optional & None: represented distinctly ( TYPE_OPTIONAL around a base, and a special TYPE_NONE ).
  • Closures: carry a function type plus captured environment.
  • Modules: form a namespace; the module itself has a type used by the compiler for resolution.

You normally don’t see these details, but they explain why types print consistently and compare cheaply.

Minimal Grammar Reminders (surface)

  • Map: map(K, V)
  • Optional: T?
  • Reference: &T
  • Array: T[expr] (fixed) or T[] (slice)
  • Function type: pure fn(T1, T2) -> R
  • Type application: Name(T, U)

For advanced material (refinements { x: T where φ } and dependent forms like Vec(T, N) ), see the companion doc.

Best Practices

  • Prefer named aliases for recurring shapes: type Port = { p: i32 where 1024 <= p && p <= 65535 };
  • Use ? sparingly; prefer sum types (e.g., Result ) when you want the caller to handle both cases explicitly.
  • Keep function types pure when possible; it improves composability.
  • Choose fixed arrays ( T[N] ) when size is intrinsic and enables better checking/optimization.

Operators

Precedence Operator(s) Description Associativity
1 (Lowest) = , += , -= , *= , /= Assignment / Compound 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

Literals

Literals are notations for representing fixed values directly in source code. Aela supports a rich set of literals for primitive and aggregate data types.


Numeric Literals

Numeric literals represent number values. They can be integers or floating-point numbers and can include type suffixes and numeric separators for readability.

Integer Literals

Integer literals represent whole numbers. They can be specified in decimal or hexadecimal format.

Decimal: Standard base-10 numbers (e.g., `123`, `42`, `1000`). Hexadecimal: Base-16 numbers, prefixed with 0x (e.g., 0xFF , 0xdeadbeef ). Numeric Separator: * The underscore _ can be used to improve readability in long numbers (e.g., 1_000_000 , 0xDE_AD_BE_EF ).

By default, an integer literal is of type i32 . You can specify a different integer type using a suffix.

Suffix Type Range
i8 8-bit signed −128 to 127
u8 8-bit unsigned 0 to 255
i16 16-bit signed −32,768 to 32,767
u16 16-bit unsigned 0 to 65,535
i32 32-bit signed −2,147,483,648 to 2,147,483,647
u32 32-bit unsigned 0 to 4,294,967,295
i64 64-bit signed −9,223,372,036,854,775,808 …
u64 64-bit unsigned 0 to 18,446,744,073,709,551,615

Example:

Example
0 let default_int = 100 ; // Type: i32
1 let large_int = 1_000_000 ; // Type: i32
2 let unsigned_val = 42u32 ; // Type: u32
3 let id = 0x1A4F ; // Type: i32
4 let big_id = 0xDE_AD_BE_EFu64 ; // Type: u64

Floating-Point Literals

Floating-point literals represent numbers with a fractional component.

Decimal Notation: `3.14`, `0.001`, `1.0` Scientific Notation: 1.5e10 ( 1.5 × 10¹⁰ ), 2.5e-3 ( 2.5 × 10⁻³ ) Numeric Separator: * _ can be used in integer or fractional parts (e.g., 1_234.567_890 )

By default, a floating-point literal is of type f64 .

Suffix Type Precision
f32 32-bit float \~7 decimal digits
f64 64-bit float \~15 decimal digits

Example:

Example
0 let pi = 3 . 14159 ; // Type: f64
1 let small_val = 1e - 6 ; // Type: f64
2 let gravity = 9 . 8f32 ; // Type: f32
3 let large_float = 1_234 . 567 ; // Type: f64

Duration Literals

Duration literals represent a span of time and are of the first-class Duration type. They are formed by an integer or floating-point literal followed by a unit suffix.

Suffix Unit Description
ns Nanoseconds The smallest unit of time
us Microseconds 1,000 nanoseconds
ms Milliseconds 1,000 microseconds
s Seconds 1,000 milliseconds
min Minutes 60 seconds
h Hours 60 minutes
d Days 24 hours

Example:

Example
0 let timeout : Duration = 250ms ;
1 let retry_interval : Duration = 3s ;
2 let frame_time : Duration = 16 . 6ms ;
3 let long_wait : Duration = 1 . 5h ;

Boolean Literals

Boolean literals represent truth values and are of type bool .

`true`: Represents logical truth. false : Represents logical falsehood.

Example:

Example
0 let is_ready : bool = true ;
1 let has_failed : bool = false ;

Character Literals

A character literal represents a single Unicode scalar value (stored as a u32 ). Enclosed in single quotes ( ' ).

Example:

Example
0 let initial : char = 'P' ;
1 let newline : char = '\n' ;
2 let escaped_quote : char = '\' ' ;

String Literals

String literals represent sequences of characters and are of type string . Aela supports two forms:

Single-Line Strings

Enclosed in double quotes ( " ). Support escape sequences:

`\n` newline \r carriage return `\t` tab \\ backslash * \" double quote

Example:

Example
0 let greeting = "Hello , World ! \n" ;

Multi-Line Strings

Enclosed in backticks (` ` ). These are *raw*: preserve all whitespace and newlines. Only \` (escaped backtick) and \\\` (escaped backslash) are special.

Example:

Example
0 let query = `
1 SELECT
2 id ,
3 name
4 FROM
5 users ;
6 ` ;

Aggregate Literals

Aggregate literals create container values like arrays and structs.

Array Literals

A comma-separated list inside [] . Elements must share a compatible type. Empty arrays require a type annotation.

Example:

Example
0 let numbers = [ 1 , 2 , 3 , 4 , 5 ] ; // inferred i32[]
1 let names : string [ ] = [ ] ; // explicit annotation required

Struct Literals

Create a struct instance with {} .

Named Struct Literal: Prefix with the struct type. Field Shorthand: Use x instead of x: x . Spread Operator: * Use ... to copy fields from another struct.

Example:

Example
0 struct Point {
1 x : i32 ;
2 y : i32 ;
3 }
4
5 let p1 = Point { x : 10 , y : 20 } ;
6
7 let x = 15 ;
8 let p2 = Point { x , y : 30 } ; // shorthand
9
10 let p3 = Point { . . . p1 , y : 40 } ; // p3 = { x: 10, y: 40 }

All Literals in Action

bool { // Numbers let int_val = 1_000; let hex_val = 0xFF; let sixty_four_bits = 12345u64; let float_val = 99.5f32; let scientific = 6.022e23; // Durations let http_timeout = 30s; let animation_frame = 16.6ms; // Booleans let is_active = true; // Characters let a = 'a'; // Strings let single = "A single line."; let multi = `A multi-line string.`; // Aggregates let ids: u64[] = [101u64, 202u64, 303u64]; let name = "Alice"; let user = User { id: 1u64, name, is_active }; t.ok(true, "Literals demonstrated"); return true; }" lang="aela" title="Example" id="678ae36ee2001">
Example
0 import { Tap } from " . . / . . / . . / lib / test . ae" ;
1
2 struct User {
3 id : u64 ;
4 name : string ;
5 is_active : bool ;
6 }
7
8 export fn all_literals_example ( t : & Tap ) - > bool {
9 // Numbers
10 let int_val = 1_000 ;
11 let hex_val = 0xFF ;
12 let sixty_four_bits = 12345u64 ;
13 let float_val = 99 . 5f32 ;
14 let scientific = 6 . 022e23 ;
15
16 // Durations
17 let http_timeout = 30s ;
18 let animation_frame = 16 . 6ms ;
19
20 // Booleans
21 let is_active = true ;
22
23 // Characters
24 let a = 'a' ;
25
26 // Strings
27 let single = "A single line . " ;
28 let multi = `A
29 multi - line
30 string . ` ;
31
32 // Aggregates
33 let ids : u64 [ ] = [ 101u64 , 202u64 , 303u64 ] ;
34 let name = "Alice" ;
35 let user = User { id : 1u64 , name , is_active } ;
36
37 t . ok ( true , "Literals demonstrated" ) ;
38 return true ;
39 }

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="a2c2d2e484d92">
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 ;

Optional

The Optional type 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="ea0d1620f4b28">
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_sb_append: fn(&StringBuilder, string); ffi ae_sb_destroy: fn(&StringBuilder); // <-- The cleanup function fn main() -> int { let sb = ae_sb_new(); ae_sb_append(sb, "hello"); // CRITICAL: You must call destroy when you are done. ae_sb_destroy(sb); // Using `sb` after this point is UNDEFINED BEHAVIOR. // ae_sb_append(sb, " world"); // <-- ERROR! return 0; }" lang="aela" title="Example: Managing Lifetimes" id="33b615ffe6335">
Example: Managing Lifetimes
0 `` `aela
1 import { StringBuilder } from " . / runtime . ae" ;
2
3 // FFI Declarations for a C string builder
4 ffi ae_sb_new : fn ( ) - > & StringBuilder ;
5 ffi ae_sb_append : fn ( & StringBuilder , string ) ;
6 ffi ae_sb_destroy : fn ( & StringBuilder ) ; // <-- The cleanup function
7
8 fn main ( ) - > int {
9 let sb = ae_sb_new ( ) ;
10 ae_sb_append ( sb , "hello" ) ;
11
12 // CRITICAL: You must call destroy when you are done.
13 ae_sb_destroy ( sb ) ;
14
15 // Using `sb` after this point is UNDEFINED BEHAVIOR.
16 // ae_sb_append(sb, " world"); // <-- ERROR!
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 }

Duration & Instant

Time-related bugs are notoriously common and usually subtle. The root cause is frequently quantity confusion: when a plain number like 10 or lastUpdated is used, its unit is ambiguous. Does it represent 10 seconds, 10 milliseconds, or 10 microseconds? The programmer's intent is lost, hidden in variable names or documentation, leading to misinterpretations and errors.

Duration a first-class type with built-in literals. This design has two major benefits:

Improved Comprehension: Code becomes self-documenting. A value like 250ms is unambiguous; it cannot be mistaken for seconds or any other unit. This clarity makes code easier to read, write, and maintain. An expression like let timeout = 1s + 500ms; is immediately understandable without needing to look up function definitions or comments.

Clarified Intent & Type Safety: By distinguishing Duration from numeric types, the compiler can enforce correctness. You cannot accidentally add a raw number to a duration (5s + 3 is a compile-time error), which prevents nonsensical operations. Function signatures become more expressive and safe, for example fn sleep(for: Duration). This forces the caller to be explicit (e.g., sleep(for: 500ms)), eliminating the possibility of passing a value with the wrong unit.

The Duration type moves the handling of time units from a convention to a language-enforced guarantee, significantly reducing a whole class of common bugs.

Literals & type

  • Literals: INT_LITERAL DurationUnit or FLOAT_LITERAL DurationUnit (e.g., 250ms , 1.5s ).
  • Type: Duration is a first-class scalar quantity (internally monotonic-time ticks; implementation detail).
  • Sign: Duration is signed . -5s is allowed via unary minus.
  • No implicit numeric conversions: Duration never implicitly converts to/from numeric types.

Unary

Form Result Notes
+d Duration no-op
-d Duration negation; overflow is checked

Binary with Duration

Expr Result Allowed? Notes
d1 + d2 Duration Yes checked overflow
d1 - d2 Duration Yes checked overflow
d1 * n Duration Yes n is integer (any int type); checked overflow
n * d1 Duration Yes symmetric
d1 / n Duration Yes n integer; trunc toward zero ; div-by-zero error
d1 / d2 F64 Yes dimensionless ratio (floating)
d1 % d2 Duration Yes remainder; d2 != 0
d1 % n No disallowed
d1 & d2 - No no bitwise ops on Duration (including ^ , << , >> )
d1 && d2 No not booleans

Float scalars

Disallowed by default: Duration * F64 , Duration / F64 Rationale: silent precision loss. Provide library helpers instead (e.g., Duration::from_seconds_f64(x) ).

Comparison

Expr Result Allowed?
d1 == d2 Bool Yes
d1 != d2 Bool Yes
d1 < d2 , <= , > , >= Bool Yes
d1 == n , d1 < n No (no cross-type compare)

Instant

Expr Result Allowed? Notes
t1 + d Instant Yes checked overflow
d + t1 Instant Yes commutes
t1 - d Instant Yes checked overflow
t1 - t2 Duration Yes difference
t1 + t2 , t1 * d No nonsensical

Casting / construction

  • Allowed: explicit constructors, e.g. Duration::from_ms(250) , Duration::seconds_f64(1.5) .
  • Disallowed: implicit casts ( (int) d , (f64) d ).

Overflow & division semantics

  • Checked arithmetic by default: + , - , * on Duration panic on overflow (or trap).
  • Provide library variants:
  • checked_add , checked_sub , checked_mulOption
  • saturating_add , saturating_sub , saturating_mul
  • Division: d / n truncates toward zero; n must be nonzero.
  • d / d returns F64 (no truncation).

Examples

Example
0 let a : Duration = 250ms + 1s ; // ok
1 let b : Duration = 2 * 500ms ; // ok (int * Duration)
2 let c : Duration = ( 5s - 1200ms ) ; // ok, can be negative
3 let r : f64 = ( 750ms / 1 . 5s ) ; // ok: Duration / Duration -> F64 == 0.5
4
5 let bad1 = 1 . 2 * 5s ; // error: float scalar not allowed
6 let bad2 = 5s + 3 ; // error: no Duration + Int
7 let bad3 = 5s < 1000 ; // error: cross-type compare
8 let bad4 = 5s & 1s ; // error: bitwise on Duration

Suffix/literal interaction (clarity)

  • 1s + 500ms is fine; units normalize.
  • 1.5s is legal as a literal; it’s converted to integral ticks (ns) with rounding toward zero during lex/const-eval. (If you prefer bankers-rounding, specify that instead.)
  • No ambiguity with range tokens: ensure lexer orders '...' , '..=' , '..' (longest first) and treats ms/min etc. as unit suffixes , not identifiers.

Arenas

Overview

Aela's has a three-part model for safe, dynamic memory management. The model is designed to provide explicit, and verifiable memory control for both hosted (OS) and freestanding (bare-metal) environments.

The model consists of:

  • An intrinsic Arena type for memory provisioning.
  • A transactional reserve statement for scoped memory reservation.
  • A context-aware new keyword for object allocation.

The implementation is based on compile-time AST tagging, ensuring zero runtime overhead and inherent safety for asynchronous and multi-threaded code.

The Arena

The Arena is a primitive type known to the compiler, used for managing a block of memory.

Syntax

An Arena is provisioned using a special form of the new expression.

Example
0 // For freestanding targets (bare-metal)
1 'let' IDENTIFIER ':' 'Arena' '=' 'new' 'static' '{' 'size' ':' ConstantExpression '}' ';'
2
3 // For hosted targets (OS)
4 'let' IDENTIFIER ':' 'Arena' '=' 'new' '{' '}' ';'

Semantics

new {} : A runtime operation for hosted environments. It calls the system allocator (e.g., malloc). This expression is fallible and should be treated as returning an Option(Arena).

new static { size: ... } : A compile-time instruction. It directs the linker to reserve a fixed-size block of memory in the final binary's static data region (e.g., .bss). This is the primary mechanism for provisioning memory on bare metal.

The reserve Statement (Transactional Reservation)

The reserve statement transactionally reserves memory from an Arena for a specific lexical scope.

Syntax

Example
0 'reserve' size_expr 'from' arena_expr Block [ 'else' Block ]

Semantics

The reserve statement attempts to acquire size_expr bytes from the given arena_expr.

If the reservation is successful, the first Block is executed.

If the reservation fails (the arena has insufficient capacity), the else Block is executed.

A successful reservation creates a special allocation context that is active for the duration of the success block and any functions called from within it.

The new Keyword (Allocation)

The new keyword creates an object instance. Its behavior is context-dependent and verified by the compiler.

Semantics

The compiler enforces three distinct behaviors for new:

Hosted Default Context: When compiling for a hosted target and not inside a reserve block, new allocates from the system heap.

Freestanding Default Context: When compiling for a bare-metal target and not inside a reserve block, a call to new is a compile-time error. This ensures no accidental heap usage on constrained devices.

reserve Context: Inside a successful reserve block, new allocates from the reserved memory. This allocation is infallible and returns a value of type T, not Option(T).

Complete Bare-Metal Example

Example
0 // 1. PROVISIONING (Compile-Time)
1 // The compiler reserves 64KB of static memory.
2 var MY_ARENA : Arena = new static { size : 65536 } ;
3
4 // This function is only called from within a `reserve` block, so `new` is safe.
5 fn create_header ( ) - > Header {
6 // This `new` call inherits the reservation context from its caller.
7 return new shared Header { } ;
8 }
9
10 fn create_packet ( ) - > Option ( Packet ) {
11 // 2. RESERVATION (Transactional Check)
12 reserve 2048b from MY_ARENA {
13 // This block is entered only if the reservation succeeds.
14
15 // 3. ALLOCATION (Infallible)
16 // `new` is now infallible and allocates from MY_ARENA.
17 let packet = new shared Packet { } ;
18 packet . header = create_header ( ) ;
19
20 return Some ( packet ) ;
21 } else {
22 // The reservation failed; handle the error.
23 return None ;
24 }
25 }

Buffers

Introduction

Buffer(T) is a fundamental intrinsic type that provides a low-level, direct interface to a contiguous block of allocated memory (from where depending on if you do or don't use a reserve block). It is the primitive that higher-level, safe collection types like Vec(T) and String are built.

As an intrinsic , the compiler has special knowledge of Buffer(T) , allowing it to enforce powerful compile-time guarantees about memory ownership and borrowing. It's important to understand that Buffer(T) is intentionally designed as an unsafe primitive . Its core operations do not perform runtime bounds checking, providing a zero-overhead foundation for performance-critical code and the standard library. Your code can make it safe

Core Concepts

Representation

A Buffer(T) is a "fat pointer" containing two fields:

  1. A raw pointer to the start of the memory block.
  2. The capacity of the buffer (the total number of elements it can hold).

A Buffer(T) only tracks its total capacity. It does not track how many elements are currently initialized or in use (its length ). This responsibility is left to higher-level abstractions.

Ownership

The Buffer(T) value is the unique owner of the memory it controls. The compiler's verifier enforces this ownership model strictly:

  • When a Buffer(T) is moved, ownership is transferred. The original variable can no longer be used.
  • When a Buffer(T) variable goes out of scope, its memory is automatically deallocated.
  • The std::buffer::drop intrinsic can be used to explicitly deallocate the memory, consuming the buffer variable.

This model guarantees at compile time that the buffer's memory is freed exactly once, eliminating memory leaks and double-free errors.

The Intrinsic API

The following functions provide the raw manipulation capabilities for Buffer(T) .

std::buffer::alloc

Signature std::buffer::alloc(capacity: int, elem_size: int) -> Buffer(T)
Description Allocates an uninitialized buffer on the heap. The element type T is inferred from the context.

std::buffer::write

Signature std::buffer::write(mut buf: Buffer(T), index: int, value: T)
Description Writes a value into the buffer at a given index. This is an unsafe operation and does not perform bounds checking.

std::buffer::read

Signature std::buffer::read(buf: &Buffer(T), index: int) -> T
Description Reads the value from the buffer at a given index. This is an unsafe operation and does not perform bounds checking.

std::buffer::capacity

Signature std::buffer::capacity(buf: &Buffer(T)) -> int
Description Returns the total number of elements the buffer can hold. This operation is always safe.

std::buffer::drop

Signature std::buffer::drop(buf: Buffer(T))
Description Explicitly deallocates the buffer's memory. The verifier prevents any subsequent use of the buf variable.

std::buffer::view

Signature std::buffer::view(buf: &Buffer(T), start: int, len: int) -> &T[]
Description Creates an immutable slice ( &T[] ) that borrows a portion of the buffer's data. This is an unsafe operation as it does not check if the range is in bounds.

std::buffer::slice

Signature std::buffer::slice(mut buf: Buffer(T), start: int, len: int) -> T[]
Description Creates a mutable slice ( T[] ) that mutably borrows a portion of the buffer's data. This is an unsafe operation as it does not check if the range is in bounds.

The Safety Model: A Layered Approach

The safety of Buffer(T) and its ecosystem is best understood as a series of layers, where stronger guarantees are built upon more primitive ones.

Layer 1: The Unsafe Buffer(T) Primitive

The intrinsic functions themselves form the base layer. They are designed to be as close to the machine as possible. std::buffer::write compiles to a single store instruction, and std::buffer::read to a single load . They do not have bounds checks because they are meant to be the absolute zero-cost building blocks. This layer is primarily intended for the authors of the standard library and other highly-optimized, low-level code.

Layer 2: Compile-Time Safety via the Verifier

The compiler's verifier (or "borrow checker") provides the next layer of safety, and it does so with zero runtime cost . It enforces:

  • Ownership & Lifetimes : Guarantees that a Buffer is dropped exactly once and that any view or slice cannot outlive the Buffer it borrows from.
  • Aliasing Rules : Prevents data races by ensuring that you cannot have a mutable borrow ( T[] ) at the same time as any other borrow of the same data.

These checks happen entirely at compile time.

Layer 3: Provable Safety via Refinement Types

This is the highest level of safety, allowing for the creation of truly safe abstractions on top of the unsafe Buffer primitive. The language allows types to be "refined" with predicates that the compiler must prove.

A safe Vec(T) type in the standard library would not expose the unsafe read / write intrinsics. Instead, it would provide methods whose signatures use refinement types to enforce correctness:

Example
0 // Hypothetical safe API for a Vec(T) built on Buffer(T)
1 fn Vec . get ( & self , index : { i : int where i > = 0 & & i < self.size() }) -> & T {
2 // The compiler has already proven the index is valid, so we can
3 // safely call the unsafe intrinsic with no additional runtime check.
4 return std : : buffer : : view ( & self . buffer , index , 1 ) [ 0 ] ;
5 }

This system provides two powerful benefits:

  1. Compile-Time Proof : If you call my_vec.get(5) and the compiler can prove the vector's length is greater than 5, the safety is guaranteed and the generated code is just a direct memory access. The safety check has zero runtime cost.
  1. Compiler-Enforced Runtime Checks : If the compiler cannot prove the index is safe (e.g., it comes from user input), it will issue a compile-time error. This forces the programmer to add an explicit if check, which provides the compiler with the proof it needs inside the if block.
Example
0 let i = get_user_input ( ) ;
1 if ( i > = 0 & & i < my_vec . size ( ) ) {
2 // This is now valid. The compiler accepts the call because
3 // the 'if' condition satisfies the refinement type's predicate.
4 let element = my_vec . get ( i ) ;
5 }

This layered approach is the essence of a zero-cost abstraction: safety is guaranteed by the compiler wherever possible, and runtime costs are only incurred when logically necessary and are made explicit in the program's control flow.

Concurrency

Aela's concurrency is built on two orthogonal keywords, async and thread , that modify function declarations. These provide a clear, explicit syntax for defining concurrent work. The runtime manages a thread pool to execute these tasks, enabling both I/O-bound concurrency and CPU-bound parallelism that work in concert.

Core Keywords: async and thread

It is crucial to understand that `async` and `thread` are two separate modifiers with distinct meanings. Even though they are designed to work in a way that feels cohesive.

async

Marks a function as pausable . An async function can use the await keyword to non-blockingly wait for I/O or other long-running operations. It primarily relates to concurrency .

The decision to have a langauge-native async engine provides significant advantages for compile-time determinism.

thread

Marks a function as a parallel task . Calling a thread fn is a special operation that is non-blocking. It immediately submits the function to the runtime's thread pool for execution and returns a Task handle. It primarily relates to parallelism .

These keywords can be combined to define tasks with different behaviors:

Combination Meaning Primary Use Case
fn A regular, blocking function. Standard synchronous logic.
async fn A pausable, awaitable function. I/O-bound work on the current thread.
thread fn A function that runs in parallel in another thread. It cannot use await . CPU-intensive, blocking computations that should not stall the main thread.
async thread fn A pausable function that runs in parallel in a thread. A self-contained parallel task that performs its own I/O (e.g., a network client).

Defining and Spawning Tasks

Threaded tasks are ideal for the parallel processing of CPU intensive workloads.

Example
0 // This function is defined as a parallel task.
1 thread fn process_data ( source : DataSource ) - > Report {
2 return generate_report ( data ) ;
3 }

But sometimes a threaded task to compartmentalize work. For example, you might want some async processing to happen in another thread, separately from a UI thread.

The async keyword is optional and is used if the task needs to await I/O. The function's return type defines the final value returned when the task is complete.

Example
0 // This function is defined as a parallel task that is also async.
1 async thread fn process_data ( source : DataSource ) - > Report {
2 let data = await source . read ( ) ;
3 // ...
4 return generate_report ( data ) ;
5 }

Calling a function marked thread is a non-blocking operation. It starts the task in the background and immediately returns a Thread handle.

Example
0 // This call is non-blocking and returns a handle.
1 let handle : Thread ( Report ) = process_data ( my_source ) ;
2
3 // Await the handle to get the final result.
4 let report : Report = await handle . join ( ) ;

Structured Parallelism: thread { ... } Block

The thread block is used to run a group of tasks in parallel and wait for all of them to complete before continuing.

Example
0 async fn get_dashboard_data ( ) - > Dashboard {
1 var user : User ;
2 var orders : [ Order ] ;
3
4 // This block runs its internal async calls in parallel.
5 thread {
6 let user_task = fetch_user_profile ( 123 ) ;
7 let orders_task = fetch_recent_orders ( 123 ) ;
8
9 // The block implicitly awaits both before exiting.
10 user = await user_task ;
11 orders = await orders_task ;
12 }
13
14 // This line is only reached after both tasks are complete.
15 return Dashboard ( user , orders ) ;
16 }

Channels

Channels are used for streaming communication between threads. The type Channel(T) is a valid type according to the TypeApplication grammar rule.

Example
0 // Create a channel that transports string values.
1 let my_channel : Channel ( string ) = new { } ;

Streams

This pattern pauses the current function to process each message from a channel sequentially.

Example
0 // The `for await` loop consumes the receiver.
1 for await ( let message : string in my_channel . receiver ) {
2 process_message ( message ) ;
3 }

Events

This registers a handler and immediately returns, allowing the current function to continue its work.

Example
0 // `on_receive` takes a function expression as an argument.
1 my_channel . receiver . listen ( ( message : string ) - > void {
2 io : : print ( "Event received : { } " , message ) ;
3 } ) ;
4
5 // ... code here continues to run without blocking ...

Integrated Vehicle Safety vs. Aftermarket Parts

Think of it like the safety systems in a car.

A Library-Based Ecosystem (like Rust's): This is like buying a car chassis and then adding safety features yourself. You buy airbags from one company, an anti-lock braking system from another, and traction control from a third. They might be excellent components, but they weren't designed to work together as a single, cohesive system.

A Built-in Scheduler (Aela's model): This is like buying a modern car where the manufacturer has designed the airbags, ABS, traction control, and crumple zones to work together as a single, integrated safety system. It's tested as a whole and provides guarantees that the individual parts can't.

Here are the specific safety wins this integration provides.

  1. Compile-Time Data-Race Prevention

Because the scheduler is part of the language, the compiler has a deep understanding of what it means to "cross a thread boundary." It can enforce Send and Sync rules at compile-time. This means it's a syntax error to try to send non-thread-safe data into a thread fn or across a channel, completely eliminating an entire class of data-race bugs before the program can even run.

  1. A Single, Safe Bridge for Blocking Code

As we discussed, blocking in an async context is a huge foot-gun. A built-in runtime provides one, official, and well-defined function for handling this: std::task::run_blocking(). This prevents a scenario where different libraries provide their own, subtly different (and potentially unsafe) ways of handling blocking calls, which would lead to confusion and bugs.

  1. Guaranteed Structured Concurrency

The thread { ... } block is a major safety feature. Because it's a language construct powered by the built-in scheduler, the compiler can absolutely guarantee that all child tasks are completed before the parent function is allowed to continue. This prevents "leaked" tasks and makes error handling robust and predictable. In a library, this guarantee might be weaker or easier to accidentally bypass.

  1. Predictable Task Lifecycles

With a built-in scheduler, the behavior of a Task handle is predictable across the entire ecosystem. The rule that a handle will detach on drop is a language-level guarantee. This prevents situations where one library's handle might join on drop while another's aborts, leading to surprising behavior and resource leaks.

In short, a built-in scheduler allows Aela to treat concurrency as a core feature, subject to the same rigorous, compile-time safety analysis as the rest of the language.

Pool Control & Oversubscription

The Aela runtime is built on a work-stealing thread pool that defaults to using the number of logical CPU cores available (std::thread::available_parallelism()).

Pool Size & Pinning: The pool size can be overridden at startup via an environment variable (e.g., AELA_THREAD_COUNT=4). Fine-grained control like core pinning and thread priorities is considered a low-level OS feature and is not exposed through the high-level async/thread API. For expert use cases, a separate std::os::thread module could provide these unsafe, platform-specific controls.

On tiny targets without an OS, the runtime can be compiled in a "pool disabled" mode, using a single-threaded cooperative scheduler.

Oversubscription: The runtime's global task queue is bounded (e.g., a default capacity of 256 tasks). Calling a thread fn is cheap, but if the task queue is full, the call itself becomes an async operation. It will pause the calling function until space becomes available in the queue. This provides natural back-pressure and prevents developers from overwhelming the system.

Example
0 async fn io_bound_work1 ( ) - > int { // This already works
1 await doSomething ( ) ; // This already works
2 return 0 ;
3 }
4
5 thread fn compute_bound_work1 ( c : & Channel ( int ) ) - > int {
6 c . send ( 42 ) ; // some new data is ready
7 return 0 ; // the thread is finished
8 }
9
10 async fn main ( ) {
11 let c : Channel ( int ) = new shared { } ;
12
13 c . receive = fn ( i : int ) - > {
14 io : : print ( " { } " , i ) ;
15 } ;
16
17 let h : int = await compute_bound_work1 ( c ) ; // Actually returns `Task(int)` and resolves it
18 let i : int = await io_bound_work1 ( ) ; // Actually returns `Future(int)` and resolves it
19
20 let a : int [ ] = await std : : all ( [
21 compute_bound_work2 ( ) , // dispatched to the scheduler
22 compute_bound_work2 ( ) , // dispatched to the scheduler
23 io_bound_work2 ( ) // dispatched to the eventloop
24 ] ) ;
25
26 let h2 : Task ( int ) = compute_bound_work1 ( c ) ; // returns `Task(int)` lazy, not executing.
27
28 let i2 : int = await h2 ; // starts executing (ie, `await compute_bound_work1();`)
29 }
Example
0 enum RecvJob {
1 Msg ( Job ) ,
2 Cancelled ,
3 Timeout
4 }
5
6 async fn next_recv ( jobs : & Channel ( Job ) ) - > RecvJob {
7 let j : Job = await jobs . recv ( ) ;
8 return RecvJob : : Msg ( j ) ;
9 }
10
11 async fn on_cancel ( tok : & CancellationToken ) - > RecvJob {
12 await tok . cancelled ( ) ;
13 return RecvJob : : Cancelled ;
14 }
15
16 async fn after ( ms : Int ) - > RecvJob {
17 await timer . after_ms ( ms ) ;
18 return RecvJob : : Timeout ;
19 }
20
21 thread fn handle ( job : Job ) - > int {
22 // something interesting and cpu intensive
23 return 0 ;
24 }
25
26 async fn run_worker ( jobs : & Channel ( Job ) , tok : & CancellationToken ) - > int {
27 while ( true ) {
28
29 // `await select` atomically starts all provided cold awaitables, yields
30 // the first one to complete (as a tagged case for its arm), and cancels the rest.
31
32 let evt : RecvJob = await std : : select ( [
33 next_recv ( jobs ) ,
34 on_cancel ( tok ) ,
35 after ( 250 )
36 ] ) ;
37
38 match ( evt ) {
39 RecvJob : : Msg ( job ) = > { await handle ( job ) ; }
40 RecvJob : : Cancelled = > { break ; }
41 RecvJob : : Timeout = > { io : : print ( "tick" ) ; }
42 }
43 }
44 return 0 ;
45 }

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

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.

Formal Grammar Spec

' ReturnType RefinementType ::= '{' IDENTIFIER ':' Type KW_WHERE Expression '}' PrimitiveType ::= KW_U8 | KW_I8 | KW_U16 | KW_I16 | KW_U32 | KW_I32 | KW_U64 | KW_I64 | KW_F32 | KW_F64 | KW_BOOL | KW_CHAR | KW_STRING | KW_INT | KW_ARENA TypeArguments ::= '(' [ Type { ',' Type } ] ')' CompileTimeParameters ::= CompileTimeParameter { ',' CompileTimeParameter } CompileTimeParameter ::= IDENTIFIER | IDENTIFIER ':' Type RunTimeParameters ::= Parameter { ',' Parameter } FunctionParameters ::= RunTimeParameters | CompileTimeParameters [ ';' [ RunTimeParameters ] ] Parameter ::= [ '...' ] [ KW_MUT ] IDENTIFIER ':' Type FunctionTypeParameters ::= FunctionTypeParameter { ',' FunctionTypeParameter } FunctionTypeParameter ::= [ KW_MUT ] Type ArrayTypeModifier ::= '[' [ Expression ] ']' (* -------------------------------------------------------- ) ( COMMENTS ) ( -------------------------------------------------------- *) (* A single-line comment starts with // and continues to the end of the line ) SingleLineComment ::= '//' { ~('\n' | '\r') } (* A multi-line comment starts with /* and ends with */ ) MultiLineComment ::= '/*' { . } '*/' (* -------------------------------------------------------- ) ( STATEMENTS (Unambiguous) ) ( -------------------------------------------------------- *) Statement ::= MatchedStatement | UnmatchedStatement MatchedStatement ::= KW_IF '(' Expression ')' MatchedStatement KW_ELSE MatchedStatement | KW_IF KW_LET Pattern '=' Expression Block KW_ELSE MatchedStatement | Block | KW_RETURN [ Expression ] ';' | RaiseStatement | BreakStatement | ContinueStatement | WhileStatement | ForStatement | MatchStatement | ReserveStatement | ExpressionStatement | VarDeclaration | FunctionDeclaration | ';' UnmatchedStatement ::= KW_IF '(' Expression ')' Statement | KW_IF '(' Expression ')' MatchedStatement KW_ELSE UnmatchedStatement | KW_IF KW_LET Pattern '=' Expression Block | KW_IF KW_LET Pattern '=' Expression Block KW_ELSE UnmatchedStatement Block ::= '{' { Statement } '}' ExpressionStatement ::= Expression ';' BreakStatement ::= KW_BREAK ';' ContinueStatement ::= KW_CONTINUE ';' WhileStatement ::= KW_WHILE '(' Expression ')' Statement ForStatement ::= KW_FOR '(' ForDeclaratorList KW_IN Expression ')' Statement ForDeclaratorList ::= ForDeclarator { ',' ForDeclarator } ForDeclarator ::= ( KW_LET | KW_VAR ) IDENTIFIER ':' Type RaiseStatement ::= KW_RAISE Expression ';' (* -------------------------------------------------------- ) ( MATCH (Mandatory Exhaustive) ) ( - Expression form for atomic initialization. ) ( - Statement form for control flow. ) ( - Guards, @-bindings, and nesting are disallowed. ) ( -------------------------------------------------------- *) MatchStatement ::= KW_MATCH '(' Expression ')' '{' [ MatchStmtArm { ',' MatchStmtArm } [ ',' ] ] '}' MatchStmtArm ::= MatchArmPattern '=>' ( Block | ';' ) MatchArmPattern ::= Pattern { '|' Pattern } Pattern ::= LiteralPattern | IDENTIFIER // binding | '_' // wildcard | PathExpression [ '(' [ PatternList ] ')' ] // unit/tuple-variant | TuplePattern | StructPattern TuplePattern ::= '(' [ PatternList [ ',' ] ] ')' StructPattern ::= '{' [ StructFieldPat { ',' StructFieldPat } [ ',' ] ] '}' StructFieldPat ::= IDENTIFIER ( ':' Pattern )? | '...' IDENTIFIER PatternList ::= Pattern { ',' Pattern } RangePattern ::= INT_LITERAL ('..' | '..=') INT_LITERAL | CHAR_LITERAL ('..' | '..=') CHAR_LITERAL LiteralPattern ::= INT_LITERAL | STRING_LITERAL | CHAR_LITERAL | KW_TRUE | KW_FALSE | RangePattern (* -------------------------------------------------------- ) ( EXPRESSIONS (Pratt Parser Aligned) ) ( -------------------------------------------------------- *) Expression ::= AssignmentExpression AssignmentExpression ::= CoalescingExpression | AssignmentTarget AssignmentOperator AssignmentExpression (* keep syntax permissive; lvalue-ness is a semantic check *) AssignmentTarget ::= CoalescingExpression AssignmentOperator ::= '=' | '+=' | '-=' | '*=' | '/=' CoalescingExpression ::= LogicalOrExpression { '??' LogicalOrExpression } LogicalOrExpression ::= LogicalAndExpression { '||' LogicalAndExpression } LogicalAndExpression ::= BitwiseOrExpression { '&&' BitwiseOrExpression } BitwiseOrExpression ::= BitwiseXorExpression { '|' BitwiseXorExpression } BitwiseXorExpression ::= BitwiseAndExpression { '^' BitwiseAndExpression } BitwiseAndExpression ::= ShiftExpression { '&' ShiftExpression } ShiftExpression ::= EqualityExpression { ( '<<' | '>>' ) EqualityExpression } EqualityExpression ::= ComparisonExpression { ( '==' | '!=' ) ComparisonExpression } ComparisonExpression ::= AdditiveExpression { ( '<' | '<=' | '>' | '>=' ) AdditiveExpression } AdditiveExpression ::= MultiplicativeExpression { ( '+' | '-' ) MultiplicativeExpression } MultiplicativeExpression ::= CastExpression { ( '*' | '/' | '%' ) CastExpression } CastExpression ::= UnaryExpression { KW_AS Type } UnaryExpression ::= ( KW_AWAIT | '-' | '!' | '&' | '~' ) UnaryExpression | PostfixExpression PostfixExpression ::= PrimaryExpression { '(' [ ArgumentList ] ')' | '[' Expression ']' | ( '.' | '?.' ) IDENTIFIER } PrimaryExpression ::= PathExpression | Literal | '(' Expression ')' | ArrayLiteral | NamedStructLiteral | AnonymousStructLiteral | FunctionExpression | NewExpression | MatchExpression MatchExpression ::= KW_MATCH '(' Expression ')' '{' [ MatchExprArm { ',' MatchExprArm } [ ',' ] ] '}' MatchExprArm ::= MatchArmPattern '=>' Expression PathExpression ::= IDENTIFIER { '::' IDENTIFIER } (* -------------------------------------------------------- ) ( TEMPORAL EXPRESSIONS ) ( -------------------------------------------------------- *) TemporalExpression ::= KW_ALWAYS Expression | KW_EVENTUALLY Expression | KW_NEXT Expression | Expression KW_UNTIL Expression | Expression KW_RELEASE Expression | KW_FORALL IDENTIFIER KW_IN Expression ':' Expression | KW_EXISTS IDENTIFIER KW_IN Expression ':' Expression | Expression (* -------------------------------------------------------- ) ( LITERALS & HELPER RULES ) ( -------------------------------------------------------- *) Literal ::= INT_LITERAL | FLOAT_LITERAL | STRING_LITERAL | STRING_MULTILINE | CHAR_LITERAL | DurationLiteral | KW_TRUE | KW_FALSE ArrayLiteral ::= '[' [ ArgumentList ] ']' NamedStructLiteral ::= PathExpression StructLiteralBody AnonymousStructLiteral ::= StructLiteralBody StructLiteralBody ::= '{' [ StructElement { ',' StructElement } [ ',' ] ] '}' StructElement ::= ( IDENTIFIER ':' Expression ) | IDENTIFIER | '...' Expression ArgumentList ::= CallArgument { ',' CallArgument } CallArgument ::= [ '...' ] [ KW_MUT ] Expression FunctionExpression ::= FnModifiers KW_FN '(' [ FunctionParameters ] ')' '->' ReturnType FunctionBodyWithReturn (* -------------------------------------------------------- ) ( Automatic Dereference: All values returned by `new`, with ) ( or without modifiers, are reference types and are ) ( automatically dereferenced when used in expression and ) ( member access contexts. Users do not need to explicitly ) ( write *x to access the underlying value; the compiler ) ( inserts dereferences implicitly. ) ( -------------------------------------------------------- *) NewExpression ::= KW_NEW [ AllocationModifiers ] AllocationBody AllocationModifiers ::= KW_SHARED [ KW_ATOMIC ] | KW_STATIC [ KW_ATOMIC ] | KW_WEAK AllocationBody ::= PrimaryExpression | StructLiteralBody ReserveStatement ::= KW_RESERVE Expression KW_FROM Expression Block [ KW_ELSE Block ] (* -------------------------------------------------------- ) ( TERMINALS (TOKENS) ) ( -------------------------------------------------------- *) IDENTIFIER INT_LITERAL, FLOAT_LITERAL, STRING_LITERAL, STRING_MULTILINE, CHAR_LITERAL (* Keywords: *) KW_LET, KW_VAR, KW_FN, KW_IF, KW_IN, KW_ELSE, KW_WHILE, KW_FOR, KW_RETURN, KW_BREAK, KW_CONTINUE, KW_WHERE, KW_ASYNC, KW_AWAIT, KW_AS, KW_STRUCT, KW_IMPL, KW_THREAD, KW_PURE KW_ENUM, KW_MATCH, KW_TYPE, KW_VOID, KW_INT, KW_ARENA, KW_U8, KW_I8, KW_U16, KW_I16, KW_U32, KW_I32, KW_U64, KW_I64, KW_F32, KW_F64, KW_BOOL, KW_CHAR, KW_STRING, KW_TRUE, KW_FALSE, KW_IMPORT, KW_EXPORT, KW_FROM, KW_FFI, KW_MAP, KW_DURATION, KW_INSTANT, (* No shared mutability without atomics! *) KW_NEW, KW_RESERVE, KW_SHARED, KW_ATOMIC, KW_WEAK, KW_STATIC, KW_MUT, KW_SYSTEM, KW_ACTION, KW_REQUIRES, KW_ENSURES, KW_INVARIANT, KW_PROPERTY, KW_FORALL, KW_EXISTS, KW_ALWAYS, KW_EVENTUALLY, KW_NEXT, KW_UNTIL, KW_RELEASE, KW_FAULT, KW_RAISE (* Operators and Delimiters: Arithmetic Wraps ) '=', '+=', '-=', '*=', '/=', '+', '-', '*', '/', '%', '&', '==', '!=', '<', '<=', '>', '>=', '!', '&&', '||', '|', '^', '~', '<<', '>>', '(', ')', '{', '}', '[', ']', ',', ';', '.', ':', '::', '?', '?.', '??', '...', '..=', '..', '->', '_', '=>' EOF " title="Grammar" id="8107cf2fa09a3">
Grammar
0 ( * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = )
1 ( Aela Language Grammar 0 . 0 . 8 )
2 ( Finalized : 2025 - 10 - 12 )
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 | FaultDeclaration
24 )
25
26 TypeAliasDeclaration : : = KW_TYPE IDENTIFIER '=' Type ';'
27
28 ReExportDeclaration : : =
29 KW_EXPORT ( NamedImport | IDENTIFIER )
30 KW_FROM STRING_LITERAL ';'
31
32 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
33 ( IMPORTS )
34 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
35
36 ImportStatement : : =
37 KW_IMPORT ( NamedImport | IDENTIFIER )
38 KW_FROM STRING_LITERAL ';'
39
40 NamedImport : : = '{' [ ImportSpecifier { ',' ImportSpecifier } [ ',' ] ] '}'
41 ImportSpecifier : : = IDENTIFIER [ ':' IDENTIFIER ]
42
43 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
44 ( FFI ( Foreign Function Interface ) )
45 ( - Contracts are compile - time enforced to be UB - free )
46 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
47
48 FfiDeclaration : : = KW_FFI IDENTIFIER '=' Type ';'
49
50 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
51 ( DECLARATIONS )
52 ( - var is mutable , let is immutable )
53 ( - aliases are borrow - checked by the analyzer )
54 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
55
56 VarDeclaration : : = ( KW_VAR | KW_LET ) IDENTIFIER ':' Type
57 [ '=' Expression ] ';'
58
59 StructDeclaration : : = KW_STRUCT IDENTIFIER '(' [ FunctionParameters ] ')' . . .
60
61 StructFieldDeclaration : : =
62 ( IDENTIFIER ':' Type )
63 | ( '...' IDENTIFIER )
64
65 VarDeclaration : : = ( KW_VAR | KW_LET ) IDENTIFIER ':' Type
66 [ '=' Expression ] ';'
67
68 FnModifiers : : =
69 [ ( KW_THREAD [ KW_PURE ] ) // thread; pure+thread allowed
70 | ( KW_PURE [ KW_THREAD ] ) // pure; optional thread
71 | KW_ASYNC // async alone (no pure)
72 ]
73
74 ImplBlock : : = KW_IMPL Type '{' { FunctionDeclaration | InvariantDeclaration } '}'
75
76
77 FunctionDeclaration : : =
78   FnModifiers KW_FN IDENTIFIER
79   '(' [ FunctionParameters ] ')' '->' ReturnType
80   ( ';' | FunctionBodyWithReturn )
81
82 FunctionBodyWithReturn : : =
83 '{' { Statement } ReturnStatement '}'
84
85 ReturnStatement : : = KW_RETURN [ Expression ] ';'
86
87 EnumDeclaration : : = KW_ENUM IDENTIFIER '(' [ FunctionParameters ] ')' . . .
88
89 TypeArguments : : = '(' [ TypeOrConst { ',' TypeOrConst } ] ')'
90 TypeOrConst : : = Type | ConstExpression
91
92 SystemDeclaration : : = KW_SYSTEM IDENTIFIER '{'
93 { ActionDeclaration
94 | InvariantDeclaration
95 | PropertyDeclaration
96 }
97 '}'
98
99 ActionDeclaration : : = KW_ACTION IDENTIFIER
100 '(' [ FunctionParameters ] ')'
101 [ RequiresClause ]
102 [ EnsuresClause ]
103 Block
104
105 RequiresClause : : = KW_REQUIRES Expression
106 EnsuresClause : : = KW_ENSURES Expression
107
108 InvariantDeclaration : : = KW_INVARIANT IDENTIFIER ':' Expression
109 PropertyDeclaration : : = KW_PROPERTY IDENTIFIER ':' TemporalExpression
110
111 FaultDeclaration : : = KW_FAULT IDENTIFIER '(' [ FunctionParameters ] ')' ';'
112
113 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
114 ( TYPES )
115 ( - - - - - )
116 ( The `(...)` syntax following a type identifier is used )
117 ( for type - level parameters , which can include both )
118 ( types AND values , to support Dependent Types . This )
119 ( differs from the generics syntax in languages like )
120 ( Rust or C + + , which typically use `<...>` for type - only )
121 ( parameters . )
122 ( )
123 ( Aela does not add built in properties or methods , instead )
124 ( it uses std : : length ( v ) , std : : size ( v ) , or standard library )
125 ( functions ie `import { vec } from "core/vector.ae";` )
126 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
127
128 Type : : = [ '&' ] PostfixType
129
130 PostfixType : : = SimpleType { ArrayTypeModifier | TypeArguments | '?' }
131
132 ReturnType : : = Type [ '|' FaultTypeList ]
133 FaultTypeList : : = FaultType { '|' FaultType }
134 FaultType : : = PathExpression
135
136 MapType : : = KW_MAP '(' Type ',' Type ')'
137
138 SimpleType : : = PrimitiveType
139 | KW_VOID
140 | FunctionTypeSignature
141 | PathExpression
142 | MapType
143 | RefinementType
144 | '(' Type ')'
145
146 FunctionTypeSignature : : =
147 FnModifiers KW_FN '(' [ FunctionTypeParameters ] ')' '->' ReturnType
148
149 RefinementType : : = '{' IDENTIFIER ':' Type KW_WHERE Expression '}'
150
151 PrimitiveType : : = KW_U8 | KW_I8 | KW_U16 | KW_I16 | KW_U32 | KW_I32
152 | KW_U64 | KW_I64 | KW_F32 | KW_F64 | KW_BOOL
153 | KW_CHAR | KW_STRING | KW_INT | KW_ARENA
154
155 TypeArguments : : = '(' [ Type { ',' Type } ] ')'
156
157 CompileTimeParameters : : = CompileTimeParameter { ',' CompileTimeParameter }
158 CompileTimeParameter : : = IDENTIFIER | IDENTIFIER ':' Type
159 RunTimeParameters : : = Parameter { ',' Parameter }
160
161 FunctionParameters : : =
162 RunTimeParameters
163 | CompileTimeParameters [ ';' [ RunTimeParameters ] ]
164
165 Parameter : : = [ '...' ] [ KW_MUT ] IDENTIFIER ':' Type
166 FunctionTypeParameters : : = FunctionTypeParameter { ',' FunctionTypeParameter }
167 FunctionTypeParameter : : = [ KW_MUT ] Type
168 ArrayTypeModifier : : = '[' [ Expression ] ']'
169
170 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
171 ( COMMENTS )
172 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
173
174 ( * A single - line comment starts with // and continues to the end of the line )
175 SingleLineComment : : = '//' { ~('\n' | '\r') }
176
177 ( * A multi - line comment starts with /* and ends with */ )
178 MultiLineComment : : = '/*' { . } '*/'
179
180 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
181 ( STATEMENTS ( Unambiguous ) )
182 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
183
184 Statement : : = MatchedStatement | UnmatchedStatement
185
186 MatchedStatement : : =
187 KW_IF '(' Expression ')' MatchedStatement KW_ELSE MatchedStatement
188 | KW_IF KW_LET Pattern '=' Expression Block KW_ELSE MatchedStatement
189 | Block
190 | KW_RETURN [ Expression ] ';'
191 | RaiseStatement
192 | BreakStatement
193 | ContinueStatement
194 | WhileStatement
195 | ForStatement
196 | MatchStatement
197 | ReserveStatement
198 | ExpressionStatement
199 | VarDeclaration
200 | FunctionDeclaration
201 | ';'
202
203 UnmatchedStatement : : =
204 KW_IF '(' Expression ')' Statement
205 | KW_IF '(' Expression ')' MatchedStatement KW_ELSE UnmatchedStatement
206 | KW_IF KW_LET Pattern '=' Expression Block
207 | KW_IF KW_LET Pattern '=' Expression Block KW_ELSE UnmatchedStatement
208
209 Block : : = '{' { Statement } '}'
210 ExpressionStatement : : = Expression ';'
211 BreakStatement : : = KW_BREAK ';'
212 ContinueStatement : : = KW_CONTINUE ';'
213
214 WhileStatement : : = KW_WHILE '(' Expression ')' Statement
215
216 ForStatement : : = KW_FOR '(' ForDeclaratorList KW_IN Expression ')' Statement
217 ForDeclaratorList : : = ForDeclarator { ',' ForDeclarator }
218 ForDeclarator : : = ( KW_LET | KW_VAR ) IDENTIFIER ':' Type
219
220 RaiseStatement : : = KW_RAISE Expression ';'
221
222 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
223 ( MATCH ( Mandatory Exhaustive ) )
224 ( - Expression form for atomic initialization . )
225 ( - Statement form for control flow . )
226 ( - Guards , @ - bindings , and nesting are disallowed . )
227 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
228
229 MatchStatement : : = KW_MATCH '(' Expression ')' '{'
230 [ MatchStmtArm { ',' MatchStmtArm } [ ',' ] ]
231 '}'
232
233 MatchStmtArm : : = MatchArmPattern '=>' ( Block | ';' )
234
235 MatchArmPattern : : = Pattern { '|' Pattern }
236
237 Pattern : : =
238 LiteralPattern
239 | IDENTIFIER // binding
240 | '_' // wildcard
241 | PathExpression [ '(' [ PatternList ] ')' ] // unit/tuple-variant
242 | TuplePattern
243 | StructPattern
244
245 TuplePattern : : = '(' [ PatternList [ ',' ] ] ')'
246
247 StructPattern : : = '{' [ StructFieldPat { ',' StructFieldPat } [ ',' ] ] '}'
248 StructFieldPat : : = IDENTIFIER ( ':' Pattern ) ? | '...' IDENTIFIER
249
250 PatternList : : = Pattern { ',' Pattern }
251
252 RangePattern : : =
253 INT_LITERAL ( '..' | '..=' ) INT_LITERAL
254 | CHAR_LITERAL ( '..' | '..=' ) CHAR_LITERAL
255
256 LiteralPattern : : = INT_LITERAL | STRING_LITERAL | CHAR_LITERAL | KW_TRUE | KW_FALSE | RangePattern
257
258 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
259 ( EXPRESSIONS ( Pratt Parser Aligned ) )
260 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
261
262 Expression : : = AssignmentExpression
263
264 AssignmentExpression : : =
265 CoalescingExpression
266 | AssignmentTarget AssignmentOperator AssignmentExpression
267
268 ( * keep syntax permissive ; lvalue - ness is a semantic check * )
269 AssignmentTarget : : = CoalescingExpression
270
271 AssignmentOperator : : = '=' | '+=' | '-=' | '*=' | '/='
272
273 CoalescingExpression : : = LogicalOrExpression { '??' LogicalOrExpression }
274
275 LogicalOrExpression : : = LogicalAndExpression { '||' LogicalAndExpression }
276
277 LogicalAndExpression : : = BitwiseOrExpression { '&&' BitwiseOrExpression }
278
279 BitwiseOrExpression : : = BitwiseXorExpression { '|' BitwiseXorExpression }
280
281 BitwiseXorExpression : : = BitwiseAndExpression { '^' BitwiseAndExpression }
282
283 BitwiseAndExpression : : = ShiftExpression { '&' ShiftExpression }
284
285 ShiftExpression : : = EqualityExpression { ( '<<' | '>>' ) EqualityExpression }
286
287 EqualityExpression : : = ComparisonExpression { ( '==' | '!=' ) ComparisonExpression }
288
289 ComparisonExpression : : = AdditiveExpression { ( '<' | '<=' | '>' | '>=' ) AdditiveExpression }
290
291 AdditiveExpression : : = MultiplicativeExpression { ( '+' | '-' ) MultiplicativeExpression }
292
293 MultiplicativeExpression : : = CastExpression { ( '*' | '/' | '%' ) CastExpression }
294
295 CastExpression : : = UnaryExpression { KW_AS Type }
296
297 UnaryExpression : : =
298 ( KW_AWAIT | '-' | '!' | '&' | '~' ) UnaryExpression
299 | PostfixExpression
300
301 PostfixExpression : : =
302 PrimaryExpression {
303 '(' [ ArgumentList ] ')'
304 | '[' Expression ']'
305 | ( '.' | '?.' ) IDENTIFIER
306 }
307
308 PrimaryExpression : : =
309 PathExpression
310 | Literal
311 | '(' Expression ')'
312 | ArrayLiteral
313 | NamedStructLiteral
314 | AnonymousStructLiteral
315 | FunctionExpression
316 | NewExpression
317 | MatchExpression
318
319 MatchExpression : : = KW_MATCH '(' Expression ')' '{'
320 [ MatchExprArm { ',' MatchExprArm } [ ',' ] ]
321 '}'
322
323 MatchExprArm : : = MatchArmPattern '=>' Expression
324
325 PathExpression : : = IDENTIFIER { '::' IDENTIFIER }
326
327 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
328 ( TEMPORAL EXPRESSIONS )
329 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
330
331 TemporalExpression : : =
332 KW_ALWAYS Expression
333 | KW_EVENTUALLY Expression
334 | KW_NEXT Expression
335 | Expression KW_UNTIL Expression
336 | Expression KW_RELEASE Expression
337 | KW_FORALL IDENTIFIER KW_IN Expression ':' Expression
338 | KW_EXISTS IDENTIFIER KW_IN Expression ':' Expression
339 | Expression
340
341 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
342 ( LITERALS & HELPER RULES )
343 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
344
345 Literal : : =
346 INT_LITERAL | FLOAT_LITERAL | STRING_LITERAL | STRING_MULTILINE
347 | CHAR_LITERAL | DurationLiteral | KW_TRUE | KW_FALSE
348
349 ArrayLiteral : : = '[' [ ArgumentList ] ']'
350 NamedStructLiteral : : = PathExpression StructLiteralBody
351 AnonymousStructLiteral : : = StructLiteralBody
352 StructLiteralBody : : = '{' [ StructElement { ',' StructElement } [ ',' ] ] '}'
353 StructElement : : = ( IDENTIFIER ':' Expression ) | IDENTIFIER | '...' Expression
354
355 ArgumentList : : = CallArgument { ',' CallArgument }
356 CallArgument : : = [ '...' ] [ KW_MUT ] Expression
357
358 FunctionExpression : : = FnModifiers KW_FN '(' [ FunctionParameters ] ')' '->' ReturnType FunctionBodyWithReturn
359
360 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
361 ( Automatic Dereference : All values returned by `new` , with )
362 ( or without modifiers , are reference types and are )
363 ( automatically dereferenced when used in expression and )
364 ( member access contexts . Users do not need to explicitly )
365 ( write * x to access the underlying value ; the compiler )
366 ( inserts dereferences implicitly . )
367 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
368
369 NewExpression : : =
370 KW_NEW [ AllocationModifiers ] AllocationBody
371
372 AllocationModifiers : : =
373 KW_SHARED [ KW_ATOMIC ]
374 | KW_STATIC [ KW_ATOMIC ]
375 | KW_WEAK
376
377 AllocationBody : : =
378 PrimaryExpression
379 | StructLiteralBody
380
381 ReserveStatement : : =
382 KW_RESERVE Expression KW_FROM Expression Block [ KW_ELSE Block ]
383
384 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
385 ( TERMINALS ( TOKENS ) )
386 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
387
388 IDENTIFIER
389 INT_LITERAL , FLOAT_LITERAL , STRING_LITERAL , STRING_MULTILINE , CHAR_LITERAL
390
391 ( * Keywords : * )
392 KW_LET , KW_VAR , KW_FN , KW_IF , KW_IN , KW_ELSE , KW_WHILE , KW_FOR ,
393 KW_RETURN , KW_BREAK , KW_CONTINUE , KW_WHERE ,
394 KW_ASYNC , KW_AWAIT , KW_AS , KW_STRUCT , KW_IMPL , KW_THREAD , KW_PURE
395 KW_ENUM , KW_MATCH , KW_TYPE , KW_VOID , KW_INT , KW_ARENA ,
396 KW_U8 , KW_I8 , KW_U16 , KW_I16 , KW_U32 , KW_I32 , KW_U64 , KW_I64 ,
397 KW_F32 , KW_F64 , KW_BOOL , KW_CHAR , KW_STRING , KW_TRUE , KW_FALSE ,
398 KW_IMPORT , KW_EXPORT , KW_FROM , KW_FFI , KW_MAP ,
399 KW_DURATION , KW_INSTANT ,
400
401 ( * No shared mutability without atomics ! * )
402 KW_NEW , KW_RESERVE , KW_SHARED , KW_ATOMIC , KW_WEAK , KW_STATIC , KW_MUT ,
403
404 KW_SYSTEM , KW_ACTION , KW_REQUIRES , KW_ENSURES ,
405 KW_INVARIANT , KW_PROPERTY , KW_FORALL , KW_EXISTS , KW_ALWAYS , KW_EVENTUALLY , KW_NEXT , KW_UNTIL ,
406 KW_RELEASE , KW_FAULT , KW_RAISE
407
408 ( * Operators and Delimiters : Arithmetic Wraps )
409 '=' , '+=' , '-=' , '*=' , '/=' , '+' , '-' , '*' , '/' , '%' , '&' , '==' , '!=' , '<' , '<=' , '>' , '>=' ,
410 '!' , '&&' , '||' , '|' , '^' , '~' , '<<' , '>>' , '(' , ')' , '{' , '}' , '[' , ']' ,
411 ',' , ';' , '.' , ':' , '::' , '?' , '?.' , '??' , '...' , '..=' , '..' , '->' , '_' , '=>'
412
413 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="ff3661e49117c">
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 daemonize

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 This will enable you to set watches on directories and enable incremental builds, and maintain stateful sessions.
Why This is ideal or developers who prefer working in the terminal, or for anyone using AI tooling.

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.

Types

Quick Reference

Category Surface Syntax Examples Notes
Booleans bool true , false Logical values.
Integers (fixed width) u8 i8 u16 i16 u32 i32 u64 i64 let n: i32 = 42; Signed/unsigned bit‑widths.
Integer (platform) int let n: int = 1; Implementation/default integer.
Floats f32 f64 let x: f64 = 3.14; IEEE‑754.
Char char 'A' Unicode scalar.
String string "hello" Immutable text; multi‑line strings supported.
Void / Unit void fn foo () -> void {} Functions that return nothing.
Time Types Instant , Duration let i: Instant = std::now(); Specialized i64 types for time measurement. Instant is a time point; Duration is a span.
Optional T? User? , i32? null / none allowed; use match , ?. , ?? .
None (value) null / none The distinguished empty value; typed as T? .
Reference (borrow) &T &User Borrowed reference. Analyzer enforces aliasing rules.
Arrays / Slices T[] , T[N] i32[] , byte[32] Dynamic slice vs fixed length (compile‑time N ).
Maps map(K, V) map(string, i32) Built‑in map/dictionary.
Function Types fn(params) -> R pure fn(i32)->i32 Modifiers: pure , thread , async (values).
Closures (function value) let f = fn(x:i32)->i32 { return x+1 }; Captures env; typed as fn(...) -> ... .
Structs (nominal) struct Name ... then Name(...) Point , Option(T) User‑defined records; may be parameterized.
Enums (sum types) enum Name { ... } Result(T,E) Tagged variants; pattern‑matched.
Modules (qualified name) pkg::Type Namespacing; module itself has a type internally.
Futures Future(T) returned by async fn Produced by async functions; await yields T .

Postfix builders: you can apply ? , array [...] , and type application ( ... ) to base types where applicable.

Nominal & Parameterized Types

Structs and enums define named (nominal) types. They may take type parameters and, as the language evolves, const parameters . Use ordinary type application:

Example
0 struct Pair ( T , U ) { first : T ; second : U ; }
1 let p : Pair ( i32 , string ) = { first : 1 , second : "hi" } ;
2
3 enum Option ( T ) { Some ( T ) , None }
4 let x : Option ( i32 ) = Option : : Some ( 3 ) ;

For functions, Aela uses a unified parameter list with a ; split: fn id(T; x:T) -> T (compile‑time params before ; , run‑time after). See the Functions section.

Optionals & None vs. Void

  • T? means maybe a `T` . Use match , ?. , or ?? to handle absence.
  • none / null is the empty value that inhabits optional types.
  • void means no value is returned (a function that completes for effects only). It is not the same as none .
Example
0 fn find_user ( ; id : i32 ) - > User ? { /* ... */ }
1 let u = find_user ( 42 ) ? ? default_user ( ) ;

Arrays & Maps

  • Dynamic slices: T[] — size known at run time; indexing and length available via stdlib.
  • Fixed arrays: T[N] — size is part of the type; N must be compile‑time evaluable.
  • Maps: map(K, V) — associative container; keys and values are regular types.
Example
0 let bytes : u8 [ 32 ] ;
1 let names : string [ ] ;
2 let counts : map ( string , i32 ) ;

References (borrowing)

&T is a borrowed reference to T .

  • Mutable vs immutable is governed by parameter modifiers ( mut ) and aliasing rules enforced by the analyzer (no shared mutability without atomics).
  • Think of &T as a view ; the underlying ownership model is enforced by the compiler.
Example
0 fn length ( ; s : & string ) - > i32 { return std : : length ( s ) ; }

Function & Closure Types

Function types and values share the same shape: fn(params) -> Return with optional modifiers.

Example
0 // Type position
1 let f : pure fn ( i32 ) - > i32 = add_one ;
2
3 // Value (closure) position
4 let g = pure fn ( x : i32 ) - > i32 { return x + 1 ; } ;

Modifiers:

  • pure — no observable side effects; enables stronger reasoning/optimizations.
  • thread — safe to run in a separate thread (may be combined with pure ).
  • async — produces a Future(Return) ; use await to get the value.

Unified parameter list (declarations):

Example
0 fn map ( T , U ; f : fn ( T ) - > U , xs : T [ ] ) - > U [ ] { /* ... */ }

Enums (sum types)

Enums declare a closed set of variants and are exhaustively pattern‑matched.

"err" } }" lang="aela" title="Example" id="e1732e6ca2be3">
Example
0 enum Result ( T , E ) { Ok ( T ) , Err ( E ) }
1
2 fn handle ( T , E ; r : Result ( T , E ) ) - > string {
3 return match ( r ) {
4 Result : : Ok ( x ) = > "ok" ,
5 Result : : Err ( e ) = > "err"
6 }
7 }

Futures & Async

async functions return future values that represent a computation in progress. Conceptually, the type is Future(T) , and await yields T .

Example
0 async fn fetch ( ; url : string ) - > string { /* ... */ }
1
2 fn use_it ( ; ) - > void {
3 let fut = fetch ( " / data" ) ;
4 let s = await fut ; // s: string
5 }

How the Compiler Thinks About Types (High‑Level)

Internally, every type has a kind (primitive, struct, enum, map, array, function, optional, reference, etc.) and a canonical signature string (e.g., "fn(i32)->bool" ). The compiler interns types in a cache so that equality checks are fast pointer comparisons.

  • Compatibility: types_are_compatible(dst, src) determines assignment/call compatibility (more than just raw equality when coercions are allowed).
  • Optional & None: represented distinctly ( TYPE_OPTIONAL around a base, and a special TYPE_NONE ).
  • Closures: carry a function type plus captured environment.
  • Modules: form a namespace; the module itself has a type used by the compiler for resolution.

You normally don’t see these details, but they explain why types print consistently and compare cheaply.

Minimal Grammar Reminders (surface)

  • Map: map(K, V)
  • Optional: T?
  • Reference: &T
  • Array: T[expr] (fixed) or T[] (slice)
  • Function type: pure fn(T1, T2) -> R
  • Type application: Name(T, U)

For advanced material (refinements { x: T where φ } and dependent forms like Vec(T, N) ), see the companion doc.

Best Practices

  • Prefer named aliases for recurring shapes: type Port = { p: i32 where 1024 <= p && p <= 65535 };
  • Use ? sparingly; prefer sum types (e.g., Result ) when you want the caller to handle both cases explicitly.
  • Keep function types pure when possible; it improves composability.
  • Choose fixed arrays ( T[N] ) when size is intrinsic and enables better checking/optimization.

Operators

Precedence Operator(s) Description Associativity
1 (Lowest) = , += , -= , *= , /= Assignment / Compound 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

Literals

Literals are notations for representing fixed values directly in source code. Aela supports a rich set of literals for primitive and aggregate data types.


Numeric Literals

Numeric literals represent number values. They can be integers or floating-point numbers and can include type suffixes and numeric separators for readability.

Integer Literals

Integer literals represent whole numbers. They can be specified in decimal or hexadecimal format.

Decimal: Standard base-10 numbers (e.g., `123`, `42`, `1000`). Hexadecimal: Base-16 numbers, prefixed with 0x (e.g., 0xFF , 0xdeadbeef ). Numeric Separator: * The underscore _ can be used to improve readability in long numbers (e.g., 1_000_000 , 0xDE_AD_BE_EF ).

By default, an integer literal is of type i32 . You can specify a different integer type using a suffix.

Suffix Type Range
i8 8-bit signed −128 to 127
u8 8-bit unsigned 0 to 255
i16 16-bit signed −32,768 to 32,767
u16 16-bit unsigned 0 to 65,535
i32 32-bit signed −2,147,483,648 to 2,147,483,647
u32 32-bit unsigned 0 to 4,294,967,295
i64 64-bit signed −9,223,372,036,854,775,808 …
u64 64-bit unsigned 0 to 18,446,744,073,709,551,615

Example:

Example
0 let default_int = 100 ; // Type: i32
1 let large_int = 1_000_000 ; // Type: i32
2 let unsigned_val = 42u32 ; // Type: u32
3 let id = 0x1A4F ; // Type: i32
4 let big_id = 0xDE_AD_BE_EFu64 ; // Type: u64

Floating-Point Literals

Floating-point literals represent numbers with a fractional component.

Decimal Notation: `3.14`, `0.001`, `1.0` Scientific Notation: 1.5e10 ( 1.5 × 10¹⁰ ), 2.5e-3 ( 2.5 × 10⁻³ ) Numeric Separator: * _ can be used in integer or fractional parts (e.g., 1_234.567_890 )

By default, a floating-point literal is of type f64 .

Suffix Type Precision
f32 32-bit float \~7 decimal digits
f64 64-bit float \~15 decimal digits

Example:

Example
0 let pi = 3 . 14159 ; // Type: f64
1 let small_val = 1e - 6 ; // Type: f64
2 let gravity = 9 . 8f32 ; // Type: f32
3 let large_float = 1_234 . 567 ; // Type: f64

Duration Literals

Duration literals represent a span of time and are of the first-class Duration type. They are formed by an integer or floating-point literal followed by a unit suffix.

Suffix Unit Description
ns Nanoseconds The smallest unit of time
us Microseconds 1,000 nanoseconds
ms Milliseconds 1,000 microseconds
s Seconds 1,000 milliseconds
min Minutes 60 seconds
h Hours 60 minutes
d Days 24 hours

Example:

Example
0 let timeout : Duration = 250ms ;
1 let retry_interval : Duration = 3s ;
2 let frame_time : Duration = 16 . 6ms ;
3 let long_wait : Duration = 1 . 5h ;

Boolean Literals

Boolean literals represent truth values and are of type bool .

`true`: Represents logical truth. false : Represents logical falsehood.

Example:

Example
0 let is_ready : bool = true ;
1 let has_failed : bool = false ;

Character Literals

A character literal represents a single Unicode scalar value (stored as a u32 ). Enclosed in single quotes ( ' ).

Example:

Example
0 let initial : char = 'P' ;
1 let newline : char = '\n' ;
2 let escaped_quote : char = '\' ' ;

String Literals

String literals represent sequences of characters and are of type string . Aela supports two forms:

Single-Line Strings

Enclosed in double quotes ( " ). Support escape sequences:

`\n` newline \r carriage return `\t` tab \\ backslash * \" double quote

Example:

Example
0 let greeting = "Hello , World ! \n" ;

Multi-Line Strings

Enclosed in backticks (` ` ). These are *raw*: preserve all whitespace and newlines. Only \` (escaped backtick) and \\\` (escaped backslash) are special.

Example:

Example
0 let query = `
1 SELECT
2 id ,
3 name
4 FROM
5 users ;
6 ` ;

Aggregate Literals

Aggregate literals create container values like arrays and structs.

Array Literals

A comma-separated list inside [] . Elements must share a compatible type. Empty arrays require a type annotation.

Example:

Example
0 let numbers = [ 1 , 2 , 3 , 4 , 5 ] ; // inferred i32[]
1 let names : string [ ] = [ ] ; // explicit annotation required

Struct Literals

Create a struct instance with {} .

Named Struct Literal: Prefix with the struct type. Field Shorthand: Use x instead of x: x . Spread Operator: * Use ... to copy fields from another struct.

Example:

Example
0 struct Point {
1 x : i32 ;
2 y : i32 ;
3 }
4
5 let p1 = Point { x : 10 , y : 20 } ;
6
7 let x = 15 ;
8 let p2 = Point { x , y : 30 } ; // shorthand
9
10 let p3 = Point { . . . p1 , y : 40 } ; // p3 = { x: 10, y: 40 }

All Literals in Action

bool { // Numbers let int_val = 1_000; let hex_val = 0xFF; let sixty_four_bits = 12345u64; let float_val = 99.5f32; let scientific = 6.022e23; // Durations let http_timeout = 30s; let animation_frame = 16.6ms; // Booleans let is_active = true; // Characters let a = 'a'; // Strings let single = "A single line."; let multi = `A multi-line string.`; // Aggregates let ids: u64[] = [101u64, 202u64, 303u64]; let name = "Alice"; let user = User { id: 1u64, name, is_active }; t.ok(true, "Literals demonstrated"); return true; }" lang="aela" title="Example" id="678ae36ee2001">
Example
0 import { Tap } from " . . / . . / . . / lib / test . ae" ;
1
2 struct User {
3 id : u64 ;
4 name : string ;
5 is_active : bool ;
6 }
7
8 export fn all_literals_example ( t : & Tap ) - > bool {
9 // Numbers
10 let int_val = 1_000 ;
11 let hex_val = 0xFF ;
12 let sixty_four_bits = 12345u64 ;
13 let float_val = 99 . 5f32 ;
14 let scientific = 6 . 022e23 ;
15
16 // Durations
17 let http_timeout = 30s ;
18 let animation_frame = 16 . 6ms ;
19
20 // Booleans
21 let is_active = true ;
22
23 // Characters
24 let a = 'a' ;
25
26 // Strings
27 let single = "A single line . " ;
28 let multi = `A
29 multi - line
30 string . ` ;
31
32 // Aggregates
33 let ids : u64 [ ] = [ 101u64 , 202u64 , 303u64 ] ;
34 let name = "Alice" ;
35 let user = User { id : 1u64 , name , is_active } ;
36
37 t . ok ( true , "Literals demonstrated" ) ;
38 return true ;
39 }

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="a2c2d2e484d92">
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 ;

Optional

The Optional type 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="ea0d1620f4b28">
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_sb_append: fn(&StringBuilder, string); ffi ae_sb_destroy: fn(&StringBuilder); // <-- The cleanup function fn main() -> int { let sb = ae_sb_new(); ae_sb_append(sb, "hello"); // CRITICAL: You must call destroy when you are done. ae_sb_destroy(sb); // Using `sb` after this point is UNDEFINED BEHAVIOR. // ae_sb_append(sb, " world"); // <-- ERROR! return 0; }" lang="aela" title="Example: Managing Lifetimes" id="33b615ffe6335">
Example: Managing Lifetimes
0 `` `aela
1 import { StringBuilder } from " . / runtime . ae" ;
2
3 // FFI Declarations for a C string builder
4 ffi ae_sb_new : fn ( ) - > & StringBuilder ;
5 ffi ae_sb_append : fn ( & StringBuilder , string ) ;
6 ffi ae_sb_destroy : fn ( & StringBuilder ) ; // <-- The cleanup function
7
8 fn main ( ) - > int {
9 let sb = ae_sb_new ( ) ;
10 ae_sb_append ( sb , "hello" ) ;
11
12 // CRITICAL: You must call destroy when you are done.
13 ae_sb_destroy ( sb ) ;
14
15 // Using `sb` after this point is UNDEFINED BEHAVIOR.
16 // ae_sb_append(sb, " world"); // <-- ERROR!
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 }

Duration & Instant

Time-related bugs are notoriously common and usually subtle. The root cause is frequently quantity confusion: when a plain number like 10 or lastUpdated is used, its unit is ambiguous. Does it represent 10 seconds, 10 milliseconds, or 10 microseconds? The programmer's intent is lost, hidden in variable names or documentation, leading to misinterpretations and errors.

Duration a first-class type with built-in literals. This design has two major benefits:

Improved Comprehension: Code becomes self-documenting. A value like 250ms is unambiguous; it cannot be mistaken for seconds or any other unit. This clarity makes code easier to read, write, and maintain. An expression like let timeout = 1s + 500ms; is immediately understandable without needing to look up function definitions or comments.

Clarified Intent & Type Safety: By distinguishing Duration from numeric types, the compiler can enforce correctness. You cannot accidentally add a raw number to a duration (5s + 3 is a compile-time error), which prevents nonsensical operations. Function signatures become more expressive and safe, for example fn sleep(for: Duration). This forces the caller to be explicit (e.g., sleep(for: 500ms)), eliminating the possibility of passing a value with the wrong unit.

The Duration type moves the handling of time units from a convention to a language-enforced guarantee, significantly reducing a whole class of common bugs.

Literals & type

  • Literals: INT_LITERAL DurationUnit or FLOAT_LITERAL DurationUnit (e.g., 250ms , 1.5s ).
  • Type: Duration is a first-class scalar quantity (internally monotonic-time ticks; implementation detail).
  • Sign: Duration is signed . -5s is allowed via unary minus.
  • No implicit numeric conversions: Duration never implicitly converts to/from numeric types.

Unary

Form Result Notes
+d Duration no-op
-d Duration negation; overflow is checked

Binary with Duration

Expr Result Allowed? Notes
d1 + d2 Duration Yes checked overflow
d1 - d2 Duration Yes checked overflow
d1 * n Duration Yes n is integer (any int type); checked overflow
n * d1 Duration Yes symmetric
d1 / n Duration Yes n integer; trunc toward zero ; div-by-zero error
d1 / d2 F64 Yes dimensionless ratio (floating)
d1 % d2 Duration Yes remainder; d2 != 0
d1 % n No disallowed
d1 & d2 - No no bitwise ops on Duration (including ^ , << , >> )
d1 && d2 No not booleans

Float scalars

Disallowed by default: Duration * F64 , Duration / F64 Rationale: silent precision loss. Provide library helpers instead (e.g., Duration::from_seconds_f64(x) ).

Comparison

Expr Result Allowed?
d1 == d2 Bool Yes
d1 != d2 Bool Yes
d1 < d2 , <= , > , >= Bool Yes
d1 == n , d1 < n No (no cross-type compare)

Instant

Expr Result Allowed? Notes
t1 + d Instant Yes checked overflow
d + t1 Instant Yes commutes
t1 - d Instant Yes checked overflow
t1 - t2 Duration Yes difference
t1 + t2 , t1 * d No nonsensical

Casting / construction

  • Allowed: explicit constructors, e.g. Duration::from_ms(250) , Duration::seconds_f64(1.5) .
  • Disallowed: implicit casts ( (int) d , (f64) d ).

Overflow & division semantics

  • Checked arithmetic by default: + , - , * on Duration panic on overflow (or trap).
  • Provide library variants:
  • checked_add , checked_sub , checked_mulOption
  • saturating_add , saturating_sub , saturating_mul
  • Division: d / n truncates toward zero; n must be nonzero.
  • d / d returns F64 (no truncation).

Examples

Example
0 let a : Duration = 250ms + 1s ; // ok
1 let b : Duration = 2 * 500ms ; // ok (int * Duration)
2 let c : Duration = ( 5s - 1200ms ) ; // ok, can be negative
3 let r : f64 = ( 750ms / 1 . 5s ) ; // ok: Duration / Duration -> F64 == 0.5
4
5 let bad1 = 1 . 2 * 5s ; // error: float scalar not allowed
6 let bad2 = 5s + 3 ; // error: no Duration + Int
7 let bad3 = 5s < 1000 ; // error: cross-type compare
8 let bad4 = 5s & 1s ; // error: bitwise on Duration

Suffix/literal interaction (clarity)

  • 1s + 500ms is fine; units normalize.
  • 1.5s is legal as a literal; it’s converted to integral ticks (ns) with rounding toward zero during lex/const-eval. (If you prefer bankers-rounding, specify that instead.)
  • No ambiguity with range tokens: ensure lexer orders '...' , '..=' , '..' (longest first) and treats ms/min etc. as unit suffixes , not identifiers.

Arenas

Overview

Aela's has a three-part model for safe, dynamic memory management. The model is designed to provide explicit, and verifiable memory control for both hosted (OS) and freestanding (bare-metal) environments.

The model consists of:

  • An intrinsic Arena type for memory provisioning.
  • A transactional reserve statement for scoped memory reservation.
  • A context-aware new keyword for object allocation.

The implementation is based on compile-time AST tagging, ensuring zero runtime overhead and inherent safety for asynchronous and multi-threaded code.

The Arena

The Arena is a primitive type known to the compiler, used for managing a block of memory.

Syntax

An Arena is provisioned using a special form of the new expression.

Example
0 // For freestanding targets (bare-metal)
1 'let' IDENTIFIER ':' 'Arena' '=' 'new' 'static' '{' 'size' ':' ConstantExpression '}' ';'
2
3 // For hosted targets (OS)
4 'let' IDENTIFIER ':' 'Arena' '=' 'new' '{' '}' ';'

Semantics

new {} : A runtime operation for hosted environments. It calls the system allocator (e.g., malloc). This expression is fallible and should be treated as returning an Option(Arena).

new static { size: ... } : A compile-time instruction. It directs the linker to reserve a fixed-size block of memory in the final binary's static data region (e.g., .bss). This is the primary mechanism for provisioning memory on bare metal.

The reserve Statement (Transactional Reservation)

The reserve statement transactionally reserves memory from an Arena for a specific lexical scope.

Syntax

Example
0 'reserve' size_expr 'from' arena_expr Block [ 'else' Block ]

Semantics

The reserve statement attempts to acquire size_expr bytes from the given arena_expr.

If the reservation is successful, the first Block is executed.

If the reservation fails (the arena has insufficient capacity), the else Block is executed.

A successful reservation creates a special allocation context that is active for the duration of the success block and any functions called from within it.

The new Keyword (Allocation)

The new keyword creates an object instance. Its behavior is context-dependent and verified by the compiler.

Semantics

The compiler enforces three distinct behaviors for new:

Hosted Default Context: When compiling for a hosted target and not inside a reserve block, new allocates from the system heap.

Freestanding Default Context: When compiling for a bare-metal target and not inside a reserve block, a call to new is a compile-time error. This ensures no accidental heap usage on constrained devices.

reserve Context: Inside a successful reserve block, new allocates from the reserved memory. This allocation is infallible and returns a value of type T, not Option(T).

Complete Bare-Metal Example

Example
0 // 1. PROVISIONING (Compile-Time)
1 // The compiler reserves 64KB of static memory.
2 var MY_ARENA : Arena = new static { size : 65536 } ;
3
4 // This function is only called from within a `reserve` block, so `new` is safe.
5 fn create_header ( ) - > Header {
6 // This `new` call inherits the reservation context from its caller.
7 return new shared Header { } ;
8 }
9
10 fn create_packet ( ) - > Option ( Packet ) {
11 // 2. RESERVATION (Transactional Check)
12 reserve 2048b from MY_ARENA {
13 // This block is entered only if the reservation succeeds.
14
15 // 3. ALLOCATION (Infallible)
16 // `new` is now infallible and allocates from MY_ARENA.
17 let packet = new shared Packet { } ;
18 packet . header = create_header ( ) ;
19
20 return Some ( packet ) ;
21 } else {
22 // The reservation failed; handle the error.
23 return None ;
24 }
25 }

Buffers

Introduction

Buffer(T) is a fundamental intrinsic type that provides a low-level, direct interface to a contiguous block of allocated memory (from where depending on if you do or don't use a reserve block). It is the primitive that higher-level, safe collection types like Vec(T) and String are built.

As an intrinsic , the compiler has special knowledge of Buffer(T) , allowing it to enforce powerful compile-time guarantees about memory ownership and borrowing. It's important to understand that Buffer(T) is intentionally designed as an unsafe primitive . Its core operations do not perform runtime bounds checking, providing a zero-overhead foundation for performance-critical code and the standard library. Your code can make it safe

Core Concepts

Representation

A Buffer(T) is a "fat pointer" containing two fields:

  1. A raw pointer to the start of the memory block.
  2. The capacity of the buffer (the total number of elements it can hold).

A Buffer(T) only tracks its total capacity. It does not track how many elements are currently initialized or in use (its length ). This responsibility is left to higher-level abstractions.

Ownership

The Buffer(T) value is the unique owner of the memory it controls. The compiler's verifier enforces this ownership model strictly:

  • When a Buffer(T) is moved, ownership is transferred. The original variable can no longer be used.
  • When a Buffer(T) variable goes out of scope, its memory is automatically deallocated.
  • The std::buffer::drop intrinsic can be used to explicitly deallocate the memory, consuming the buffer variable.

This model guarantees at compile time that the buffer's memory is freed exactly once, eliminating memory leaks and double-free errors.

The Intrinsic API

The following functions provide the raw manipulation capabilities for Buffer(T) .

std::buffer::alloc

Signature std::buffer::alloc(capacity: int, elem_size: int) -> Buffer(T)
Description Allocates an uninitialized buffer on the heap. The element type T is inferred from the context.

std::buffer::write

Signature std::buffer::write(mut buf: Buffer(T), index: int, value: T)
Description Writes a value into the buffer at a given index. This is an unsafe operation and does not perform bounds checking.

std::buffer::read

Signature std::buffer::read(buf: &Buffer(T), index: int) -> T
Description Reads the value from the buffer at a given index. This is an unsafe operation and does not perform bounds checking.

std::buffer::capacity

Signature std::buffer::capacity(buf: &Buffer(T)) -> int
Description Returns the total number of elements the buffer can hold. This operation is always safe.

std::buffer::drop

Signature std::buffer::drop(buf: Buffer(T))
Description Explicitly deallocates the buffer's memory. The verifier prevents any subsequent use of the buf variable.

std::buffer::view

Signature std::buffer::view(buf: &Buffer(T), start: int, len: int) -> &T[]
Description Creates an immutable slice ( &T[] ) that borrows a portion of the buffer's data. This is an unsafe operation as it does not check if the range is in bounds.

std::buffer::slice

Signature std::buffer::slice(mut buf: Buffer(T), start: int, len: int) -> T[]
Description Creates a mutable slice ( T[] ) that mutably borrows a portion of the buffer's data. This is an unsafe operation as it does not check if the range is in bounds.

The Safety Model: A Layered Approach

The safety of Buffer(T) and its ecosystem is best understood as a series of layers, where stronger guarantees are built upon more primitive ones.

Layer 1: The Unsafe Buffer(T) Primitive

The intrinsic functions themselves form the base layer. They are designed to be as close to the machine as possible. std::buffer::write compiles to a single store instruction, and std::buffer::read to a single load . They do not have bounds checks because they are meant to be the absolute zero-cost building blocks. This layer is primarily intended for the authors of the standard library and other highly-optimized, low-level code.

Layer 2: Compile-Time Safety via the Verifier

The compiler's verifier (or "borrow checker") provides the next layer of safety, and it does so with zero runtime cost . It enforces:

  • Ownership & Lifetimes : Guarantees that a Buffer is dropped exactly once and that any view or slice cannot outlive the Buffer it borrows from.
  • Aliasing Rules : Prevents data races by ensuring that you cannot have a mutable borrow ( T[] ) at the same time as any other borrow of the same data.

These checks happen entirely at compile time.

Layer 3: Provable Safety via Refinement Types

This is the highest level of safety, allowing for the creation of truly safe abstractions on top of the unsafe Buffer primitive. The language allows types to be "refined" with predicates that the compiler must prove.

A safe Vec(T) type in the standard library would not expose the unsafe read / write intrinsics. Instead, it would provide methods whose signatures use refinement types to enforce correctness:

Example
0 // Hypothetical safe API for a Vec(T) built on Buffer(T)
1 fn Vec . get ( & self , index : { i : int where i > = 0 & & i < self.size() }) -> & T {
2 // The compiler has already proven the index is valid, so we can
3 // safely call the unsafe intrinsic with no additional runtime check.
4 return std : : buffer : : view ( & self . buffer , index , 1 ) [ 0 ] ;
5 }

This system provides two powerful benefits:

  1. Compile-Time Proof : If you call my_vec.get(5) and the compiler can prove the vector's length is greater than 5, the safety is guaranteed and the generated code is just a direct memory access. The safety check has zero runtime cost.
  1. Compiler-Enforced Runtime Checks : If the compiler cannot prove the index is safe (e.g., it comes from user input), it will issue a compile-time error. This forces the programmer to add an explicit if check, which provides the compiler with the proof it needs inside the if block.
Example
0 let i = get_user_input ( ) ;
1 if ( i > = 0 & & i < my_vec . size ( ) ) {
2 // This is now valid. The compiler accepts the call because
3 // the 'if' condition satisfies the refinement type's predicate.
4 let element = my_vec . get ( i ) ;
5 }

This layered approach is the essence of a zero-cost abstraction: safety is guaranteed by the compiler wherever possible, and runtime costs are only incurred when logically necessary and are made explicit in the program's control flow.

Concurrency

Aela's concurrency is built on two orthogonal keywords, async and thread , that modify function declarations. These provide a clear, explicit syntax for defining concurrent work. The runtime manages a thread pool to execute these tasks, enabling both I/O-bound concurrency and CPU-bound parallelism that work in concert.

Core Keywords: async and thread

It is crucial to understand that `async` and `thread` are two separate modifiers with distinct meanings. Even though they are designed to work in a way that feels cohesive.

async

Marks a function as pausable . An async function can use the await keyword to non-blockingly wait for I/O or other long-running operations. It primarily relates to concurrency .

The decision to have a langauge-native async engine provides significant advantages for compile-time determinism.

thread

Marks a function as a parallel task . Calling a thread fn is a special operation that is non-blocking. It immediately submits the function to the runtime's thread pool for execution and returns a Task handle. It primarily relates to parallelism .

These keywords can be combined to define tasks with different behaviors:

Combination Meaning Primary Use Case
fn A regular, blocking function. Standard synchronous logic.
async fn A pausable, awaitable function. I/O-bound work on the current thread.
thread fn A function that runs in parallel in another thread. It cannot use await . CPU-intensive, blocking computations that should not stall the main thread.
async thread fn A pausable function that runs in parallel in a thread. A self-contained parallel task that performs its own I/O (e.g., a network client).

Defining and Spawning Tasks

Threaded tasks are ideal for the parallel processing of CPU intensive workloads.

Example
0 // This function is defined as a parallel task.
1 thread fn process_data ( source : DataSource ) - > Report {
2 return generate_report ( data ) ;
3 }

But sometimes a threaded task to compartmentalize work. For example, you might want some async processing to happen in another thread, separately from a UI thread.

The async keyword is optional and is used if the task needs to await I/O. The function's return type defines the final value returned when the task is complete.

Example
0 // This function is defined as a parallel task that is also async.
1 async thread fn process_data ( source : DataSource ) - > Report {
2 let data = await source . read ( ) ;
3 // ...
4 return generate_report ( data ) ;
5 }

Calling a function marked thread is a non-blocking operation. It starts the task in the background and immediately returns a Thread handle.

Example
0 // This call is non-blocking and returns a handle.
1 let handle : Thread ( Report ) = process_data ( my_source ) ;
2
3 // Await the handle to get the final result.
4 let report : Report = await handle . join ( ) ;

Structured Parallelism: thread { ... } Block

The thread block is used to run a group of tasks in parallel and wait for all of them to complete before continuing.

Example
0 async fn get_dashboard_data ( ) - > Dashboard {
1 var user : User ;
2 var orders : [ Order ] ;
3
4 // This block runs its internal async calls in parallel.
5 thread {
6 let user_task = fetch_user_profile ( 123 ) ;
7 let orders_task = fetch_recent_orders ( 123 ) ;
8
9 // The block implicitly awaits both before exiting.
10 user = await user_task ;
11 orders = await orders_task ;
12 }
13
14 // This line is only reached after both tasks are complete.
15 return Dashboard ( user , orders ) ;
16 }

Channels

Channels are used for streaming communication between threads. The type Channel(T) is a valid type according to the TypeApplication grammar rule.

Example
0 // Create a channel that transports string values.
1 let my_channel : Channel ( string ) = new { } ;

Streams

This pattern pauses the current function to process each message from a channel sequentially.

Example
0 // The `for await` loop consumes the receiver.
1 for await ( let message : string in my_channel . receiver ) {
2 process_message ( message ) ;
3 }

Events

This registers a handler and immediately returns, allowing the current function to continue its work.

Example
0 // `on_receive` takes a function expression as an argument.
1 my_channel . receiver . listen ( ( message : string ) - > void {
2 io : : print ( "Event received : { } " , message ) ;
3 } ) ;
4
5 // ... code here continues to run without blocking ...

Integrated Vehicle Safety vs. Aftermarket Parts

Think of it like the safety systems in a car.

A Library-Based Ecosystem (like Rust's): This is like buying a car chassis and then adding safety features yourself. You buy airbags from one company, an anti-lock braking system from another, and traction control from a third. They might be excellent components, but they weren't designed to work together as a single, cohesive system.

A Built-in Scheduler (Aela's model): This is like buying a modern car where the manufacturer has designed the airbags, ABS, traction control, and crumple zones to work together as a single, integrated safety system. It's tested as a whole and provides guarantees that the individual parts can't.

Here are the specific safety wins this integration provides.

  1. Compile-Time Data-Race Prevention

Because the scheduler is part of the language, the compiler has a deep understanding of what it means to "cross a thread boundary." It can enforce Send and Sync rules at compile-time. This means it's a syntax error to try to send non-thread-safe data into a thread fn or across a channel, completely eliminating an entire class of data-race bugs before the program can even run.

  1. A Single, Safe Bridge for Blocking Code

As we discussed, blocking in an async context is a huge foot-gun. A built-in runtime provides one, official, and well-defined function for handling this: std::task::run_blocking(). This prevents a scenario where different libraries provide their own, subtly different (and potentially unsafe) ways of handling blocking calls, which would lead to confusion and bugs.

  1. Guaranteed Structured Concurrency

The thread { ... } block is a major safety feature. Because it's a language construct powered by the built-in scheduler, the compiler can absolutely guarantee that all child tasks are completed before the parent function is allowed to continue. This prevents "leaked" tasks and makes error handling robust and predictable. In a library, this guarantee might be weaker or easier to accidentally bypass.

  1. Predictable Task Lifecycles

With a built-in scheduler, the behavior of a Task handle is predictable across the entire ecosystem. The rule that a handle will detach on drop is a language-level guarantee. This prevents situations where one library's handle might join on drop while another's aborts, leading to surprising behavior and resource leaks.

In short, a built-in scheduler allows Aela to treat concurrency as a core feature, subject to the same rigorous, compile-time safety analysis as the rest of the language.

Pool Control & Oversubscription

The Aela runtime is built on a work-stealing thread pool that defaults to using the number of logical CPU cores available (std::thread::available_parallelism()).

Pool Size & Pinning: The pool size can be overridden at startup via an environment variable (e.g., AELA_THREAD_COUNT=4). Fine-grained control like core pinning and thread priorities is considered a low-level OS feature and is not exposed through the high-level async/thread API. For expert use cases, a separate std::os::thread module could provide these unsafe, platform-specific controls.

On tiny targets without an OS, the runtime can be compiled in a "pool disabled" mode, using a single-threaded cooperative scheduler.

Oversubscription: The runtime's global task queue is bounded (e.g., a default capacity of 256 tasks). Calling a thread fn is cheap, but if the task queue is full, the call itself becomes an async operation. It will pause the calling function until space becomes available in the queue. This provides natural back-pressure and prevents developers from overwhelming the system.

Example
0 async fn io_bound_work1 ( ) - > int { // This already works
1 await doSomething ( ) ; // This already works
2 return 0 ;
3 }
4
5 thread fn compute_bound_work1 ( c : & Channel ( int ) ) - > int {
6 c . send ( 42 ) ; // some new data is ready
7 return 0 ; // the thread is finished
8 }
9
10 async fn main ( ) {
11 let c : Channel ( int ) = new shared { } ;
12
13 c . receive = fn ( i : int ) - > {
14 io : : print ( " { } " , i ) ;
15 } ;
16
17 let h : int = await compute_bound_work1 ( c ) ; // Actually returns `Task(int)` and resolves it
18 let i : int = await io_bound_work1 ( ) ; // Actually returns `Future(int)` and resolves it
19
20 let a : int [ ] = await std : : all ( [
21 compute_bound_work2 ( ) , // dispatched to the scheduler
22 compute_bound_work2 ( ) , // dispatched to the scheduler
23 io_bound_work2 ( ) // dispatched to the eventloop
24 ] ) ;
25
26 let h2 : Task ( int ) = compute_bound_work1 ( c ) ; // returns `Task(int)` lazy, not executing.
27
28 let i2 : int = await h2 ; // starts executing (ie, `await compute_bound_work1();`)
29 }
Example
0 enum RecvJob {
1 Msg ( Job ) ,
2 Cancelled ,
3 Timeout
4 }
5
6 async fn next_recv ( jobs : & Channel ( Job ) ) - > RecvJob {
7 let j : Job = await jobs . recv ( ) ;
8 return RecvJob : : Msg ( j ) ;
9 }
10
11 async fn on_cancel ( tok : & CancellationToken ) - > RecvJob {
12 await tok . cancelled ( ) ;
13 return RecvJob : : Cancelled ;
14 }
15
16 async fn after ( ms : Int ) - > RecvJob {
17 await timer . after_ms ( ms ) ;
18 return RecvJob : : Timeout ;
19 }
20
21 thread fn handle ( job : Job ) - > int {
22 // something interesting and cpu intensive
23 return 0 ;
24 }
25
26 async fn run_worker ( jobs : & Channel ( Job ) , tok : & CancellationToken ) - > int {
27 while ( true ) {
28
29 // `await select` atomically starts all provided cold awaitables, yields
30 // the first one to complete (as a tagged case for its arm), and cancels the rest.
31
32 let evt : RecvJob = await std : : select ( [
33 next_recv ( jobs ) ,
34 on_cancel ( tok ) ,
35 after ( 250 )
36 ] ) ;
37
38 match ( evt ) {
39 RecvJob : : Msg ( job ) = > { await handle ( job ) ; }
40 RecvJob : : Cancelled = > { break ; }
41 RecvJob : : Timeout = > { io : : print ( "tick" ) ; }
42 }
43 }
44 return 0 ;
45 }

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

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.

Formal Grammar Spec

' ReturnType RefinementType ::= '{' IDENTIFIER ':' Type KW_WHERE Expression '}' PrimitiveType ::= KW_U8 | KW_I8 | KW_U16 | KW_I16 | KW_U32 | KW_I32 | KW_U64 | KW_I64 | KW_F32 | KW_F64 | KW_BOOL | KW_CHAR | KW_STRING | KW_INT | KW_ARENA TypeArguments ::= '(' [ Type { ',' Type } ] ')' CompileTimeParameters ::= CompileTimeParameter { ',' CompileTimeParameter } CompileTimeParameter ::= IDENTIFIER | IDENTIFIER ':' Type RunTimeParameters ::= Parameter { ',' Parameter } FunctionParameters ::= RunTimeParameters | CompileTimeParameters [ ';' [ RunTimeParameters ] ] Parameter ::= [ '...' ] [ KW_MUT ] IDENTIFIER ':' Type FunctionTypeParameters ::= FunctionTypeParameter { ',' FunctionTypeParameter } FunctionTypeParameter ::= [ KW_MUT ] Type ArrayTypeModifier ::= '[' [ Expression ] ']' (* -------------------------------------------------------- ) ( COMMENTS ) ( -------------------------------------------------------- *) (* A single-line comment starts with // and continues to the end of the line ) SingleLineComment ::= '//' { ~('\n' | '\r') } (* A multi-line comment starts with /* and ends with */ ) MultiLineComment ::= '/*' { . } '*/' (* -------------------------------------------------------- ) ( STATEMENTS (Unambiguous) ) ( -------------------------------------------------------- *) Statement ::= MatchedStatement | UnmatchedStatement MatchedStatement ::= KW_IF '(' Expression ')' MatchedStatement KW_ELSE MatchedStatement | KW_IF KW_LET Pattern '=' Expression Block KW_ELSE MatchedStatement | Block | KW_RETURN [ Expression ] ';' | RaiseStatement | BreakStatement | ContinueStatement | WhileStatement | ForStatement | MatchStatement | ReserveStatement | ExpressionStatement | VarDeclaration | FunctionDeclaration | ';' UnmatchedStatement ::= KW_IF '(' Expression ')' Statement | KW_IF '(' Expression ')' MatchedStatement KW_ELSE UnmatchedStatement | KW_IF KW_LET Pattern '=' Expression Block | KW_IF KW_LET Pattern '=' Expression Block KW_ELSE UnmatchedStatement Block ::= '{' { Statement } '}' ExpressionStatement ::= Expression ';' BreakStatement ::= KW_BREAK ';' ContinueStatement ::= KW_CONTINUE ';' WhileStatement ::= KW_WHILE '(' Expression ')' Statement ForStatement ::= KW_FOR '(' ForDeclaratorList KW_IN Expression ')' Statement ForDeclaratorList ::= ForDeclarator { ',' ForDeclarator } ForDeclarator ::= ( KW_LET | KW_VAR ) IDENTIFIER ':' Type RaiseStatement ::= KW_RAISE Expression ';' (* -------------------------------------------------------- ) ( MATCH (Mandatory Exhaustive) ) ( - Expression form for atomic initialization. ) ( - Statement form for control flow. ) ( - Guards, @-bindings, and nesting are disallowed. ) ( -------------------------------------------------------- *) MatchStatement ::= KW_MATCH '(' Expression ')' '{' [ MatchStmtArm { ',' MatchStmtArm } [ ',' ] ] '}' MatchStmtArm ::= MatchArmPattern '=>' ( Block | ';' ) MatchArmPattern ::= Pattern { '|' Pattern } Pattern ::= LiteralPattern | IDENTIFIER // binding | '_' // wildcard | PathExpression [ '(' [ PatternList ] ')' ] // unit/tuple-variant | TuplePattern | StructPattern TuplePattern ::= '(' [ PatternList [ ',' ] ] ')' StructPattern ::= '{' [ StructFieldPat { ',' StructFieldPat } [ ',' ] ] '}' StructFieldPat ::= IDENTIFIER ( ':' Pattern )? | '...' IDENTIFIER PatternList ::= Pattern { ',' Pattern } RangePattern ::= INT_LITERAL ('..' | '..=') INT_LITERAL | CHAR_LITERAL ('..' | '..=') CHAR_LITERAL LiteralPattern ::= INT_LITERAL | STRING_LITERAL | CHAR_LITERAL | KW_TRUE | KW_FALSE | RangePattern (* -------------------------------------------------------- ) ( EXPRESSIONS (Pratt Parser Aligned) ) ( -------------------------------------------------------- *) Expression ::= AssignmentExpression AssignmentExpression ::= CoalescingExpression | AssignmentTarget AssignmentOperator AssignmentExpression (* keep syntax permissive; lvalue-ness is a semantic check *) AssignmentTarget ::= CoalescingExpression AssignmentOperator ::= '=' | '+=' | '-=' | '*=' | '/=' CoalescingExpression ::= LogicalOrExpression { '??' LogicalOrExpression } LogicalOrExpression ::= LogicalAndExpression { '||' LogicalAndExpression } LogicalAndExpression ::= BitwiseOrExpression { '&&' BitwiseOrExpression } BitwiseOrExpression ::= BitwiseXorExpression { '|' BitwiseXorExpression } BitwiseXorExpression ::= BitwiseAndExpression { '^' BitwiseAndExpression } BitwiseAndExpression ::= ShiftExpression { '&' ShiftExpression } ShiftExpression ::= EqualityExpression { ( '<<' | '>>' ) EqualityExpression } EqualityExpression ::= ComparisonExpression { ( '==' | '!=' ) ComparisonExpression } ComparisonExpression ::= AdditiveExpression { ( '<' | '<=' | '>' | '>=' ) AdditiveExpression } AdditiveExpression ::= MultiplicativeExpression { ( '+' | '-' ) MultiplicativeExpression } MultiplicativeExpression ::= CastExpression { ( '*' | '/' | '%' ) CastExpression } CastExpression ::= UnaryExpression { KW_AS Type } UnaryExpression ::= ( KW_AWAIT | '-' | '!' | '&' | '~' ) UnaryExpression | PostfixExpression PostfixExpression ::= PrimaryExpression { '(' [ ArgumentList ] ')' | '[' Expression ']' | ( '.' | '?.' ) IDENTIFIER } PrimaryExpression ::= PathExpression | Literal | '(' Expression ')' | ArrayLiteral | NamedStructLiteral | AnonymousStructLiteral | FunctionExpression | NewExpression | MatchExpression MatchExpression ::= KW_MATCH '(' Expression ')' '{' [ MatchExprArm { ',' MatchExprArm } [ ',' ] ] '}' MatchExprArm ::= MatchArmPattern '=>' Expression PathExpression ::= IDENTIFIER { '::' IDENTIFIER } (* -------------------------------------------------------- ) ( TEMPORAL EXPRESSIONS ) ( -------------------------------------------------------- *) TemporalExpression ::= KW_ALWAYS Expression | KW_EVENTUALLY Expression | KW_NEXT Expression | Expression KW_UNTIL Expression | Expression KW_RELEASE Expression | KW_FORALL IDENTIFIER KW_IN Expression ':' Expression | KW_EXISTS IDENTIFIER KW_IN Expression ':' Expression | Expression (* -------------------------------------------------------- ) ( LITERALS & HELPER RULES ) ( -------------------------------------------------------- *) Literal ::= INT_LITERAL | FLOAT_LITERAL | STRING_LITERAL | STRING_MULTILINE | CHAR_LITERAL | DurationLiteral | KW_TRUE | KW_FALSE ArrayLiteral ::= '[' [ ArgumentList ] ']' NamedStructLiteral ::= PathExpression StructLiteralBody AnonymousStructLiteral ::= StructLiteralBody StructLiteralBody ::= '{' [ StructElement { ',' StructElement } [ ',' ] ] '}' StructElement ::= ( IDENTIFIER ':' Expression ) | IDENTIFIER | '...' Expression ArgumentList ::= CallArgument { ',' CallArgument } CallArgument ::= [ '...' ] [ KW_MUT ] Expression FunctionExpression ::= FnModifiers KW_FN '(' [ FunctionParameters ] ')' '->' ReturnType FunctionBodyWithReturn (* -------------------------------------------------------- ) ( Automatic Dereference: All values returned by `new`, with ) ( or without modifiers, are reference types and are ) ( automatically dereferenced when used in expression and ) ( member access contexts. Users do not need to explicitly ) ( write *x to access the underlying value; the compiler ) ( inserts dereferences implicitly. ) ( -------------------------------------------------------- *) NewExpression ::= KW_NEW [ AllocationModifiers ] AllocationBody AllocationModifiers ::= KW_SHARED [ KW_ATOMIC ] | KW_STATIC [ KW_ATOMIC ] | KW_WEAK AllocationBody ::= PrimaryExpression | StructLiteralBody ReserveStatement ::= KW_RESERVE Expression KW_FROM Expression Block [ KW_ELSE Block ] (* -------------------------------------------------------- ) ( TERMINALS (TOKENS) ) ( -------------------------------------------------------- *) IDENTIFIER INT_LITERAL, FLOAT_LITERAL, STRING_LITERAL, STRING_MULTILINE, CHAR_LITERAL (* Keywords: *) KW_LET, KW_VAR, KW_FN, KW_IF, KW_IN, KW_ELSE, KW_WHILE, KW_FOR, KW_RETURN, KW_BREAK, KW_CONTINUE, KW_WHERE, KW_ASYNC, KW_AWAIT, KW_AS, KW_STRUCT, KW_IMPL, KW_THREAD, KW_PURE KW_ENUM, KW_MATCH, KW_TYPE, KW_VOID, KW_INT, KW_ARENA, KW_U8, KW_I8, KW_U16, KW_I16, KW_U32, KW_I32, KW_U64, KW_I64, KW_F32, KW_F64, KW_BOOL, KW_CHAR, KW_STRING, KW_TRUE, KW_FALSE, KW_IMPORT, KW_EXPORT, KW_FROM, KW_FFI, KW_MAP, KW_DURATION, KW_INSTANT, (* No shared mutability without atomics! *) KW_NEW, KW_RESERVE, KW_SHARED, KW_ATOMIC, KW_WEAK, KW_STATIC, KW_MUT, KW_SYSTEM, KW_ACTION, KW_REQUIRES, KW_ENSURES, KW_INVARIANT, KW_PROPERTY, KW_FORALL, KW_EXISTS, KW_ALWAYS, KW_EVENTUALLY, KW_NEXT, KW_UNTIL, KW_RELEASE, KW_FAULT, KW_RAISE (* Operators and Delimiters: Arithmetic Wraps ) '=', '+=', '-=', '*=', '/=', '+', '-', '*', '/', '%', '&', '==', '!=', '<', '<=', '>', '>=', '!', '&&', '||', '|', '^', '~', '<<', '>>', '(', ')', '{', '}', '[', ']', ',', ';', '.', ':', '::', '?', '?.', '??', '...', '..=', '..', '->', '_', '=>' EOF " title="Grammar" id="8107cf2fa09a3">
Grammar
0 ( * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = )
1 ( Aela Language Grammar 0 . 0 . 8 )
2 ( Finalized : 2025 - 10 - 12 )
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 | FaultDeclaration
24 )
25
26 TypeAliasDeclaration : : = KW_TYPE IDENTIFIER '=' Type ';'
27
28 ReExportDeclaration : : =
29 KW_EXPORT ( NamedImport | IDENTIFIER )
30 KW_FROM STRING_LITERAL ';'
31
32 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
33 ( IMPORTS )
34 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
35
36 ImportStatement : : =
37 KW_IMPORT ( NamedImport | IDENTIFIER )
38 KW_FROM STRING_LITERAL ';'
39
40 NamedImport : : = '{' [ ImportSpecifier { ',' ImportSpecifier } [ ',' ] ] '}'
41 ImportSpecifier : : = IDENTIFIER [ ':' IDENTIFIER ]
42
43 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
44 ( FFI ( Foreign Function Interface ) )
45 ( - Contracts are compile - time enforced to be UB - free )
46 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
47
48 FfiDeclaration : : = KW_FFI IDENTIFIER '=' Type ';'
49
50 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
51 ( DECLARATIONS )
52 ( - var is mutable , let is immutable )
53 ( - aliases are borrow - checked by the analyzer )
54 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
55
56 VarDeclaration : : = ( KW_VAR | KW_LET ) IDENTIFIER ':' Type
57 [ '=' Expression ] ';'
58
59 StructDeclaration : : = KW_STRUCT IDENTIFIER '(' [ FunctionParameters ] ')' . . .
60
61 StructFieldDeclaration : : =
62 ( IDENTIFIER ':' Type )
63 | ( '...' IDENTIFIER )
64
65 VarDeclaration : : = ( KW_VAR | KW_LET ) IDENTIFIER ':' Type
66 [ '=' Expression ] ';'
67
68 FnModifiers : : =
69 [ ( KW_THREAD [ KW_PURE ] ) // thread; pure+thread allowed
70 | ( KW_PURE [ KW_THREAD ] ) // pure; optional thread
71 | KW_ASYNC // async alone (no pure)
72 ]
73
74 ImplBlock : : = KW_IMPL Type '{' { FunctionDeclaration | InvariantDeclaration } '}'
75
76
77 FunctionDeclaration : : =
78   FnModifiers KW_FN IDENTIFIER
79   '(' [ FunctionParameters ] ')' '->' ReturnType
80   ( ';' | FunctionBodyWithReturn )
81
82 FunctionBodyWithReturn : : =
83 '{' { Statement } ReturnStatement '}'
84
85 ReturnStatement : : = KW_RETURN [ Expression ] ';'
86
87 EnumDeclaration : : = KW_ENUM IDENTIFIER '(' [ FunctionParameters ] ')' . . .
88
89 TypeArguments : : = '(' [ TypeOrConst { ',' TypeOrConst } ] ')'
90 TypeOrConst : : = Type | ConstExpression
91
92 SystemDeclaration : : = KW_SYSTEM IDENTIFIER '{'
93 { ActionDeclaration
94 | InvariantDeclaration
95 | PropertyDeclaration
96 }
97 '}'
98
99 ActionDeclaration : : = KW_ACTION IDENTIFIER
100 '(' [ FunctionParameters ] ')'
101 [ RequiresClause ]
102 [ EnsuresClause ]
103 Block
104
105 RequiresClause : : = KW_REQUIRES Expression
106 EnsuresClause : : = KW_ENSURES Expression
107
108 InvariantDeclaration : : = KW_INVARIANT IDENTIFIER ':' Expression
109 PropertyDeclaration : : = KW_PROPERTY IDENTIFIER ':' TemporalExpression
110
111 FaultDeclaration : : = KW_FAULT IDENTIFIER '(' [ FunctionParameters ] ')' ';'
112
113 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
114 ( TYPES )
115 ( - - - - - )
116 ( The `(...)` syntax following a type identifier is used )
117 ( for type - level parameters , which can include both )
118 ( types AND values , to support Dependent Types . This )
119 ( differs from the generics syntax in languages like )
120 ( Rust or C + + , which typically use `<...>` for type - only )
121 ( parameters . )
122 ( )
123 ( Aela does not add built in properties or methods , instead )
124 ( it uses std : : length ( v ) , std : : size ( v ) , or standard library )
125 ( functions ie `import { vec } from "core/vector.ae";` )
126 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
127
128 Type : : = [ '&' ] PostfixType
129
130 PostfixType : : = SimpleType { ArrayTypeModifier | TypeArguments | '?' }
131
132 ReturnType : : = Type [ '|' FaultTypeList ]
133 FaultTypeList : : = FaultType { '|' FaultType }
134 FaultType : : = PathExpression
135
136 MapType : : = KW_MAP '(' Type ',' Type ')'
137
138 SimpleType : : = PrimitiveType
139 | KW_VOID
140 | FunctionTypeSignature
141 | PathExpression
142 | MapType
143 | RefinementType
144 | '(' Type ')'
145
146 FunctionTypeSignature : : =
147 FnModifiers KW_FN '(' [ FunctionTypeParameters ] ')' '->' ReturnType
148
149 RefinementType : : = '{' IDENTIFIER ':' Type KW_WHERE Expression '}'
150
151 PrimitiveType : : = KW_U8 | KW_I8 | KW_U16 | KW_I16 | KW_U32 | KW_I32
152 | KW_U64 | KW_I64 | KW_F32 | KW_F64 | KW_BOOL
153 | KW_CHAR | KW_STRING | KW_INT | KW_ARENA
154
155 TypeArguments : : = '(' [ Type { ',' Type } ] ')'
156
157 CompileTimeParameters : : = CompileTimeParameter { ',' CompileTimeParameter }
158 CompileTimeParameter : : = IDENTIFIER | IDENTIFIER ':' Type
159 RunTimeParameters : : = Parameter { ',' Parameter }
160
161 FunctionParameters : : =
162 RunTimeParameters
163 | CompileTimeParameters [ ';' [ RunTimeParameters ] ]
164
165 Parameter : : = [ '...' ] [ KW_MUT ] IDENTIFIER ':' Type
166 FunctionTypeParameters : : = FunctionTypeParameter { ',' FunctionTypeParameter }
167 FunctionTypeParameter : : = [ KW_MUT ] Type
168 ArrayTypeModifier : : = '[' [ Expression ] ']'
169
170 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
171 ( COMMENTS )
172 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
173
174 ( * A single - line comment starts with // and continues to the end of the line )
175 SingleLineComment : : = '//' { ~('\n' | '\r') }
176
177 ( * A multi - line comment starts with /* and ends with */ )
178 MultiLineComment : : = '/*' { . } '*/'
179
180 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
181 ( STATEMENTS ( Unambiguous ) )
182 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
183
184 Statement : : = MatchedStatement | UnmatchedStatement
185
186 MatchedStatement : : =
187 KW_IF '(' Expression ')' MatchedStatement KW_ELSE MatchedStatement
188 | KW_IF KW_LET Pattern '=' Expression Block KW_ELSE MatchedStatement
189 | Block
190 | KW_RETURN [ Expression ] ';'
191 | RaiseStatement
192 | BreakStatement
193 | ContinueStatement
194 | WhileStatement
195 | ForStatement
196 | MatchStatement
197 | ReserveStatement
198 | ExpressionStatement
199 | VarDeclaration
200 | FunctionDeclaration
201 | ';'
202
203 UnmatchedStatement : : =
204 KW_IF '(' Expression ')' Statement
205 | KW_IF '(' Expression ')' MatchedStatement KW_ELSE UnmatchedStatement
206 | KW_IF KW_LET Pattern '=' Expression Block
207 | KW_IF KW_LET Pattern '=' Expression Block KW_ELSE UnmatchedStatement
208
209 Block : : = '{' { Statement } '}'
210 ExpressionStatement : : = Expression ';'
211 BreakStatement : : = KW_BREAK ';'
212 ContinueStatement : : = KW_CONTINUE ';'
213
214 WhileStatement : : = KW_WHILE '(' Expression ')' Statement
215
216 ForStatement : : = KW_FOR '(' ForDeclaratorList KW_IN Expression ')' Statement
217 ForDeclaratorList : : = ForDeclarator { ',' ForDeclarator }
218 ForDeclarator : : = ( KW_LET | KW_VAR ) IDENTIFIER ':' Type
219
220 RaiseStatement : : = KW_RAISE Expression ';'
221
222 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
223 ( MATCH ( Mandatory Exhaustive ) )
224 ( - Expression form for atomic initialization . )
225 ( - Statement form for control flow . )
226 ( - Guards , @ - bindings , and nesting are disallowed . )
227 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
228
229 MatchStatement : : = KW_MATCH '(' Expression ')' '{'
230 [ MatchStmtArm { ',' MatchStmtArm } [ ',' ] ]
231 '}'
232
233 MatchStmtArm : : = MatchArmPattern '=>' ( Block | ';' )
234
235 MatchArmPattern : : = Pattern { '|' Pattern }
236
237 Pattern : : =
238 LiteralPattern
239 | IDENTIFIER // binding
240 | '_' // wildcard
241 | PathExpression [ '(' [ PatternList ] ')' ] // unit/tuple-variant
242 | TuplePattern
243 | StructPattern
244
245 TuplePattern : : = '(' [ PatternList [ ',' ] ] ')'
246
247 StructPattern : : = '{' [ StructFieldPat { ',' StructFieldPat } [ ',' ] ] '}'
248 StructFieldPat : : = IDENTIFIER ( ':' Pattern ) ? | '...' IDENTIFIER
249
250 PatternList : : = Pattern { ',' Pattern }
251
252 RangePattern : : =
253 INT_LITERAL ( '..' | '..=' ) INT_LITERAL
254 | CHAR_LITERAL ( '..' | '..=' ) CHAR_LITERAL
255
256 LiteralPattern : : = INT_LITERAL | STRING_LITERAL | CHAR_LITERAL | KW_TRUE | KW_FALSE | RangePattern
257
258 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
259 ( EXPRESSIONS ( Pratt Parser Aligned ) )
260 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
261
262 Expression : : = AssignmentExpression
263
264 AssignmentExpression : : =
265 CoalescingExpression
266 | AssignmentTarget AssignmentOperator AssignmentExpression
267
268 ( * keep syntax permissive ; lvalue - ness is a semantic check * )
269 AssignmentTarget : : = CoalescingExpression
270
271 AssignmentOperator : : = '=' | '+=' | '-=' | '*=' | '/='
272
273 CoalescingExpression : : = LogicalOrExpression { '??' LogicalOrExpression }
274
275 LogicalOrExpression : : = LogicalAndExpression { '||' LogicalAndExpression }
276
277 LogicalAndExpression : : = BitwiseOrExpression { '&&' BitwiseOrExpression }
278
279 BitwiseOrExpression : : = BitwiseXorExpression { '|' BitwiseXorExpression }
280
281 BitwiseXorExpression : : = BitwiseAndExpression { '^' BitwiseAndExpression }
282
283 BitwiseAndExpression : : = ShiftExpression { '&' ShiftExpression }
284
285 ShiftExpression : : = EqualityExpression { ( '<<' | '>>' ) EqualityExpression }
286
287 EqualityExpression : : = ComparisonExpression { ( '==' | '!=' ) ComparisonExpression }
288
289 ComparisonExpression : : = AdditiveExpression { ( '<' | '<=' | '>' | '>=' ) AdditiveExpression }
290
291 AdditiveExpression : : = MultiplicativeExpression { ( '+' | '-' ) MultiplicativeExpression }
292
293 MultiplicativeExpression : : = CastExpression { ( '*' | '/' | '%' ) CastExpression }
294
295 CastExpression : : = UnaryExpression { KW_AS Type }
296
297 UnaryExpression : : =
298 ( KW_AWAIT | '-' | '!' | '&' | '~' ) UnaryExpression
299 | PostfixExpression
300
301 PostfixExpression : : =
302 PrimaryExpression {
303 '(' [ ArgumentList ] ')'
304 | '[' Expression ']'
305 | ( '.' | '?.' ) IDENTIFIER
306 }
307
308 PrimaryExpression : : =
309 PathExpression
310 | Literal
311 | '(' Expression ')'
312 | ArrayLiteral
313 | NamedStructLiteral
314 | AnonymousStructLiteral
315 | FunctionExpression
316 | NewExpression
317 | MatchExpression
318
319 MatchExpression : : = KW_MATCH '(' Expression ')' '{'
320 [ MatchExprArm { ',' MatchExprArm } [ ',' ] ]
321 '}'
322
323 MatchExprArm : : = MatchArmPattern '=>' Expression
324
325 PathExpression : : = IDENTIFIER { '::' IDENTIFIER }
326
327 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
328 ( TEMPORAL EXPRESSIONS )
329 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
330
331 TemporalExpression : : =
332 KW_ALWAYS Expression
333 | KW_EVENTUALLY Expression
334 | KW_NEXT Expression
335 | Expression KW_UNTIL Expression
336 | Expression KW_RELEASE Expression
337 | KW_FORALL IDENTIFIER KW_IN Expression ':' Expression
338 | KW_EXISTS IDENTIFIER KW_IN Expression ':' Expression
339 | Expression
340
341 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
342 ( LITERALS & HELPER RULES )
343 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
344
345 Literal : : =
346 INT_LITERAL | FLOAT_LITERAL | STRING_LITERAL | STRING_MULTILINE
347 | CHAR_LITERAL | DurationLiteral | KW_TRUE | KW_FALSE
348
349 ArrayLiteral : : = '[' [ ArgumentList ] ']'
350 NamedStructLiteral : : = PathExpression StructLiteralBody
351 AnonymousStructLiteral : : = StructLiteralBody
352 StructLiteralBody : : = '{' [ StructElement { ',' StructElement } [ ',' ] ] '}'
353 StructElement : : = ( IDENTIFIER ':' Expression ) | IDENTIFIER | '...' Expression
354
355 ArgumentList : : = CallArgument { ',' CallArgument }
356 CallArgument : : = [ '...' ] [ KW_MUT ] Expression
357
358 FunctionExpression : : = FnModifiers KW_FN '(' [ FunctionParameters ] ')' '->' ReturnType FunctionBodyWithReturn
359
360 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
361 ( Automatic Dereference : All values returned by `new` , with )
362 ( or without modifiers , are reference types and are )
363 ( automatically dereferenced when used in expression and )
364 ( member access contexts . Users do not need to explicitly )
365 ( write * x to access the underlying value ; the compiler )
366 ( inserts dereferences implicitly . )
367 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
368
369 NewExpression : : =
370 KW_NEW [ AllocationModifiers ] AllocationBody
371
372 AllocationModifiers : : =
373 KW_SHARED [ KW_ATOMIC ]
374 | KW_STATIC [ KW_ATOMIC ]
375 | KW_WEAK
376
377 AllocationBody : : =
378 PrimaryExpression
379 | StructLiteralBody
380
381 ReserveStatement : : =
382 KW_RESERVE Expression KW_FROM Expression Block [ KW_ELSE Block ]
383
384 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
385 ( TERMINALS ( TOKENS ) )
386 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
387
388 IDENTIFIER
389 INT_LITERAL , FLOAT_LITERAL , STRING_LITERAL , STRING_MULTILINE , CHAR_LITERAL
390
391 ( * Keywords : * )
392 KW_LET , KW_VAR , KW_FN , KW_IF , KW_IN , KW_ELSE , KW_WHILE , KW_FOR ,
393 KW_RETURN , KW_BREAK , KW_CONTINUE , KW_WHERE ,
394 KW_ASYNC , KW_AWAIT , KW_AS , KW_STRUCT , KW_IMPL , KW_THREAD , KW_PURE
395 KW_ENUM , KW_MATCH , KW_TYPE , KW_VOID , KW_INT , KW_ARENA ,
396 KW_U8 , KW_I8 , KW_U16 , KW_I16 , KW_U32 , KW_I32 , KW_U64 , KW_I64 ,
397 KW_F32 , KW_F64 , KW_BOOL , KW_CHAR , KW_STRING , KW_TRUE , KW_FALSE ,
398 KW_IMPORT , KW_EXPORT , KW_FROM , KW_FFI , KW_MAP ,
399 KW_DURATION , KW_INSTANT ,
400
401 ( * No shared mutability without atomics ! * )
402 KW_NEW , KW_RESERVE , KW_SHARED , KW_ATOMIC , KW_WEAK , KW_STATIC , KW_MUT ,
403
404 KW_SYSTEM , KW_ACTION , KW_REQUIRES , KW_ENSURES ,
405 KW_INVARIANT , KW_PROPERTY , KW_FORALL , KW_EXISTS , KW_ALWAYS , KW_EVENTUALLY , KW_NEXT , KW_UNTIL ,
406 KW_RELEASE , KW_FAULT , KW_RAISE
407
408 ( * Operators and Delimiters : Arithmetic Wraps )
409 '=' , '+=' , '-=' , '*=' , '/=' , '+' , '-' , '*' , '/' , '%' , '&' , '==' , '!=' , '<' , '<=' , '>' , '>=' ,
410 '!' , '&&' , '||' , '|' , '^' , '~' , '<<' , '>>' , '(' , ')' , '{' , '}' , '[' , ']' ,
411 ',' , ';' , '.' , ':' , '::' , '?' , '?.' , '??' , '...' , '..=' , '..' , '->' , '_' , '=>'
412
413 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="ff3661e49117c">
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 daemonize

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 This will enable you to set watches on directories and enable incremental builds, and maintain stateful sessions.
Why This is ideal or developers who prefer working in the terminal, or for anyone using AI tooling.

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.

Types

Quick Reference

Category Surface Syntax Examples Notes
Booleans bool true , false Logical values.
Integers (fixed width) u8 i8 u16 i16 u32 i32 u64 i64 let n: i32 = 42; Signed/unsigned bit‑widths.
Integer (platform) int let n: int = 1; Implementation/default integer.
Floats f32 f64 let x: f64 = 3.14; IEEE‑754.
Char char 'A' Unicode scalar.
String string "hello" Immutable text; multi‑line strings supported.
Void / Unit void fn foo () -> void {} Functions that return nothing.
Time Types Instant , Duration let i: Instant = std::now(); Specialized i64 types for time measurement. Instant is a time point; Duration is a span.
Optional T? User? , i32? null / none allowed; use match , ?. , ?? .
None (value) null / none The distinguished empty value; typed as T? .
Reference (borrow) &T &User Borrowed reference. Analyzer enforces aliasing rules.
Arrays / Slices T[] , T[N] i32[] , byte[32] Dynamic slice vs fixed length (compile‑time N ).
Maps map(K, V) map(string, i32) Built‑in map/dictionary.
Function Types fn(params) -> R pure fn(i32)->i32 Modifiers: pure , thread , async (values).
Closures (function value) let f = fn(x:i32)->i32 { return x+1 }; Captures env; typed as fn(...) -> ... .
Structs (nominal) struct Name ... then Name(...) Point , Option(T) User‑defined records; may be parameterized.
Enums (sum types) enum Name { ... } Result(T,E) Tagged variants; pattern‑matched.
Modules (qualified name) pkg::Type Namespacing; module itself has a type internally.
Futures Future(T) returned by async fn Produced by async functions; await yields T .

Postfix builders: you can apply ? , array [...] , and type application ( ... ) to base types where applicable.

Nominal & Parameterized Types

Structs and enums define named (nominal) types. They may take type parameters and, as the language evolves, const parameters . Use ordinary type application:

Example
0 struct Pair ( T , U ) { first : T ; second : U ; }
1 let p : Pair ( i32 , string ) = { first : 1 , second : "hi" } ;
2
3 enum Option ( T ) { Some ( T ) , None }
4 let x : Option ( i32 ) = Option : : Some ( 3 ) ;

For functions, Aela uses a unified parameter list with a ; split: fn id(T; x:T) -> T (compile‑time params before ; , run‑time after). See the Functions section.

Optionals & None vs. Void

  • T? means maybe a `T` . Use match , ?. , or ?? to handle absence.
  • none / null is the empty value that inhabits optional types.
  • void means no value is returned (a function that completes for effects only). It is not the same as none .
Example
0 fn find_user ( ; id : i32 ) - > User ? { /* ... */ }
1 let u = find_user ( 42 ) ? ? default_user ( ) ;

Arrays & Maps

  • Dynamic slices: T[] — size known at run time; indexing and length available via stdlib.
  • Fixed arrays: T[N] — size is part of the type; N must be compile‑time evaluable.
  • Maps: map(K, V) — associative container; keys and values are regular types.
Example
0 let bytes : u8 [ 32 ] ;
1 let names : string [ ] ;
2 let counts : map ( string , i32 ) ;

References (borrowing)

&T is a borrowed reference to T .

  • Mutable vs immutable is governed by parameter modifiers ( mut ) and aliasing rules enforced by the analyzer (no shared mutability without atomics).
  • Think of &T as a view ; the underlying ownership model is enforced by the compiler.
Example
0 fn length ( ; s : & string ) - > i32 { return std : : length ( s ) ; }

Function & Closure Types

Function types and values share the same shape: fn(params) -> Return with optional modifiers.

Example
0 // Type position
1 let f : pure fn ( i32 ) - > i32 = add_one ;
2
3 // Value (closure) position
4 let g = pure fn ( x : i32 ) - > i32 { return x + 1 ; } ;

Modifiers:

  • pure — no observable side effects; enables stronger reasoning/optimizations.
  • thread — safe to run in a separate thread (may be combined with pure ).
  • async — produces a Future(Return) ; use await to get the value.

Unified parameter list (declarations):

Example
0 fn map ( T , U ; f : fn ( T ) - > U , xs : T [ ] ) - > U [ ] { /* ... */ }

Enums (sum types)

Enums declare a closed set of variants and are exhaustively pattern‑matched.

"err" } }" lang="aela" title="Example" id="e1732e6ca2be3">
Example
0 enum Result ( T , E ) { Ok ( T ) , Err ( E ) }
1
2 fn handle ( T , E ; r : Result ( T , E ) ) - > string {
3 return match ( r ) {
4 Result : : Ok ( x ) = > "ok" ,
5 Result : : Err ( e ) = > "err"
6 }
7 }

Futures & Async

async functions return future values that represent a computation in progress. Conceptually, the type is Future(T) , and await yields T .

Example
0 async fn fetch ( ; url : string ) - > string { /* ... */ }
1
2 fn use_it ( ; ) - > void {
3 let fut = fetch ( " / data" ) ;
4 let s = await fut ; // s: string
5 }

How the Compiler Thinks About Types (High‑Level)

Internally, every type has a kind (primitive, struct, enum, map, array, function, optional, reference, etc.) and a canonical signature string (e.g., "fn(i32)->bool" ). The compiler interns types in a cache so that equality checks are fast pointer comparisons.

  • Compatibility: types_are_compatible(dst, src) determines assignment/call compatibility (more than just raw equality when coercions are allowed).
  • Optional & None: represented distinctly ( TYPE_OPTIONAL around a base, and a special TYPE_NONE ).
  • Closures: carry a function type plus captured environment.
  • Modules: form a namespace; the module itself has a type used by the compiler for resolution.

You normally don’t see these details, but they explain why types print consistently and compare cheaply.

Minimal Grammar Reminders (surface)

  • Map: map(K, V)
  • Optional: T?
  • Reference: &T
  • Array: T[expr] (fixed) or T[] (slice)
  • Function type: pure fn(T1, T2) -> R
  • Type application: Name(T, U)

For advanced material (refinements { x: T where φ } and dependent forms like Vec(T, N) ), see the companion doc.

Best Practices

  • Prefer named aliases for recurring shapes: type Port = { p: i32 where 1024 <= p && p <= 65535 };
  • Use ? sparingly; prefer sum types (e.g., Result ) when you want the caller to handle both cases explicitly.
  • Keep function types pure when possible; it improves composability.
  • Choose fixed arrays ( T[N] ) when size is intrinsic and enables better checking/optimization.

Operators

Precedence Operator(s) Description Associativity
1 (Lowest) = , += , -= , *= , /= Assignment / Compound 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

Literals

Literals are notations for representing fixed values directly in source code. Aela supports a rich set of literals for primitive and aggregate data types.


Numeric Literals

Numeric literals represent number values. They can be integers or floating-point numbers and can include type suffixes and numeric separators for readability.

Integer Literals

Integer literals represent whole numbers. They can be specified in decimal or hexadecimal format.

Decimal: Standard base-10 numbers (e.g., `123`, `42`, `1000`). Hexadecimal: Base-16 numbers, prefixed with 0x (e.g., 0xFF , 0xdeadbeef ). Numeric Separator: * The underscore _ can be used to improve readability in long numbers (e.g., 1_000_000 , 0xDE_AD_BE_EF ).

By default, an integer literal is of type i32 . You can specify a different integer type using a suffix.

Suffix Type Range
i8 8-bit signed −128 to 127
u8 8-bit unsigned 0 to 255
i16 16-bit signed −32,768 to 32,767
u16 16-bit unsigned 0 to 65,535
i32 32-bit signed −2,147,483,648 to 2,147,483,647
u32 32-bit unsigned 0 to 4,294,967,295
i64 64-bit signed −9,223,372,036,854,775,808 …
u64 64-bit unsigned 0 to 18,446,744,073,709,551,615

Example:

Example
0 let default_int = 100 ; // Type: i32
1 let large_int = 1_000_000 ; // Type: i32
2 let unsigned_val = 42u32 ; // Type: u32
3 let id = 0x1A4F ; // Type: i32
4 let big_id = 0xDE_AD_BE_EFu64 ; // Type: u64

Floating-Point Literals

Floating-point literals represent numbers with a fractional component.

Decimal Notation: `3.14`, `0.001`, `1.0` Scientific Notation: 1.5e10 ( 1.5 × 10¹⁰ ), 2.5e-3 ( 2.5 × 10⁻³ ) Numeric Separator: * _ can be used in integer or fractional parts (e.g., 1_234.567_890 )

By default, a floating-point literal is of type f64 .

Suffix Type Precision
f32 32-bit float \~7 decimal digits
f64 64-bit float \~15 decimal digits

Example:

Example
0 let pi = 3 . 14159 ; // Type: f64
1 let small_val = 1e - 6 ; // Type: f64
2 let gravity = 9 . 8f32 ; // Type: f32
3 let large_float = 1_234 . 567 ; // Type: f64

Duration Literals

Duration literals represent a span of time and are of the first-class Duration type. They are formed by an integer or floating-point literal followed by a unit suffix.

Suffix Unit Description
ns Nanoseconds The smallest unit of time
us Microseconds 1,000 nanoseconds
ms Milliseconds 1,000 microseconds
s Seconds 1,000 milliseconds
min Minutes 60 seconds
h Hours 60 minutes
d Days 24 hours

Example:

Example
0 let timeout : Duration = 250ms ;
1 let retry_interval : Duration = 3s ;
2 let frame_time : Duration = 16 . 6ms ;
3 let long_wait : Duration = 1 . 5h ;

Boolean Literals

Boolean literals represent truth values and are of type bool .

`true`: Represents logical truth. false : Represents logical falsehood.

Example:

Example
0 let is_ready : bool = true ;
1 let has_failed : bool = false ;

Character Literals

A character literal represents a single Unicode scalar value (stored as a u32 ). Enclosed in single quotes ( ' ).

Example:

Example
0 let initial : char = 'P' ;
1 let newline : char = '\n' ;
2 let escaped_quote : char = '\' ' ;

String Literals

String literals represent sequences of characters and are of type string . Aela supports two forms:

Single-Line Strings

Enclosed in double quotes ( " ). Support escape sequences:

`\n` newline \r carriage return `\t` tab \\ backslash * \" double quote

Example:

Example
0 let greeting = "Hello , World ! \n" ;

Multi-Line Strings

Enclosed in backticks (` ` ). These are *raw*: preserve all whitespace and newlines. Only \` (escaped backtick) and \\\` (escaped backslash) are special.

Example:

Example
0 let query = `
1 SELECT
2 id ,
3 name
4 FROM
5 users ;
6 ` ;

Aggregate Literals

Aggregate literals create container values like arrays and structs.

Array Literals

A comma-separated list inside [] . Elements must share a compatible type. Empty arrays require a type annotation.

Example:

Example
0 let numbers = [ 1 , 2 , 3 , 4 , 5 ] ; // inferred i32[]
1 let names : string [ ] = [ ] ; // explicit annotation required

Struct Literals

Create a struct instance with {} .

Named Struct Literal: Prefix with the struct type. Field Shorthand: Use x instead of x: x . Spread Operator: * Use ... to copy fields from another struct.

Example:

Example
0 struct Point {
1 x : i32 ;
2 y : i32 ;
3 }
4
5 let p1 = Point { x : 10 , y : 20 } ;
6
7 let x = 15 ;
8 let p2 = Point { x , y : 30 } ; // shorthand
9
10 let p3 = Point { . . . p1 , y : 40 } ; // p3 = { x: 10, y: 40 }

All Literals in Action

bool { // Numbers let int_val = 1_000; let hex_val = 0xFF; let sixty_four_bits = 12345u64; let float_val = 99.5f32; let scientific = 6.022e23; // Durations let http_timeout = 30s; let animation_frame = 16.6ms; // Booleans let is_active = true; // Characters let a = 'a'; // Strings let single = "A single line."; let multi = `A multi-line string.`; // Aggregates let ids: u64[] = [101u64, 202u64, 303u64]; let name = "Alice"; let user = User { id: 1u64, name, is_active }; t.ok(true, "Literals demonstrated"); return true; }" lang="aela" title="Example" id="678ae36ee2001">
Example
0 import { Tap } from " . . / . . / . . / lib / test . ae" ;
1
2 struct User {
3 id : u64 ;
4 name : string ;
5 is_active : bool ;
6 }
7
8 export fn all_literals_example ( t : & Tap ) - > bool {
9 // Numbers
10 let int_val = 1_000 ;
11 let hex_val = 0xFF ;
12 let sixty_four_bits = 12345u64 ;
13 let float_val = 99 . 5f32 ;
14 let scientific = 6 . 022e23 ;
15
16 // Durations
17 let http_timeout = 30s ;
18 let animation_frame = 16 . 6ms ;
19
20 // Booleans
21 let is_active = true ;
22
23 // Characters
24 let a = 'a' ;
25
26 // Strings
27 let single = "A single line . " ;
28 let multi = `A
29 multi - line
30 string . ` ;
31
32 // Aggregates
33 let ids : u64 [ ] = [ 101u64 , 202u64 , 303u64 ] ;
34 let name = "Alice" ;
35 let user = User { id : 1u64 , name , is_active } ;
36
37 t . ok ( true , "Literals demonstrated" ) ;
38 return true ;
39 }

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="a2c2d2e484d92">
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 ;

Optional

The Optional type 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="ea0d1620f4b28">
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_sb_append: fn(&StringBuilder, string); ffi ae_sb_destroy: fn(&StringBuilder); // <-- The cleanup function fn main() -> int { let sb = ae_sb_new(); ae_sb_append(sb, "hello"); // CRITICAL: You must call destroy when you are done. ae_sb_destroy(sb); // Using `sb` after this point is UNDEFINED BEHAVIOR. // ae_sb_append(sb, " world"); // <-- ERROR! return 0; }" lang="aela" title="Example: Managing Lifetimes" id="33b615ffe6335">
Example: Managing Lifetimes
0 `` `aela
1 import { StringBuilder } from " . / runtime . ae" ;
2
3 // FFI Declarations for a C string builder
4 ffi ae_sb_new : fn ( ) - > & StringBuilder ;
5 ffi ae_sb_append : fn ( & StringBuilder , string ) ;
6 ffi ae_sb_destroy : fn ( & StringBuilder ) ; // <-- The cleanup function
7
8 fn main ( ) - > int {
9 let sb = ae_sb_new ( ) ;
10 ae_sb_append ( sb , "hello" ) ;
11
12 // CRITICAL: You must call destroy when you are done.
13 ae_sb_destroy ( sb ) ;
14
15 // Using `sb` after this point is UNDEFINED BEHAVIOR.
16 // ae_sb_append(sb, " world"); // <-- ERROR!
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 }

Duration & Instant

Time-related bugs are notoriously common and usually subtle. The root cause is frequently quantity confusion: when a plain number like 10 or lastUpdated is used, its unit is ambiguous. Does it represent 10 seconds, 10 milliseconds, or 10 microseconds? The programmer's intent is lost, hidden in variable names or documentation, leading to misinterpretations and errors.

Duration a first-class type with built-in literals. This design has two major benefits:

Improved Comprehension: Code becomes self-documenting. A value like 250ms is unambiguous; it cannot be mistaken for seconds or any other unit. This clarity makes code easier to read, write, and maintain. An expression like let timeout = 1s + 500ms; is immediately understandable without needing to look up function definitions or comments.

Clarified Intent & Type Safety: By distinguishing Duration from numeric types, the compiler can enforce correctness. You cannot accidentally add a raw number to a duration (5s + 3 is a compile-time error), which prevents nonsensical operations. Function signatures become more expressive and safe, for example fn sleep(for: Duration). This forces the caller to be explicit (e.g., sleep(for: 500ms)), eliminating the possibility of passing a value with the wrong unit.

The Duration type moves the handling of time units from a convention to a language-enforced guarantee, significantly reducing a whole class of common bugs.

Literals & type

  • Literals: INT_LITERAL DurationUnit or FLOAT_LITERAL DurationUnit (e.g., 250ms , 1.5s ).
  • Type: Duration is a first-class scalar quantity (internally monotonic-time ticks; implementation detail).
  • Sign: Duration is signed . -5s is allowed via unary minus.
  • No implicit numeric conversions: Duration never implicitly converts to/from numeric types.

Unary

Form Result Notes
+d Duration no-op
-d Duration negation; overflow is checked

Binary with Duration

Expr Result Allowed? Notes
d1 + d2 Duration Yes checked overflow
d1 - d2 Duration Yes checked overflow
d1 * n Duration Yes n is integer (any int type); checked overflow
n * d1 Duration Yes symmetric
d1 / n Duration Yes n integer; trunc toward zero ; div-by-zero error
d1 / d2 F64 Yes dimensionless ratio (floating)
d1 % d2 Duration Yes remainder; d2 != 0
d1 % n No disallowed
d1 & d2 - No no bitwise ops on Duration (including ^ , << , >> )
d1 && d2 No not booleans

Float scalars

Disallowed by default: Duration * F64 , Duration / F64 Rationale: silent precision loss. Provide library helpers instead (e.g., Duration::from_seconds_f64(x) ).

Comparison

Expr Result Allowed?
d1 == d2 Bool Yes
d1 != d2 Bool Yes
d1 < d2 , <= , > , >= Bool Yes
d1 == n , d1 < n No (no cross-type compare)

Instant

Expr Result Allowed? Notes
t1 + d Instant Yes checked overflow
d + t1 Instant Yes commutes
t1 - d Instant Yes checked overflow
t1 - t2 Duration Yes difference
t1 + t2 , t1 * d No nonsensical

Casting / construction

  • Allowed: explicit constructors, e.g. Duration::from_ms(250) , Duration::seconds_f64(1.5) .
  • Disallowed: implicit casts ( (int) d , (f64) d ).

Overflow & division semantics

  • Checked arithmetic by default: + , - , * on Duration panic on overflow (or trap).
  • Provide library variants:
  • checked_add , checked_sub , checked_mulOption
  • saturating_add , saturating_sub , saturating_mul
  • Division: d / n truncates toward zero; n must be nonzero.
  • d / d returns F64 (no truncation).

Examples

Example
0 let a : Duration = 250ms + 1s ; // ok
1 let b : Duration = 2 * 500ms ; // ok (int * Duration)
2 let c : Duration = ( 5s - 1200ms ) ; // ok, can be negative
3 let r : f64 = ( 750ms / 1 . 5s ) ; // ok: Duration / Duration -> F64 == 0.5
4
5 let bad1 = 1 . 2 * 5s ; // error: float scalar not allowed
6 let bad2 = 5s + 3 ; // error: no Duration + Int
7 let bad3 = 5s < 1000 ; // error: cross-type compare
8 let bad4 = 5s & 1s ; // error: bitwise on Duration

Suffix/literal interaction (clarity)

  • 1s + 500ms is fine; units normalize.
  • 1.5s is legal as a literal; it’s converted to integral ticks (ns) with rounding toward zero during lex/const-eval. (If you prefer bankers-rounding, specify that instead.)
  • No ambiguity with range tokens: ensure lexer orders '...' , '..=' , '..' (longest first) and treats ms/min etc. as unit suffixes , not identifiers.

Arenas

Overview

Aela's has a three-part model for safe, dynamic memory management. The model is designed to provide explicit, and verifiable memory control for both hosted (OS) and freestanding (bare-metal) environments.

The model consists of:

  • An intrinsic Arena type for memory provisioning.
  • A transactional reserve statement for scoped memory reservation.
  • A context-aware new keyword for object allocation.

The implementation is based on compile-time AST tagging, ensuring zero runtime overhead and inherent safety for asynchronous and multi-threaded code.

The Arena

The Arena is a primitive type known to the compiler, used for managing a block of memory.

Syntax

An Arena is provisioned using a special form of the new expression.

Example
0 // For freestanding targets (bare-metal)
1 'let' IDENTIFIER ':' 'Arena' '=' 'new' 'static' '{' 'size' ':' ConstantExpression '}' ';'
2
3 // For hosted targets (OS)
4 'let' IDENTIFIER ':' 'Arena' '=' 'new' '{' '}' ';'

Semantics

new {} : A runtime operation for hosted environments. It calls the system allocator (e.g., malloc). This expression is fallible and should be treated as returning an Option(Arena).

new static { size: ... } : A compile-time instruction. It directs the linker to reserve a fixed-size block of memory in the final binary's static data region (e.g., .bss). This is the primary mechanism for provisioning memory on bare metal.

The reserve Statement (Transactional Reservation)

The reserve statement transactionally reserves memory from an Arena for a specific lexical scope.

Syntax

Example
0 'reserve' size_expr 'from' arena_expr Block [ 'else' Block ]

Semantics

The reserve statement attempts to acquire size_expr bytes from the given arena_expr.

If the reservation is successful, the first Block is executed.

If the reservation fails (the arena has insufficient capacity), the else Block is executed.

A successful reservation creates a special allocation context that is active for the duration of the success block and any functions called from within it.

The new Keyword (Allocation)

The new keyword creates an object instance. Its behavior is context-dependent and verified by the compiler.

Semantics

The compiler enforces three distinct behaviors for new:

Hosted Default Context: When compiling for a hosted target and not inside a reserve block, new allocates from the system heap.

Freestanding Default Context: When compiling for a bare-metal target and not inside a reserve block, a call to new is a compile-time error. This ensures no accidental heap usage on constrained devices.

reserve Context: Inside a successful reserve block, new allocates from the reserved memory. This allocation is infallible and returns a value of type T, not Option(T).

Complete Bare-Metal Example

Example
0 // 1. PROVISIONING (Compile-Time)
1 // The compiler reserves 64KB of static memory.
2 var MY_ARENA : Arena = new static { size : 65536 } ;
3
4 // This function is only called from within a `reserve` block, so `new` is safe.
5 fn create_header ( ) - > Header {
6 // This `new` call inherits the reservation context from its caller.
7 return new shared Header { } ;
8 }
9
10 fn create_packet ( ) - > Option ( Packet ) {
11 // 2. RESERVATION (Transactional Check)
12 reserve 2048b from MY_ARENA {
13 // This block is entered only if the reservation succeeds.
14
15 // 3. ALLOCATION (Infallible)
16 // `new` is now infallible and allocates from MY_ARENA.
17 let packet = new shared Packet { } ;
18 packet . header = create_header ( ) ;
19
20 return Some ( packet ) ;
21 } else {
22 // The reservation failed; handle the error.
23 return None ;
24 }
25 }

Buffers

Introduction

Buffer(T) is a fundamental intrinsic type that provides a low-level, direct interface to a contiguous block of allocated memory (from where depending on if you do or don't use a reserve block). It is the primitive that higher-level, safe collection types like Vec(T) and String are built.

As an intrinsic , the compiler has special knowledge of Buffer(T) , allowing it to enforce powerful compile-time guarantees about memory ownership and borrowing. It's important to understand that Buffer(T) is intentionally designed as an unsafe primitive . Its core operations do not perform runtime bounds checking, providing a zero-overhead foundation for performance-critical code and the standard library. Your code can make it safe

Core Concepts

Representation

A Buffer(T) is a "fat pointer" containing two fields:

  1. A raw pointer to the start of the memory block.
  2. The capacity of the buffer (the total number of elements it can hold).

A Buffer(T) only tracks its total capacity. It does not track how many elements are currently initialized or in use (its length ). This responsibility is left to higher-level abstractions.

Ownership

The Buffer(T) value is the unique owner of the memory it controls. The compiler's verifier enforces this ownership model strictly:

  • When a Buffer(T) is moved, ownership is transferred. The original variable can no longer be used.
  • When a Buffer(T) variable goes out of scope, its memory is automatically deallocated.
  • The std::buffer::drop intrinsic can be used to explicitly deallocate the memory, consuming the buffer variable.

This model guarantees at compile time that the buffer's memory is freed exactly once, eliminating memory leaks and double-free errors.

The Intrinsic API

The following functions provide the raw manipulation capabilities for Buffer(T) .

std::buffer::alloc

Signature std::buffer::alloc(capacity: int, elem_size: int) -> Buffer(T)
Description Allocates an uninitialized buffer on the heap. The element type T is inferred from the context.

std::buffer::write

Signature std::buffer::write(mut buf: Buffer(T), index: int, value: T)
Description Writes a value into the buffer at a given index. This is an unsafe operation and does not perform bounds checking.

std::buffer::read

Signature std::buffer::read(buf: &Buffer(T), index: int) -> T
Description Reads the value from the buffer at a given index. This is an unsafe operation and does not perform bounds checking.

std::buffer::capacity

Signature std::buffer::capacity(buf: &Buffer(T)) -> int
Description Returns the total number of elements the buffer can hold. This operation is always safe.

std::buffer::drop

Signature std::buffer::drop(buf: Buffer(T))
Description Explicitly deallocates the buffer's memory. The verifier prevents any subsequent use of the buf variable.

std::buffer::view

Signature std::buffer::view(buf: &Buffer(T), start: int, len: int) -> &T[]
Description Creates an immutable slice ( &T[] ) that borrows a portion of the buffer's data. This is an unsafe operation as it does not check if the range is in bounds.

std::buffer::slice

Signature std::buffer::slice(mut buf: Buffer(T), start: int, len: int) -> T[]
Description Creates a mutable slice ( T[] ) that mutably borrows a portion of the buffer's data. This is an unsafe operation as it does not check if the range is in bounds.

The Safety Model: A Layered Approach

The safety of Buffer(T) and its ecosystem is best understood as a series of layers, where stronger guarantees are built upon more primitive ones.

Layer 1: The Unsafe Buffer(T) Primitive

The intrinsic functions themselves form the base layer. They are designed to be as close to the machine as possible. std::buffer::write compiles to a single store instruction, and std::buffer::read to a single load . They do not have bounds checks because they are meant to be the absolute zero-cost building blocks. This layer is primarily intended for the authors of the standard library and other highly-optimized, low-level code.

Layer 2: Compile-Time Safety via the Verifier

The compiler's verifier (or "borrow checker") provides the next layer of safety, and it does so with zero runtime cost . It enforces:

  • Ownership & Lifetimes : Guarantees that a Buffer is dropped exactly once and that any view or slice cannot outlive the Buffer it borrows from.
  • Aliasing Rules : Prevents data races by ensuring that you cannot have a mutable borrow ( T[] ) at the same time as any other borrow of the same data.

These checks happen entirely at compile time.

Layer 3: Provable Safety via Refinement Types

This is the highest level of safety, allowing for the creation of truly safe abstractions on top of the unsafe Buffer primitive. The language allows types to be "refined" with predicates that the compiler must prove.

A safe Vec(T) type in the standard library would not expose the unsafe read / write intrinsics. Instead, it would provide methods whose signatures use refinement types to enforce correctness:

Example
0 // Hypothetical safe API for a Vec(T) built on Buffer(T)
1 fn Vec . get ( & self , index : { i : int where i > = 0 & & i < self.size() }) -> & T {
2 // The compiler has already proven the index is valid, so we can
3 // safely call the unsafe intrinsic with no additional runtime check.
4 return std : : buffer : : view ( & self . buffer , index , 1 ) [ 0 ] ;
5 }

This system provides two powerful benefits:

  1. Compile-Time Proof : If you call my_vec.get(5) and the compiler can prove the vector's length is greater than 5, the safety is guaranteed and the generated code is just a direct memory access. The safety check has zero runtime cost.
  1. Compiler-Enforced Runtime Checks : If the compiler cannot prove the index is safe (e.g., it comes from user input), it will issue a compile-time error. This forces the programmer to add an explicit if check, which provides the compiler with the proof it needs inside the if block.
Example
0 let i = get_user_input ( ) ;
1 if ( i > = 0 & & i < my_vec . size ( ) ) {
2 // This is now valid. The compiler accepts the call because
3 // the 'if' condition satisfies the refinement type's predicate.
4 let element = my_vec . get ( i ) ;
5 }

This layered approach is the essence of a zero-cost abstraction: safety is guaranteed by the compiler wherever possible, and runtime costs are only incurred when logically necessary and are made explicit in the program's control flow.

Concurrency

Aela's concurrency is built on two orthogonal keywords, async and thread , that modify function declarations. These provide a clear, explicit syntax for defining concurrent work. The runtime manages a thread pool to execute these tasks, enabling both I/O-bound concurrency and CPU-bound parallelism that work in concert.

Core Keywords: async and thread

It is crucial to understand that `async` and `thread` are two separate modifiers with distinct meanings. Even though they are designed to work in a way that feels cohesive.

async

Marks a function as pausable . An async function can use the await keyword to non-blockingly wait for I/O or other long-running operations. It primarily relates to concurrency .

The decision to have a langauge-native async engine provides significant advantages for compile-time determinism.

thread

Marks a function as a parallel task . Calling a thread fn is a special operation that is non-blocking. It immediately submits the function to the runtime's thread pool for execution and returns a Task handle. It primarily relates to parallelism .

These keywords can be combined to define tasks with different behaviors:

Combination Meaning Primary Use Case
fn A regular, blocking function. Standard synchronous logic.
async fn A pausable, awaitable function. I/O-bound work on the current thread.
thread fn A function that runs in parallel in another thread. It cannot use await . CPU-intensive, blocking computations that should not stall the main thread.
async thread fn A pausable function that runs in parallel in a thread. A self-contained parallel task that performs its own I/O (e.g., a network client).

Defining and Spawning Tasks

Threaded tasks are ideal for the parallel processing of CPU intensive workloads.

Example
0 // This function is defined as a parallel task.
1 thread fn process_data ( source : DataSource ) - > Report {
2 return generate_report ( data ) ;
3 }

But sometimes a threaded task to compartmentalize work. For example, you might want some async processing to happen in another thread, separately from a UI thread.

The async keyword is optional and is used if the task needs to await I/O. The function's return type defines the final value returned when the task is complete.

Example
0 // This function is defined as a parallel task that is also async.
1 async thread fn process_data ( source : DataSource ) - > Report {
2 let data = await source . read ( ) ;
3 // ...
4 return generate_report ( data ) ;
5 }

Calling a function marked thread is a non-blocking operation. It starts the task in the background and immediately returns a Thread handle.

Example
0 // This call is non-blocking and returns a handle.
1 let handle : Thread ( Report ) = process_data ( my_source ) ;
2
3 // Await the handle to get the final result.
4 let report : Report = await handle . join ( ) ;

Structured Parallelism: thread { ... } Block

The thread block is used to run a group of tasks in parallel and wait for all of them to complete before continuing.

Example
0 async fn get_dashboard_data ( ) - > Dashboard {
1 var user : User ;
2 var orders : [ Order ] ;
3
4 // This block runs its internal async calls in parallel.
5 thread {
6 let user_task = fetch_user_profile ( 123 ) ;
7 let orders_task = fetch_recent_orders ( 123 ) ;
8
9 // The block implicitly awaits both before exiting.
10 user = await user_task ;
11 orders = await orders_task ;
12 }
13
14 // This line is only reached after both tasks are complete.
15 return Dashboard ( user , orders ) ;
16 }

Channels

Channels are used for streaming communication between threads. The type Channel(T) is a valid type according to the TypeApplication grammar rule.

Example
0 // Create a channel that transports string values.
1 let my_channel : Channel ( string ) = new { } ;

Streams

This pattern pauses the current function to process each message from a channel sequentially.

Example
0 // The `for await` loop consumes the receiver.
1 for await ( let message : string in my_channel . receiver ) {
2 process_message ( message ) ;
3 }

Events

This registers a handler and immediately returns, allowing the current function to continue its work.

Example
0 // `on_receive` takes a function expression as an argument.
1 my_channel . receiver . listen ( ( message : string ) - > void {
2 io : : print ( "Event received : { } " , message ) ;
3 } ) ;
4
5 // ... code here continues to run without blocking ...

Integrated Vehicle Safety vs. Aftermarket Parts

Think of it like the safety systems in a car.

A Library-Based Ecosystem (like Rust's): This is like buying a car chassis and then adding safety features yourself. You buy airbags from one company, an anti-lock braking system from another, and traction control from a third. They might be excellent components, but they weren't designed to work together as a single, cohesive system.

A Built-in Scheduler (Aela's model): This is like buying a modern car where the manufacturer has designed the airbags, ABS, traction control, and crumple zones to work together as a single, integrated safety system. It's tested as a whole and provides guarantees that the individual parts can't.

Here are the specific safety wins this integration provides.

  1. Compile-Time Data-Race Prevention

Because the scheduler is part of the language, the compiler has a deep understanding of what it means to "cross a thread boundary." It can enforce Send and Sync rules at compile-time. This means it's a syntax error to try to send non-thread-safe data into a thread fn or across a channel, completely eliminating an entire class of data-race bugs before the program can even run.

  1. A Single, Safe Bridge for Blocking Code

As we discussed, blocking in an async context is a huge foot-gun. A built-in runtime provides one, official, and well-defined function for handling this: std::task::run_blocking(). This prevents a scenario where different libraries provide their own, subtly different (and potentially unsafe) ways of handling blocking calls, which would lead to confusion and bugs.

  1. Guaranteed Structured Concurrency

The thread { ... } block is a major safety feature. Because it's a language construct powered by the built-in scheduler, the compiler can absolutely guarantee that all child tasks are completed before the parent function is allowed to continue. This prevents "leaked" tasks and makes error handling robust and predictable. In a library, this guarantee might be weaker or easier to accidentally bypass.

  1. Predictable Task Lifecycles

With a built-in scheduler, the behavior of a Task handle is predictable across the entire ecosystem. The rule that a handle will detach on drop is a language-level guarantee. This prevents situations where one library's handle might join on drop while another's aborts, leading to surprising behavior and resource leaks.

In short, a built-in scheduler allows Aela to treat concurrency as a core feature, subject to the same rigorous, compile-time safety analysis as the rest of the language.

Pool Control & Oversubscription

The Aela runtime is built on a work-stealing thread pool that defaults to using the number of logical CPU cores available (std::thread::available_parallelism()).

Pool Size & Pinning: The pool size can be overridden at startup via an environment variable (e.g., AELA_THREAD_COUNT=4). Fine-grained control like core pinning and thread priorities is considered a low-level OS feature and is not exposed through the high-level async/thread API. For expert use cases, a separate std::os::thread module could provide these unsafe, platform-specific controls.

On tiny targets without an OS, the runtime can be compiled in a "pool disabled" mode, using a single-threaded cooperative scheduler.

Oversubscription: The runtime's global task queue is bounded (e.g., a default capacity of 256 tasks). Calling a thread fn is cheap, but if the task queue is full, the call itself becomes an async operation. It will pause the calling function until space becomes available in the queue. This provides natural back-pressure and prevents developers from overwhelming the system.

Example
0 async fn io_bound_work1 ( ) - > int { // This already works
1 await doSomething ( ) ; // This already works
2 return 0 ;
3 }
4
5 thread fn compute_bound_work1 ( c : & Channel ( int ) ) - > int {
6 c . send ( 42 ) ; // some new data is ready
7 return 0 ; // the thread is finished
8 }
9
10 async fn main ( ) {
11 let c : Channel ( int ) = new shared { } ;
12
13 c . receive = fn ( i : int ) - > {
14 io : : print ( " { } " , i ) ;
15 } ;
16
17 let h : int = await compute_bound_work1 ( c ) ; // Actually returns `Task(int)` and resolves it
18 let i : int = await io_bound_work1 ( ) ; // Actually returns `Future(int)` and resolves it
19
20 let a : int [ ] = await std : : all ( [
21 compute_bound_work2 ( ) , // dispatched to the scheduler
22 compute_bound_work2 ( ) , // dispatched to the scheduler
23 io_bound_work2 ( ) // dispatched to the eventloop
24 ] ) ;
25
26 let h2 : Task ( int ) = compute_bound_work1 ( c ) ; // returns `Task(int)` lazy, not executing.
27
28 let i2 : int = await h2 ; // starts executing (ie, `await compute_bound_work1();`)
29 }
Example
0 enum RecvJob {
1 Msg ( Job ) ,
2 Cancelled ,
3 Timeout
4 }
5
6 async fn next_recv ( jobs : & Channel ( Job ) ) - > RecvJob {
7 let j : Job = await jobs . recv ( ) ;
8 return RecvJob : : Msg ( j ) ;
9 }
10
11 async fn on_cancel ( tok : & CancellationToken ) - > RecvJob {
12 await tok . cancelled ( ) ;
13 return RecvJob : : Cancelled ;
14 }
15
16 async fn after ( ms : Int ) - > RecvJob {
17 await timer . after_ms ( ms ) ;
18 return RecvJob : : Timeout ;
19 }
20
21 thread fn handle ( job : Job ) - > int {
22 // something interesting and cpu intensive
23 return 0 ;
24 }
25
26 async fn run_worker ( jobs : & Channel ( Job ) , tok : & CancellationToken ) - > int {
27 while ( true ) {
28
29 // `await select` atomically starts all provided cold awaitables, yields
30 // the first one to complete (as a tagged case for its arm), and cancels the rest.
31
32 let evt : RecvJob = await std : : select ( [
33 next_recv ( jobs ) ,
34 on_cancel ( tok ) ,
35 after ( 250 )
36 ] ) ;
37
38 match ( evt ) {
39 RecvJob : : Msg ( job ) = > { await handle ( job ) ; }
40 RecvJob : : Cancelled = > { break ; }
41 RecvJob : : Timeout = > { io : : print ( "tick" ) ; }
42 }
43 }
44 return 0 ;
45 }

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

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.

Formal Grammar Spec

' ReturnType RefinementType ::= '{' IDENTIFIER ':' Type KW_WHERE Expression '}' PrimitiveType ::= KW_U8 | KW_I8 | KW_U16 | KW_I16 | KW_U32 | KW_I32 | KW_U64 | KW_I64 | KW_F32 | KW_F64 | KW_BOOL | KW_CHAR | KW_STRING | KW_INT | KW_ARENA TypeArguments ::= '(' [ Type { ',' Type } ] ')' CompileTimeParameters ::= CompileTimeParameter { ',' CompileTimeParameter } CompileTimeParameter ::= IDENTIFIER | IDENTIFIER ':' Type RunTimeParameters ::= Parameter { ',' Parameter } FunctionParameters ::= RunTimeParameters | CompileTimeParameters [ ';' [ RunTimeParameters ] ] Parameter ::= [ '...' ] [ KW_MUT ] IDENTIFIER ':' Type FunctionTypeParameters ::= FunctionTypeParameter { ',' FunctionTypeParameter } FunctionTypeParameter ::= [ KW_MUT ] Type ArrayTypeModifier ::= '[' [ Expression ] ']' (* -------------------------------------------------------- ) ( COMMENTS ) ( -------------------------------------------------------- *) (* A single-line comment starts with // and continues to the end of the line ) SingleLineComment ::= '//' { ~('\n' | '\r') } (* A multi-line comment starts with /* and ends with */ ) MultiLineComment ::= '/*' { . } '*/' (* -------------------------------------------------------- ) ( STATEMENTS (Unambiguous) ) ( -------------------------------------------------------- *) Statement ::= MatchedStatement | UnmatchedStatement MatchedStatement ::= KW_IF '(' Expression ')' MatchedStatement KW_ELSE MatchedStatement | KW_IF KW_LET Pattern '=' Expression Block KW_ELSE MatchedStatement | Block | KW_RETURN [ Expression ] ';' | RaiseStatement | BreakStatement | ContinueStatement | WhileStatement | ForStatement | MatchStatement | ReserveStatement | ExpressionStatement | VarDeclaration | FunctionDeclaration | ';' UnmatchedStatement ::= KW_IF '(' Expression ')' Statement | KW_IF '(' Expression ')' MatchedStatement KW_ELSE UnmatchedStatement | KW_IF KW_LET Pattern '=' Expression Block | KW_IF KW_LET Pattern '=' Expression Block KW_ELSE UnmatchedStatement Block ::= '{' { Statement } '}' ExpressionStatement ::= Expression ';' BreakStatement ::= KW_BREAK ';' ContinueStatement ::= KW_CONTINUE ';' WhileStatement ::= KW_WHILE '(' Expression ')' Statement ForStatement ::= KW_FOR '(' ForDeclaratorList KW_IN Expression ')' Statement ForDeclaratorList ::= ForDeclarator { ',' ForDeclarator } ForDeclarator ::= ( KW_LET | KW_VAR ) IDENTIFIER ':' Type RaiseStatement ::= KW_RAISE Expression ';' (* -------------------------------------------------------- ) ( MATCH (Mandatory Exhaustive) ) ( - Expression form for atomic initialization. ) ( - Statement form for control flow. ) ( - Guards, @-bindings, and nesting are disallowed. ) ( -------------------------------------------------------- *) MatchStatement ::= KW_MATCH '(' Expression ')' '{' [ MatchStmtArm { ',' MatchStmtArm } [ ',' ] ] '}' MatchStmtArm ::= MatchArmPattern '=>' ( Block | ';' ) MatchArmPattern ::= Pattern { '|' Pattern } Pattern ::= LiteralPattern | IDENTIFIER // binding | '_' // wildcard | PathExpression [ '(' [ PatternList ] ')' ] // unit/tuple-variant | TuplePattern | StructPattern TuplePattern ::= '(' [ PatternList [ ',' ] ] ')' StructPattern ::= '{' [ StructFieldPat { ',' StructFieldPat } [ ',' ] ] '}' StructFieldPat ::= IDENTIFIER ( ':' Pattern )? | '...' IDENTIFIER PatternList ::= Pattern { ',' Pattern } RangePattern ::= INT_LITERAL ('..' | '..=') INT_LITERAL | CHAR_LITERAL ('..' | '..=') CHAR_LITERAL LiteralPattern ::= INT_LITERAL | STRING_LITERAL | CHAR_LITERAL | KW_TRUE | KW_FALSE | RangePattern (* -------------------------------------------------------- ) ( EXPRESSIONS (Pratt Parser Aligned) ) ( -------------------------------------------------------- *) Expression ::= AssignmentExpression AssignmentExpression ::= CoalescingExpression | AssignmentTarget AssignmentOperator AssignmentExpression (* keep syntax permissive; lvalue-ness is a semantic check *) AssignmentTarget ::= CoalescingExpression AssignmentOperator ::= '=' | '+=' | '-=' | '*=' | '/=' CoalescingExpression ::= LogicalOrExpression { '??' LogicalOrExpression } LogicalOrExpression ::= LogicalAndExpression { '||' LogicalAndExpression } LogicalAndExpression ::= BitwiseOrExpression { '&&' BitwiseOrExpression } BitwiseOrExpression ::= BitwiseXorExpression { '|' BitwiseXorExpression } BitwiseXorExpression ::= BitwiseAndExpression { '^' BitwiseAndExpression } BitwiseAndExpression ::= ShiftExpression { '&' ShiftExpression } ShiftExpression ::= EqualityExpression { ( '<<' | '>>' ) EqualityExpression } EqualityExpression ::= ComparisonExpression { ( '==' | '!=' ) ComparisonExpression } ComparisonExpression ::= AdditiveExpression { ( '<' | '<=' | '>' | '>=' ) AdditiveExpression } AdditiveExpression ::= MultiplicativeExpression { ( '+' | '-' ) MultiplicativeExpression } MultiplicativeExpression ::= CastExpression { ( '*' | '/' | '%' ) CastExpression } CastExpression ::= UnaryExpression { KW_AS Type } UnaryExpression ::= ( KW_AWAIT | '-' | '!' | '&' | '~' ) UnaryExpression | PostfixExpression PostfixExpression ::= PrimaryExpression { '(' [ ArgumentList ] ')' | '[' Expression ']' | ( '.' | '?.' ) IDENTIFIER } PrimaryExpression ::= PathExpression | Literal | '(' Expression ')' | ArrayLiteral | NamedStructLiteral | AnonymousStructLiteral | FunctionExpression | NewExpression | MatchExpression MatchExpression ::= KW_MATCH '(' Expression ')' '{' [ MatchExprArm { ',' MatchExprArm } [ ',' ] ] '}' MatchExprArm ::= MatchArmPattern '=>' Expression PathExpression ::= IDENTIFIER { '::' IDENTIFIER } (* -------------------------------------------------------- ) ( TEMPORAL EXPRESSIONS ) ( -------------------------------------------------------- *) TemporalExpression ::= KW_ALWAYS Expression | KW_EVENTUALLY Expression | KW_NEXT Expression | Expression KW_UNTIL Expression | Expression KW_RELEASE Expression | KW_FORALL IDENTIFIER KW_IN Expression ':' Expression | KW_EXISTS IDENTIFIER KW_IN Expression ':' Expression | Expression (* -------------------------------------------------------- ) ( LITERALS & HELPER RULES ) ( -------------------------------------------------------- *) Literal ::= INT_LITERAL | FLOAT_LITERAL | STRING_LITERAL | STRING_MULTILINE | CHAR_LITERAL | DurationLiteral | KW_TRUE | KW_FALSE ArrayLiteral ::= '[' [ ArgumentList ] ']' NamedStructLiteral ::= PathExpression StructLiteralBody AnonymousStructLiteral ::= StructLiteralBody StructLiteralBody ::= '{' [ StructElement { ',' StructElement } [ ',' ] ] '}' StructElement ::= ( IDENTIFIER ':' Expression ) | IDENTIFIER | '...' Expression ArgumentList ::= CallArgument { ',' CallArgument } CallArgument ::= [ '...' ] [ KW_MUT ] Expression FunctionExpression ::= FnModifiers KW_FN '(' [ FunctionParameters ] ')' '->' ReturnType FunctionBodyWithReturn (* -------------------------------------------------------- ) ( Automatic Dereference: All values returned by `new`, with ) ( or without modifiers, are reference types and are ) ( automatically dereferenced when used in expression and ) ( member access contexts. Users do not need to explicitly ) ( write *x to access the underlying value; the compiler ) ( inserts dereferences implicitly. ) ( -------------------------------------------------------- *) NewExpression ::= KW_NEW [ AllocationModifiers ] AllocationBody AllocationModifiers ::= KW_SHARED [ KW_ATOMIC ] | KW_STATIC [ KW_ATOMIC ] | KW_WEAK AllocationBody ::= PrimaryExpression | StructLiteralBody ReserveStatement ::= KW_RESERVE Expression KW_FROM Expression Block [ KW_ELSE Block ] (* -------------------------------------------------------- ) ( TERMINALS (TOKENS) ) ( -------------------------------------------------------- *) IDENTIFIER INT_LITERAL, FLOAT_LITERAL, STRING_LITERAL, STRING_MULTILINE, CHAR_LITERAL (* Keywords: *) KW_LET, KW_VAR, KW_FN, KW_IF, KW_IN, KW_ELSE, KW_WHILE, KW_FOR, KW_RETURN, KW_BREAK, KW_CONTINUE, KW_WHERE, KW_ASYNC, KW_AWAIT, KW_AS, KW_STRUCT, KW_IMPL, KW_THREAD, KW_PURE KW_ENUM, KW_MATCH, KW_TYPE, KW_VOID, KW_INT, KW_ARENA, KW_U8, KW_I8, KW_U16, KW_I16, KW_U32, KW_I32, KW_U64, KW_I64, KW_F32, KW_F64, KW_BOOL, KW_CHAR, KW_STRING, KW_TRUE, KW_FALSE, KW_IMPORT, KW_EXPORT, KW_FROM, KW_FFI, KW_MAP, KW_DURATION, KW_INSTANT, (* No shared mutability without atomics! *) KW_NEW, KW_RESERVE, KW_SHARED, KW_ATOMIC, KW_WEAK, KW_STATIC, KW_MUT, KW_SYSTEM, KW_ACTION, KW_REQUIRES, KW_ENSURES, KW_INVARIANT, KW_PROPERTY, KW_FORALL, KW_EXISTS, KW_ALWAYS, KW_EVENTUALLY, KW_NEXT, KW_UNTIL, KW_RELEASE, KW_FAULT, KW_RAISE (* Operators and Delimiters: Arithmetic Wraps ) '=', '+=', '-=', '*=', '/=', '+', '-', '*', '/', '%', '&', '==', '!=', '<', '<=', '>', '>=', '!', '&&', '||', '|', '^', '~', '<<', '>>', '(', ')', '{', '}', '[', ']', ',', ';', '.', ':', '::', '?', '?.', '??', '...', '..=', '..', '->', '_', '=>' EOF " title="Grammar" id="8107cf2fa09a3">
Grammar
0 ( * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = )
1 ( Aela Language Grammar 0 . 0 . 8 )
2 ( Finalized : 2025 - 10 - 12 )
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 | FaultDeclaration
24 )
25
26 TypeAliasDeclaration : : = KW_TYPE IDENTIFIER '=' Type ';'
27
28 ReExportDeclaration : : =
29 KW_EXPORT ( NamedImport | IDENTIFIER )
30 KW_FROM STRING_LITERAL ';'
31
32 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
33 ( IMPORTS )
34 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
35
36 ImportStatement : : =
37 KW_IMPORT ( NamedImport | IDENTIFIER )
38 KW_FROM STRING_LITERAL ';'
39
40 NamedImport : : = '{' [ ImportSpecifier { ',' ImportSpecifier } [ ',' ] ] '}'
41 ImportSpecifier : : = IDENTIFIER [ ':' IDENTIFIER ]
42
43 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
44 ( FFI ( Foreign Function Interface ) )
45 ( - Contracts are compile - time enforced to be UB - free )
46 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
47
48 FfiDeclaration : : = KW_FFI IDENTIFIER '=' Type ';'
49
50 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
51 ( DECLARATIONS )
52 ( - var is mutable , let is immutable )
53 ( - aliases are borrow - checked by the analyzer )
54 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
55
56 VarDeclaration : : = ( KW_VAR | KW_LET ) IDENTIFIER ':' Type
57 [ '=' Expression ] ';'
58
59 StructDeclaration : : = KW_STRUCT IDENTIFIER '(' [ FunctionParameters ] ')' . . .
60
61 StructFieldDeclaration : : =
62 ( IDENTIFIER ':' Type )
63 | ( '...' IDENTIFIER )
64
65 VarDeclaration : : = ( KW_VAR | KW_LET ) IDENTIFIER ':' Type
66 [ '=' Expression ] ';'
67
68 FnModifiers : : =
69 [ ( KW_THREAD [ KW_PURE ] ) // thread; pure+thread allowed
70 | ( KW_PURE [ KW_THREAD ] ) // pure; optional thread
71 | KW_ASYNC // async alone (no pure)
72 ]
73
74 ImplBlock : : = KW_IMPL Type '{' { FunctionDeclaration | InvariantDeclaration } '}'
75
76
77 FunctionDeclaration : : =
78   FnModifiers KW_FN IDENTIFIER
79   '(' [ FunctionParameters ] ')' '->' ReturnType
80   ( ';' | FunctionBodyWithReturn )
81
82 FunctionBodyWithReturn : : =
83 '{' { Statement } ReturnStatement '}'
84
85 ReturnStatement : : = KW_RETURN [ Expression ] ';'
86
87 EnumDeclaration : : = KW_ENUM IDENTIFIER '(' [ FunctionParameters ] ')' . . .
88
89 TypeArguments : : = '(' [ TypeOrConst { ',' TypeOrConst } ] ')'
90 TypeOrConst : : = Type | ConstExpression
91
92 SystemDeclaration : : = KW_SYSTEM IDENTIFIER '{'
93 { ActionDeclaration
94 | InvariantDeclaration
95 | PropertyDeclaration
96 }
97 '}'
98
99 ActionDeclaration : : = KW_ACTION IDENTIFIER
100 '(' [ FunctionParameters ] ')'
101 [ RequiresClause ]
102 [ EnsuresClause ]
103 Block
104
105 RequiresClause : : = KW_REQUIRES Expression
106 EnsuresClause : : = KW_ENSURES Expression
107
108 InvariantDeclaration : : = KW_INVARIANT IDENTIFIER ':' Expression
109 PropertyDeclaration : : = KW_PROPERTY IDENTIFIER ':' TemporalExpression
110
111 FaultDeclaration : : = KW_FAULT IDENTIFIER '(' [ FunctionParameters ] ')' ';'
112
113 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
114 ( TYPES )
115 ( - - - - - )
116 ( The `(...)` syntax following a type identifier is used )
117 ( for type - level parameters , which can include both )
118 ( types AND values , to support Dependent Types . This )
119 ( differs from the generics syntax in languages like )
120 ( Rust or C + + , which typically use `<...>` for type - only )
121 ( parameters . )
122 ( )
123 ( Aela does not add built in properties or methods , instead )
124 ( it uses std : : length ( v ) , std : : size ( v ) , or standard library )
125 ( functions ie `import { vec } from "core/vector.ae";` )
126 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
127
128 Type : : = [ '&' ] PostfixType
129
130 PostfixType : : = SimpleType { ArrayTypeModifier | TypeArguments | '?' }
131
132 ReturnType : : = Type [ '|' FaultTypeList ]
133 FaultTypeList : : = FaultType { '|' FaultType }
134 FaultType : : = PathExpression
135
136 MapType : : = KW_MAP '(' Type ',' Type ')'
137
138 SimpleType : : = PrimitiveType
139 | KW_VOID
140 | FunctionTypeSignature
141 | PathExpression
142 | MapType
143 | RefinementType
144 | '(' Type ')'
145
146 FunctionTypeSignature : : =
147 FnModifiers KW_FN '(' [ FunctionTypeParameters ] ')' '->' ReturnType
148
149 RefinementType : : = '{' IDENTIFIER ':' Type KW_WHERE Expression '}'
150
151 PrimitiveType : : = KW_U8 | KW_I8 | KW_U16 | KW_I16 | KW_U32 | KW_I32
152 | KW_U64 | KW_I64 | KW_F32 | KW_F64 | KW_BOOL
153 | KW_CHAR | KW_STRING | KW_INT | KW_ARENA
154
155 TypeArguments : : = '(' [ Type { ',' Type } ] ')'
156
157 CompileTimeParameters : : = CompileTimeParameter { ',' CompileTimeParameter }
158 CompileTimeParameter : : = IDENTIFIER | IDENTIFIER ':' Type
159 RunTimeParameters : : = Parameter { ',' Parameter }
160
161 FunctionParameters : : =
162 RunTimeParameters
163 | CompileTimeParameters [ ';' [ RunTimeParameters ] ]
164
165 Parameter : : = [ '...' ] [ KW_MUT ] IDENTIFIER ':' Type
166 FunctionTypeParameters : : = FunctionTypeParameter { ',' FunctionTypeParameter }
167 FunctionTypeParameter : : = [ KW_MUT ] Type
168 ArrayTypeModifier : : = '[' [ Expression ] ']'
169
170 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
171 ( COMMENTS )
172 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
173
174 ( * A single - line comment starts with // and continues to the end of the line )
175 SingleLineComment : : = '//' { ~('\n' | '\r') }
176
177 ( * A multi - line comment starts with /* and ends with */ )
178 MultiLineComment : : = '/*' { . } '*/'
179
180 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
181 ( STATEMENTS ( Unambiguous ) )
182 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
183
184 Statement : : = MatchedStatement | UnmatchedStatement
185
186 MatchedStatement : : =
187 KW_IF '(' Expression ')' MatchedStatement KW_ELSE MatchedStatement
188 | KW_IF KW_LET Pattern '=' Expression Block KW_ELSE MatchedStatement
189 | Block
190 | KW_RETURN [ Expression ] ';'
191 | RaiseStatement
192 | BreakStatement
193 | ContinueStatement
194 | WhileStatement
195 | ForStatement
196 | MatchStatement
197 | ReserveStatement
198 | ExpressionStatement
199 | VarDeclaration
200 | FunctionDeclaration
201 | ';'
202
203 UnmatchedStatement : : =
204 KW_IF '(' Expression ')' Statement
205 | KW_IF '(' Expression ')' MatchedStatement KW_ELSE UnmatchedStatement
206 | KW_IF KW_LET Pattern '=' Expression Block
207 | KW_IF KW_LET Pattern '=' Expression Block KW_ELSE UnmatchedStatement
208
209 Block : : = '{' { Statement } '}'
210 ExpressionStatement : : = Expression ';'
211 BreakStatement : : = KW_BREAK ';'
212 ContinueStatement : : = KW_CONTINUE ';'
213
214 WhileStatement : : = KW_WHILE '(' Expression ')' Statement
215
216 ForStatement : : = KW_FOR '(' ForDeclaratorList KW_IN Expression ')' Statement
217 ForDeclaratorList : : = ForDeclarator { ',' ForDeclarator }
218 ForDeclarator : : = ( KW_LET | KW_VAR ) IDENTIFIER ':' Type
219
220 RaiseStatement : : = KW_RAISE Expression ';'
221
222 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
223 ( MATCH ( Mandatory Exhaustive ) )
224 ( - Expression form for atomic initialization . )
225 ( - Statement form for control flow . )
226 ( - Guards , @ - bindings , and nesting are disallowed . )
227 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
228
229 MatchStatement : : = KW_MATCH '(' Expression ')' '{'
230 [ MatchStmtArm { ',' MatchStmtArm } [ ',' ] ]
231 '}'
232
233 MatchStmtArm : : = MatchArmPattern '=>' ( Block | ';' )
234
235 MatchArmPattern : : = Pattern { '|' Pattern }
236
237 Pattern : : =
238 LiteralPattern
239 | IDENTIFIER // binding
240 | '_' // wildcard
241 | PathExpression [ '(' [ PatternList ] ')' ] // unit/tuple-variant
242 | TuplePattern
243 | StructPattern
244
245 TuplePattern : : = '(' [ PatternList [ ',' ] ] ')'
246
247 StructPattern : : = '{' [ StructFieldPat { ',' StructFieldPat } [ ',' ] ] '}'
248 StructFieldPat : : = IDENTIFIER ( ':' Pattern ) ? | '...' IDENTIFIER
249
250 PatternList : : = Pattern { ',' Pattern }
251
252 RangePattern : : =
253 INT_LITERAL ( '..' | '..=' ) INT_LITERAL
254 | CHAR_LITERAL ( '..' | '..=' ) CHAR_LITERAL
255
256 LiteralPattern : : = INT_LITERAL | STRING_LITERAL | CHAR_LITERAL | KW_TRUE | KW_FALSE | RangePattern
257
258 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
259 ( EXPRESSIONS ( Pratt Parser Aligned ) )
260 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
261
262 Expression : : = AssignmentExpression
263
264 AssignmentExpression : : =
265 CoalescingExpression
266 | AssignmentTarget AssignmentOperator AssignmentExpression
267
268 ( * keep syntax permissive ; lvalue - ness is a semantic check * )
269 AssignmentTarget : : = CoalescingExpression
270
271 AssignmentOperator : : = '=' | '+=' | '-=' | '*=' | '/='
272
273 CoalescingExpression : : = LogicalOrExpression { '??' LogicalOrExpression }
274
275 LogicalOrExpression : : = LogicalAndExpression { '||' LogicalAndExpression }
276
277 LogicalAndExpression : : = BitwiseOrExpression { '&&' BitwiseOrExpression }
278
279 BitwiseOrExpression : : = BitwiseXorExpression { '|' BitwiseXorExpression }
280
281 BitwiseXorExpression : : = BitwiseAndExpression { '^' BitwiseAndExpression }
282
283 BitwiseAndExpression : : = ShiftExpression { '&' ShiftExpression }
284
285 ShiftExpression : : = EqualityExpression { ( '<<' | '>>' ) EqualityExpression }
286
287 EqualityExpression : : = ComparisonExpression { ( '==' | '!=' ) ComparisonExpression }
288
289 ComparisonExpression : : = AdditiveExpression { ( '<' | '<=' | '>' | '>=' ) AdditiveExpression }
290
291 AdditiveExpression : : = MultiplicativeExpression { ( '+' | '-' ) MultiplicativeExpression }
292
293 MultiplicativeExpression : : = CastExpression { ( '*' | '/' | '%' ) CastExpression }
294
295 CastExpression : : = UnaryExpression { KW_AS Type }
296
297 UnaryExpression : : =
298 ( KW_AWAIT | '-' | '!' | '&' | '~' ) UnaryExpression
299 | PostfixExpression
300
301 PostfixExpression : : =
302 PrimaryExpression {
303 '(' [ ArgumentList ] ')'
304 | '[' Expression ']'
305 | ( '.' | '?.' ) IDENTIFIER
306 }
307
308 PrimaryExpression : : =
309 PathExpression
310 | Literal
311 | '(' Expression ')'
312 | ArrayLiteral
313 | NamedStructLiteral
314 | AnonymousStructLiteral
315 | FunctionExpression
316 | NewExpression
317 | MatchExpression
318
319 MatchExpression : : = KW_MATCH '(' Expression ')' '{'
320 [ MatchExprArm { ',' MatchExprArm } [ ',' ] ]
321 '}'
322
323 MatchExprArm : : = MatchArmPattern '=>' Expression
324
325 PathExpression : : = IDENTIFIER { '::' IDENTIFIER }
326
327 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
328 ( TEMPORAL EXPRESSIONS )
329 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
330
331 TemporalExpression : : =
332 KW_ALWAYS Expression
333 | KW_EVENTUALLY Expression
334 | KW_NEXT Expression
335 | Expression KW_UNTIL Expression
336 | Expression KW_RELEASE Expression
337 | KW_FORALL IDENTIFIER KW_IN Expression ':' Expression
338 | KW_EXISTS IDENTIFIER KW_IN Expression ':' Expression
339 | Expression
340
341 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
342 ( LITERALS & HELPER RULES )
343 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
344
345 Literal : : =
346 INT_LITERAL | FLOAT_LITERAL | STRING_LITERAL | STRING_MULTILINE
347 | CHAR_LITERAL | DurationLiteral | KW_TRUE | KW_FALSE
348
349 ArrayLiteral : : = '[' [ ArgumentList ] ']'
350 NamedStructLiteral : : = PathExpression StructLiteralBody
351 AnonymousStructLiteral : : = StructLiteralBody
352 StructLiteralBody : : = '{' [ StructElement { ',' StructElement } [ ',' ] ] '}'
353 StructElement : : = ( IDENTIFIER ':' Expression ) | IDENTIFIER | '...' Expression
354
355 ArgumentList : : = CallArgument { ',' CallArgument }
356 CallArgument : : = [ '...' ] [ KW_MUT ] Expression
357
358 FunctionExpression : : = FnModifiers KW_FN '(' [ FunctionParameters ] ')' '->' ReturnType FunctionBodyWithReturn
359
360 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
361 ( Automatic Dereference : All values returned by `new` , with )
362 ( or without modifiers , are reference types and are )
363 ( automatically dereferenced when used in expression and )
364 ( member access contexts . Users do not need to explicitly )
365 ( write * x to access the underlying value ; the compiler )
366 ( inserts dereferences implicitly . )
367 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
368
369 NewExpression : : =
370 KW_NEW [ AllocationModifiers ] AllocationBody
371
372 AllocationModifiers : : =
373 KW_SHARED [ KW_ATOMIC ]
374 | KW_STATIC [ KW_ATOMIC ]
375 | KW_WEAK
376
377 AllocationBody : : =
378 PrimaryExpression
379 | StructLiteralBody
380
381 ReserveStatement : : =
382 KW_RESERVE Expression KW_FROM Expression Block [ KW_ELSE Block ]
383
384 ( * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )
385 ( TERMINALS ( TOKENS ) )
386 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * )
387
388 IDENTIFIER
389 INT_LITERAL , FLOAT_LITERAL , STRING_LITERAL , STRING_MULTILINE , CHAR_LITERAL
390
391 ( * Keywords : * )
392 KW_LET , KW_VAR , KW_FN , KW_IF , KW_IN , KW_ELSE , KW_WHILE , KW_FOR ,
393 KW_RETURN , KW_BREAK , KW_CONTINUE , KW_WHERE ,
394 KW_ASYNC , KW_AWAIT , KW_AS , KW_STRUCT , KW_IMPL , KW_THREAD , KW_PURE
395 KW_ENUM , KW_MATCH , KW_TYPE , KW_VOID , KW_INT , KW_ARENA ,
396 KW_U8 , KW_I8 , KW_U16 , KW_I16 , KW_U32 , KW_I32 , KW_U64 , KW_I64 ,
397 KW_F32 , KW_F64 , KW_BOOL , KW_CHAR , KW_STRING , KW_TRUE , KW_FALSE ,
398 KW_IMPORT , KW_EXPORT , KW_FROM , KW_FFI , KW_MAP ,
399 KW_DURATION , KW_INSTANT ,
400
401 ( * No shared mutability without atomics ! * )
402 KW_NEW , KW_RESERVE , KW_SHARED , KW_ATOMIC , KW_WEAK , KW_STATIC , KW_MUT ,
403
404 KW_SYSTEM , KW_ACTION , KW_REQUIRES , KW_ENSURES ,
405 KW_INVARIANT , KW_PROPERTY , KW_FORALL , KW_EXISTS , KW_ALWAYS , KW_EVENTUALLY , KW_NEXT , KW_UNTIL ,
406 KW_RELEASE , KW_FAULT , KW_RAISE
407
408 ( * Operators and Delimiters : Arithmetic Wraps )
409 '=' , '+=' , '-=' , '*=' , '/=' , '+' , '-' , '*' , '/' , '%' , '&' , '==' , '!=' , '<' , '<=' , '>' , '>=' ,
410 '!' , '&&' , '||' , '|' , '^' , '~' , '<<' , '>>' , '(' , ')' , '{' , '}' , '[' , ']' ,
411 ',' , ';' , '.' , ':' , '::' , '?' , '?.' , '??' , '...' , '..=' , '..' , '->' , '_' , '=>'
412
413 EOF