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.
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 ) {
1 first : T ,
2 second : U
3 }
4
5 let p : Pair ( i32 , string ) = {
6 first : 1 ,
7 second : "hi"
8 } ;
9
10 let x : Option ( i32 ) = Option : : Some ( 3 ) ;

Reference Types

&T is a borrowed reference to T . In Aela, the mut keyword isn't part of a type; it's a marker on a parameter or call argument that grants temporary, mutable access (a "mutable loan") to a value.

  • 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.
On Parameter Definition (fn):
0 fn foo ( mut param : & Type ) - > void {
1 }

This declares: "This function requires a mutable loan for param. I intend to modify the original value that the caller passes."

On Call Argument (call):
0 foo ( mut my_value ) ;

This declares: "I am granting a mutable loan to foo for this function call. I am aware that foo may modify it."

This design makes it explicit at both the function's definition site and the call site exactly where a value is allowed to be changed, aligning with the "in-out parameter" model many developers are familiar with.

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="1868225b33b17">
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

Boolean Condition
0 if ( )
1 [ else ]
Pattern-Match Binding (if-let)
0 if let =
1 [ else ]

Description

Standard conditional branching. if statements have two primary forms:

Boolean Condition: The standard form evaluates a which must result in a bool. If true, the then_statement (usually a block) is executed. If false, the optional else_statement is executed.

Boolean Condition
0 let x : i32 = 10 ;
1
2 if ( x > 0 ) {
3 print ( "Positive" ) ;
4 } else {
5 print ( "Non - positive" ) ;
6 }

Pattern-Match Binding (if-let): This form attempts to match the result of against the given .

If the match is successful, any variables bound in the are introduced, and the then_block is executed. These new variables are only in scope inside this block.

If the match is unsuccessful, the else statement is executed.

Pattern-Match Binding (if-let)
0 let v : Option ( i32 ) = Some ( 42 ) ;
1
2 if let Option : : Some ( value ) = v {
3 // 'value' is bound and only in scope here
4 print ( "Got value : { } " , value ) ;
5 } else {
6 // 'value' is not in scope here
7 print ( "Got None" ) ;
8 }

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 : i32 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

Aela supports two distinct forms of `match` :

  1. Statement `match` — used for control flow and side effects
  2. Expression `match` — used to compute a value

Which form you get is determined syntactically , not by context.

4.1 Statement match

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

A statement `match` is used for control flow. Each arm executes a block of statements and does not produce a value.

Block arms `{ ... }` are allowed return , break , and side effects are allowed * The match itself has no value

This form is typically used for branching logic, validation, logging, or early returns.

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

4.2 Expression match

Example
0 let | var : [ type ] = match ( ) {
1 = > ,
2 . . .
3 = >
4 } ;

An expression `match` computes a value.

  • Each arm must be a single expression
  • Block bodies `{ ... }` are not allowed
  • The match must be exhaustive
  • All arm expressions must unify to a single type

This restriction avoids implicit returns, ambiguous control flow, and complex typing rules.

"one", _ => "other" };" lang="aela" title="Example" id="e30ea1c5c1206">
Example
0 let label : string = match ( value ) {
1 0 = > "zero" ,
2 1 = > "one" ,
3 _ = > "other"
4 } ;

Note

Block arms are not allowed in expression-matches!

1 };" lang="aela" title="Example" id="b51c642e06fd1">
Example
0 let x = match ( value ) {
1 0 = > { print ( "zero" ) ; 0 } , // ERROR
2 _ = > 1
3 } ;

Instead, move side effects outside the expression match.

{} }" lang="aela" title="Example" id="d2a67f0969238">
Example
0 match ( value ) {
1 0 = > print ( "zero" ) ,
2 _ = > { }
3 }

In Aela, blocks are statements , not expressions. A block does not produce a value unless explicitly used as part of a language construct that allows expressions. The reason for this is, we optimize for explicitness, predictability, and bounded complexity. It separates control flow from value computation .

  • Statement `match` - control flow, blocks, side effects
  • Expression `match` - values only, expressions only

NOTE

No implicit block return or tail-values. No semicolon footguns.

Example
0 x // yields
1 x ; // discards

This creates a well-known class of bugs:

  • missing semicolon changes semantics
  • especially dangerous for JS/C-family programmers
  • hard to spot in reviews

Aela eliminates an entire class of bugs here:

  • blocks never yield values
  • expressions yield values
  • the grammar enforces the distinction

Instead of a lint rule its a language guarantee. You don't need a never type. And it keeps things easier to learn.

4.3 Struct Literals in Expression Matches

Brace syntax { ... } is still allowed in expression matches when it is a struct literal, not a block.

Example
0 let p = match ( kind ) {
1 A = > { x : 1 , y : 2 } ,
2 B = > { x : 3 , y : 4 }
3 } ;

If a brace body does not form a valid struct literal, it is rejected with an error.

4.4 Summary

Feature Statement match Expression match
Produces a value NO OK
Allows { ... } blocks OK NO
Allows return OK NO
Used for Control flow Value computation

5. return

Aela requires explicit `return` statements for all function exits. Functions do not implicitly return the value of their final expression. This is an intentional design choice that prioritizes clarity and predictability over implicit behavior.

Syntax

Example
0 return [ ] ;

Examples

Example
0 return ;
1 return x + 1 ;

Description

Exits a function immediately with the return value perscribed by the function signature.

Function boundaries are explicit

Requiring return makes it immediately obvious where a function exits and what value is returned. This is especially important in functions with multiple branches or early exits.

Example
0 fn add ( x : i32 , y : i32 ) - > i32 {
1 return x + y ;
2 }

There is no ambiguity about what value leaves the function.

Avoids semicolon-sensitive behavior

In expression-oriented languages, a missing semicolon can silently change behavior: Aela avoids this entire class of bugs by never treating the final expression of a function as an implicit return.

Example
0 x // yields
1 x ; // discards

Prevents accidental returns

Without implicit returns, writing an expression at the end of a function body does not change control flow: This makes accidental returns impossible and forces intent to be explicit.

Example
0 fn foo ( x : i32 ) - > i32 {
1 x + 42 ; // evaluated, but not returned
2 return x ;
3 }

4. Keeps return semantics simple and consistent

In Aela, return always means "Exit the current function immediately!" It's never overloaded to mean...

  • return from a block
  • yield from a match arm
  • produce the value of an expression

This simplicity avoids subtle interactions with pattern matching, block structure, and type inference.

Summary

Design Choice Result
Explicit return Clear function exits
No implicit function returns No semicolon footguns
return never yields values Simple control-flow reasoning
Match expressions stay value-only No hidden complexity

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.

NOTE

Blocks never yield implicit values!

Example

Example
0 {
1 let x : i32 = 1 ;
2 let y : i32 = 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="ead86df0f5d3d">
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 }

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 ( ) ;

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

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 [ AEA0012 ] : 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 [ public ] fn ( self : & Self , . . . ) - > { . . . }
6 }

Details

  • Methods are private by default. To allow a method or constructor to be called from another module, it must be marked with the public keyword.
  • The fn 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 public 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() -> i32 { 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="cf14d0c6cb7c9">
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 ( ) - > i32 {
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 : i32 ;
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 ( (i32) 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 (integer * 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 + integer
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: i32, elem_size: i32) -> 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: i32, 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: i32) -> 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)) -> i32
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: i32, len: i32) -> &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: i32, len: i32) -> 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 : i32 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.

Atomics

Introduction

Atomic(T) is a fundamental intrinsic type designed for high-performance concurrency. It provides a wrapper around primitive types that guarantees safe access across multiple threads.

Unlike standard variables, operations on Atomic(T) are indivisible. However, atomicity alone is insufficient for correctness; memory ordering is equally critical. This API exposes fine-grained control over how the CPU and compiler are allowed to reorder memory operations, enabling the construction of lock-free data structures and synchronization primitives.

Core Concepts

Atomicity & Type Constraints

An operation is atomic if it is indivisible: a thread observes either the old value or the new value, never a "torn" or intermediate state.

Allowed Types : `T` must be a primitive integer, boolean, pointer, or an `enum` with a fixed underlying integer representation (e.g., `repr(u8)`) where all bit patterns are valid. Alignment : Atomic(T) requires natural alignment for T . Static Check : If alignment is statically known to be insufficient, it is a compile-time error . Dynamic Check : If a pointer is misaligned at runtime, the behavior is a guaranteed trap (panic).

Lock-Free Guarantees

While Atomic(T) enables lock-free algorithms, the operations themselves are not guaranteed to be lock-free on all platforms for all types.

Hardware Support : If the target CPU supports atomic instructions for `sizeof(T)`, operations are compiled to those instructions. Software Fallback : If the hardware lacks support (e.g., 64-bit atomics on 32-bit arch), the runtime may use a hidden global lock / hashed lock pool. Intrinsic Check *: Use std::atomic::is_lock_free() -> bool to query if operations on T are truly lock-free on the current target.

Memory Ordering

The Ordering enum controls how operations synchronize with other threads.

  1. `Relaxed` : No synchronization. Only atomicity is ensured.
  2. `Acquire` : Valid for loads. It ensures that no subsequent memory accesses can be reordered before this operation.
  3. `Release` : Valid for stores. It ensures that no previous memory accesses can be reordered after this operation.
  4. `AcqRel` : Valid for RMW (Read-Modify-Write). Combines Acquire and Release.
  5. `SeqCst` : Strongest ordering. Enforces a total order on all SeqCst operations consistent with program order.

The Synchronization Contract

The primary mechanism for thread coordination is the Acquire-Release pair :

An Acquire operation on an atomic object synchronizes-with a Release operation on the same object if the Acquire reads the value written by that Release (or a value from a release sequence headed by that Release ).

Effect : All memory writes that happened before the Release are guaranteed to be visible to the thread that performed the Acquire .

The Intrinsic API

std::atomic::load

Signature load(ptr: &Atomic(T), order: Ordering) -> T
Constraints order must be Relaxed , Acquire , or SeqCst .
Description Atomically reads the value.

std::atomic::store

Signature store(ptr: &Atomic(T), val: T, order: Ordering)
Constraints order must be Relaxed , Release , or SeqCst .
Description Atomically writes a value.

std::atomic::exchange

Signature exchange(ptr: &Atomic(T), val: T, order: Ordering) -> T
Constraints Any Ordering is valid.
Description Atomically writes val and returns the previous value (the value immediately before the swap).

std::atomic::compare_exchange

Signature compare_exchange(ptr: &Atomic(T), expected: T, desired: T, success: Ordering, failure: Ordering) -> (bool, T)

| Constraints | 1. failure cannot be Release or AcqRel (failure is a load).

  1. failure cannot be stronger than success (e.g., if success is Relaxed , failure cannot be SeqCst ). |
  2. | Description | Atomically checks if *ptr == expected .

Success : Writes desired using success order. Returns (true, old_val) .

Failure : Loads current value using failure order. Returns (false, current_val) .

Note : old_val and current_val represent the value at ptr immediately before the operation.* |

std::atomic::fetch_add / sub / and / or / xor

Signature fetch_op(ptr: &Atomic(T), val: T, order: Ordering) -> T
Constraints Any Ordering is valid.
Description Atomically performs the arithmetic/bitwise operation and returns the previous value.

Usage & Safety

i32 { io::println("Hello World"); // ...normal file code... return 0; } /**  * Test 1: Basic Load and Store  */ #test fn test_atomic_basic(t: &Tap) -> bool {   var val: Atomic(i32) = Atomic(10);   // Test Initial load (Using SeqCst for strongest safety)   t.ok(std::atomic::load(&val, Ordering::SeqCst) == 10, "Atomic initializes correctly");   // Test Store   std::atomic::store(&val, 42, Ordering::SeqCst);   let result = std::atomic::load(&val, Ordering::SeqCst);   t.ok(result == 42, "Atomic store/load persisted value");   return true; } /**  * Test 2: Compare and Exchange (CAS)  */ #test fn test_atomic_cas(t: &Tap) -> bool {   var val: Atomic(i32) = Atomic(100);   // 1. Successful Swap   // Expected: 100, New: 200. Current is 100. -> Should succeed.   let old_val_1 = std::atomic::compare_exchange( &val, 100, 200, Ordering::SeqCst, // Success order Ordering::SeqCst // Failure order );   // Check if the returned value matches our expectation   let success1 = (old_val_1 == 100);   t.ok(success1 == true, "CAS success reported true (old value matched expected)");   t.ok(old_val_1 == 100, "CAS returned original value");   t.ok(std::atomic::load(&val, Ordering::SeqCst) == 200, "CAS success updated memory to 200");   // 2. Failed Swap (Stale read)   // Expected: 100 (stale), New: 300. Current is 200. -> Should fail.   let old_val_2 = std::atomic::compare_exchange( &val, 100, 300, Ordering::SeqCst, Ordering::SeqCst );   let success2 = (old_val_2 == 100);   t.ok(success2 == false, "CAS failure reported false (old value did not match)");   t.ok(old_val_2 == 200, "CAS failure returns current actual value (200)");   t.ok(std::atomic::load(&val, Ordering::SeqCst) == 200, "CAS failure did not update memory");   return true; } /**  * Test 3: Concurrent Increment  */ #test fn test_atomic_concurrency(t: &Tap) -> bool {   var counter: Atomic(i32) = Atomic(0);   let iterations = 50;   // Define the driver as a named async function to satisfy the compiler   async fn driver() -> void {     // Define the worker logic     async fn worker() -> void {       var i = 0;       while (i < iterations) { // Use Relaxed for simple counters where order relative to other vars doesn't matter         std::atomic::fetch_add(&counter, 1, Ordering::Relaxed);         await sleep(1ms);         i += 1;       }     }     // Spawn two workers     let t1 = worker();     let t2 = worker();     // Await them     await t1;     await t2;   }   // Pass the invoked function future to block_on   std::concurrency::block_on(driver());   let final_count = std::atomic::load(&counter, Ordering::SeqCst);   let expected = iterations * 2;   t.ok(final_count == expected, `Concurrent add expected ${expected}, got ${final_count}`);   return true; } #test fn main () -> i32 { // always the test main   let tests: Test[] = [     test_atomic_basic,     test_atomic_cas,     test_atomic_concurrency,   ];   let t: Tap = {};   t.comment("# Testing Atomics\n");   t.plan(9);   t.run(tests); if (t.failed > 0) { return 1; // Standard "Generic Error" code }   return 0; }" lang="ae" title="Example" id="b4f85316c768b">
Example
0 #test import { Test, Tap } from "test";
1 #test import { sleep } from "time";
2 #test import { Ordering } from "sync";
3
4 import io from "io" ;
5
6 fn main ( ) - > i32 {
7 io : : println ( "Hello World" ) ;
8 // ...normal file code...
9 return 0 ;
10 }
11
12 / * *
13   * Test 1 : Basic Load and Store
14   * /
15 #test fn test_atomic_basic(t: &Tap) -> bool {
16   var val : Atomic ( i32 ) = Atomic ( 10 ) ;
17
18   // Test Initial load (Using SeqCst for strongest safety)
19   t . ok ( std : : atomic : : load ( & val , Ordering : : SeqCst ) = = 10 , "Atomic initializes correctly" ) ;
20
21   // Test Store
22   std : : atomic : : store ( & val , 42 , Ordering : : SeqCst ) ;
23   let result = std : : atomic : : load ( & val , Ordering : : SeqCst ) ;
24
25   t . ok ( result = = 42 , "Atomic store / load persisted value" ) ;
26   return true ;
27 }
28
29 / * *
30   * Test 2 : Compare and Exchange ( CAS )
31   * /
32 #test fn test_atomic_cas(t: &Tap) -> bool {
33   var val : Atomic ( i32 ) = Atomic ( 100 ) ;
34
35   // 1. Successful Swap
36   // Expected: 100, New: 200. Current is 100. -> Should succeed.
37   let old_val_1 = std : : atomic : : compare_exchange (
38 & val ,
39 100 ,
40 200 ,
41 Ordering : : SeqCst , // Success order
42 Ordering : : SeqCst // Failure order
43 ) ;
44
45   // Check if the returned value matches our expectation
46   let success1 = ( old_val_1 = = 100 ) ;
47
48   t . ok ( success1 = = true , "CAS success reported true ( old value matched expected ) " ) ;
49   t . ok ( old_val_1 = = 100 , "CAS returned original value" ) ;
50   t . ok ( std : : atomic : : load ( & val , Ordering : : SeqCst ) = = 200 , "CAS success updated memory to 200" ) ;
51
52   // 2. Failed Swap (Stale read)
53   // Expected: 100 (stale), New: 300. Current is 200. -> Should fail.
54   let old_val_2 = std : : atomic : : compare_exchange (
55 & val ,
56 100 ,
57 300 ,
58 Ordering : : SeqCst ,
59 Ordering : : SeqCst
60 ) ;
61
62   let success2 = ( old_val_2 = = 100 ) ;
63
64   t . ok ( success2 = = false , "CAS failure reported false ( old value did not match ) " ) ;
65   t . ok ( old_val_2 = = 200 , "CAS failure returns current actual value ( 200 ) " ) ;
66   t . ok ( std : : atomic : : load ( & val , Ordering : : SeqCst ) = = 200 , "CAS failure did not update memory" ) ;
67
68   return true ;
69 }
70
71 / * *
72   * Test 3 : Concurrent Increment
73   * /
74 #test fn test_atomic_concurrency(t: &Tap) -> bool {
75   var counter : Atomic ( i32 ) = Atomic ( 0 ) ;
76   let iterations = 50 ;
77
78   // Define the driver as a named async function to satisfy the compiler
79   async fn driver ( ) - > void {
80
81     // Define the worker logic
82     async fn worker ( ) - > void {
83       var i = 0 ;
84       while ( i < iterations ) {
85 // Use Relaxed for simple counters where order relative to other vars doesn't matter
86         std : : atomic : : fetch_add ( & counter , 1 , Ordering : : Relaxed ) ;
87         await sleep ( 1ms ) ;
88         i + = 1 ;
89       }
90     }
91
92     // Spawn two workers
93     let t1 = worker ( ) ;
94     let t2 = worker ( ) ;
95
96     // Await them
97     await t1 ;
98     await t2 ;
99   }
100
101   // Pass the invoked function future to block_on
102   std : : concurrency : : block_on ( driver ( ) ) ;
103
104   let final_count = std : : atomic : : load ( & counter , Ordering : : SeqCst ) ;
105   let expected = iterations * 2 ;
106
107   t . ok ( final_count = = expected , `Concurrent add expected ${expected}, got ${final_count}` ) ;
108   return true ;
109 }
110
111 #test fn main () -> i32 { // always the test main
112   let tests : Test [ ] = [
113     test_atomic_basic ,
114     test_atomic_cas ,
115     test_atomic_concurrency ,
116   ] ;
117
118   let t : Tap = { } ;
119   t . comment ( "# Testing Atomics\n");
120   t . plan ( 9 ) ;
121   t . run ( tests ) ;
122
123 if ( t . failed > 0 ) {
124 return 1 ; // Standard "Generic Error" code
125 }
126   return 0 ;
127 }

Concurrency & Parallelism

Aela's model is built on two orthogonal keywords, `async` and `task` , 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 Concepts

It is crucial to understand that async and task are separate modifiers with distinct physical meanings, even though they are designed to feel cohesive.

Keyword Concept Execution Model Primary Use Case
`async` Concurrency Cooperative (Lazy). Runs on the current thread/loop. Pauses at await . I/O-bound operations (Network, Disk, Timers).
`task` Parallelism Preemptive (Hot). Spawns onto a thread pool . Runs in background immediately. CPU-bound operations (Encryption, Data Processing).

async : The Pausable Function

An async function is a state machine. Calling it does not start execution; it returns a Future (a "cold" task). The code only runs when you explicitly await it or pass it to a poller like std::concurrency::select .

task : The Parallel Job

A task function is a unit of parallel work. Calling it immediately submits the work to the runtime's thread pool and returns a Task handle (a "hot" task). You continue executing while the task runs in the background.

Function Modifiers

You can combine these keywords to define exactly how a function behaves.

Declaration Behavior
fn foo() Synchronous. Blocks the caller until finished.
async fn foo() Asynchronous. Returns a Future . Runs on the caller's loop when awaited.
task fn foo() Parallel. Returns a Task . Runs on a thread pool immediately. Cannot await inside (unless also async).

Parallelism: task

While async handles waiting (IO), task handles doing (CPU). Aela uses a Work-Stealing Scheduler to distribute CPU-intensive work across a pool of OS threads.

task fn : Hot Execution

A function marked with task is distinct from a normal function or an async function.

Eager Submission: When you call a `task fn`, it is immediately pushed to the global scheduler queue. You do not need to `await` it to start it. The `Task(T)` Handle: It returns a handle that tracks the running job. If you drop the handle, the task continues running (detached). Isolation: * task functions cannot capture references to the surrounding stack unless those references are Send and Sync . They are effectively "spawned" into a new root scope.

Example
0 // Defined as a parallel job
1 task fn render_frame ( data : Scene ) - > Frame {
2 // This runs on a separate thread
3 return heavy_computation ( data ) ;
4 }
5
6 async fn main ( ) {
7 // Starts running IMMEDIATELY on a worker thread.
8 // Returns a handle, does not block 'main'.
9 let handle = render_frame ( my_scene ) ;
10
11 io : : print ( "Rendering started . . . " ) ;
12
13 // Wait for the result here.
14 let frame = await handle ;
15 }

task { ... } : Structured Parallelism

The task block is a Fork-Join Barrier . It is designed to safely parallelize work within a single function scope.

The Barrier: The block does not finish until every task spawned inside it has completed. Stack Safety: Because the block guarantees that all inner tasks finish before the function continues, inner tasks can safely borrow variables from the parent function . This is impossible with standard "spawn" functions in other languages (which usually require copying/moving data). Work Stealing: * The tasks inside the block are added to the local worker's queue. Other idle threads will "steal" these tasks to help complete the block faster.

Example
0 async fn process_user_request ( uid : i32 ) - > Response {
1 var user : User ;
2 var history : [ Order ] ;
3
4 // The function PAUSES here.
5 // The runtime splits execution into 2 parallel branches.
6 task {
7 // Branch 1: Fetch user (IO + Deserialization)
8 user = await db . fetch_user ( uid ) ;
9
10 // Branch 2: Fetch history
11 history = await db . fetch_history ( uid ) ;
12 }
13 // The block waits at the end.
14 // The function RESUMES here only after both are done.
15
16 return Response ( user , history ) ;
17 }

Pool Control & Oversubscription

The runtime manages the complexity of OS threads so you don't have to.

  1. Oversubscription: The runtime defaults to one thread per CPU core ( std::task::available_parallelism() ). It is designed to be "oversubscribed" safely. You can spawn thousands of task functions; the runtime will queue them and execute them as fast as possible on the fixed number of worker threads.
  2. Backpressure: The scheduler uses a bounded global queue (default capacity: 256 tasks). If you spawn tasks faster than the CPU can process them, calling a task fn will eventually transition from non-blocking to blocking (waiting for a slot in the queue).
  3. Environment Control: For deployment tuning, you can override the thread pool size via environment variable: AELA_TASK_COUNT=8 .

Concurrency: async

Sometimes you need a small unit of async work without defining a whole function, or you want to start async work from an sync function.

async fn : A reusable pausable function.

i32 { var v = 39; await sleep(128ms); v += 1; await sleep(128ms); v += 1; await sleep(128ms); v += 1; return v; } fn main () -> i32 { var x: i32; async fn foo () -> void { x = 22; } std::concurrency::block_on(foo()); io::println("x=\(x)"); return 0; }" lang="ae" title="Example" id="7c8f9c7e6fc9d">
Example
0 import { sleep } from "time" ;
1 import io from "io" ;
2
3 async fn bar ( ) - > i32 {
4 var v = 39 ;
5 await sleep ( 128ms ) ;
6 v + = 1 ;
7 await sleep ( 128ms ) ;
8 v + = 1 ;
9 await sleep ( 128ms ) ;
10 v + = 1 ;
11 return v ;
12 }
13
14 fn main ( ) - > i32 {
15 var x : i32 ;
16
17 async fn foo ( ) - > void {
18 x = 22 ;
19 }
20
21 std : : concurrency : : block_on ( foo ( ) ) ;
22 io : : println ( "x = \ ( x ) " ) ;
23 return 0 ;
24 }

async { ... } : An anonymous Future .

It captures variables from the surrounding scope. Like async fn , it is lazy and it does not run until awaited. All tasks must be completed and handled before before the program will continue.

Example
0 fn main ( ) - > i32 {
1 var x : i32 ;
2
3 async {
4 x = await bar ( ) ;
5 }
6
7 if ( x ! = 42 ) {
8 return 1 ;
9 }
10
11 io : : println ( "x = \ ( x ) " ) ;
12 return 0 ;
13 }

Control Flow

`await` : Pauses the current function, yielding control back to the runtime until the awaited operation completes. `std::concurrency::block_on` : The bridge between synchronous and asynchronous code. It starts a temporary event loop to drive a future to completion. This is typically used only in main or unit tests. `std::concurrency::select` : Races multiple futures. It waits for the first * one to complete and cancels the others.

Example
0 let job = await std : : concurrency : : select ( [
1 // Case 1: We receive a message
2 channel . recv ( ) ,
3
4 // Case 2: We get tired of waiting (Timeout)
5 timer . after ( 500ms )
6 ] ) ;

Safety

Aela treats the scheduler as a "Safety System" rather than an aftermarket part. Because the runtime is integrated into the language, the compiler can provide stronger guarantees than if it took a library-based approach.

  1. Compile-Time Data-Race Prevention: The compiler knows the difference between a local async execution (single-threaded) and a task execution (multi-threaded). It enforces Send and Sync rules strictly at the task boundary.
  2. Single Blocking Bridge: A built-in runtime provides one, official way to handle blocking calls: std::task::run_blocking() . This prevents the "Color of your function" problem from causing deadlocks when libraries mix blocking/non-blocking strategies.
  3. Guaranteed Cleanup: task handles are tied to the runtime. If a Task handle is dropped, the language enforces a consistent policy (detachment), preventing undefined behavior or zombie threads common in ad-hoc library implementations.

Formal Verification

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.

  • #let (the ghost of...)
  • #requires (pre condition)
  • #ensures (post condition)
  • #invariant (can’t change)
  • #variant (must change)

All of them appear before the function/loop they describe. None of these exist at runtime. They’re for the verifier only.

#let — The ghost of...

#let defines a ghost name for an expression, purely for specs.

You can put it right before a function or loop to bind names used in the following specs:

Example
0 #let oldN = n;
1 #requires oldN >= 0;
2 #ensures result == oldN + 1;
3 fn addOne ( n : i32 ) - > result : i32 {
4 return n + 1 ;
5 }

Here:

  • oldN is only visible to the verifier.
  • oldN does not exist in the compiled code.
  • You can use oldN in #requires , #ensures , #invariant , #variant , etc.

Think of #let as: "Give me a ghost name for this value/expression so I can talk about it in my conditions."

#requires — Precondition

#requires means this must be true when we enter this function (or loop). Placed directly above the function:

Example
0 #requires n >= 0;
1 fn factorial ( n : i32 ) - > i32 {
2 // ...
3 }

The verifier checks every call to factorial proves n >= 0 and assumes n >= 0 inside the body. You can also (optionally) apply #requires to loops, if you want to state assumptions at loop entry:

Example
0 #requires n >= 0; // Here `#requires` is about the state at loop entry.
1 #invariant 0 <= i <= n;
2 #variant n - i;
3 while ( i < n ) {
4 i = i + 1 ;
5 }

#ensures — Postcondition

#ensures goes right before the function and says: "This will be true after this function returns."

Example:

Example
0 #requires n >= 0;
1 #ensures result >= n;
2 fn addOne ( n : i32 ) - > i32 {
3 return n + 1 ;
4 }

The verifier proves we assume n >= 0 on entry and the value returned by the function ( result ) always satisfies result >= n .

Summation

Example
0 pure fn sum_range ( a : i32 [ ] , start : i32 , end : i32 ) - > result : i32 {
1 var acc = 0 ;
2 var i = start ;
3 while ( i < end ) {
4 acc = acc + a [ i ] ;
5 i = i + 1 ;
6 }
7 result = acc ;
8 return ;
9 }
10
11 #requires for (let i in 0..std::length(a)) {
12 std : : assert ( a [ i ] > = 0 ) ;
13 }
14 #ensures result == sum_range(a, 0, std::length(a));
15 fn sum ( a : i32 [ ] ) - > result : i32 {
16
17 #let originalLength = std::length(a);
18 #let originalArray = a;
19
20 var total = 0 ;
21 var i = 0 ;
22
23 #invariant 0 <= i && i <= originalLength;
24 #invariant total == sum_range(originalArray, 0, i);
25 #variant originalLength - i;
26 while ( i < originalLength ) {
27 total = total + a [ i ] ;
28 i = i + 1 ;
29 }
30
31 result = total ;
32 return ;
33 }

#invariant

An invariant can’t change (must stay true during loop). It goes immediately before a loop:

Example
0 #invariant 0 <= i && i <= n;
1 while ( i < n ) {
2 i = i + 1 ;
3 }

This means:

  • Before the first iteration: 0 <= i <= n must hold.
  • After every iteration, if we go around again, it must still hold.
  • When the loop exits, 0 <= i <= n is still true.

The invariant describes what must not be broken across iterations. It’s not "the variable can’t change"; it’s this relationship must stay true even as variables change.

#variant

#variant also goes right before the loop , together with #invariant :

Example
0 #invariant 0 <= i && i <= n;
1 #variant n - i;
2 while i < n {
3 i = i + 1 ;
4 }

Here:

  • n - i is the variant expression .
  • The verifier proves:
  • n - i is always ≥ 0 inside the loop.
  • On every iteration that continues the loop, n - i gets strictly smaller .

This proves the loop must terminate .

So:

  • #invariant - this condition must keep holding.
  • #variant - this measure must go down when we repeat.

Your loop may increase i , but your #variant is something that decreases (like n - i ).

Examples

General rule: Directives attach to the next construct.

For functions

Example
0 #let oldN = n;
1 #requires oldN >= 0;
2 #ensures result == oldN + 1;
3 fn addOne ( n : i32 ) - > result : i32 {
4 return n + 1 ;
5 }

For loops

Example
0 #invariant 0 <= i && i <= n;
1 #variant n - i;
2 while ( i < n ) {
3 i = i + 1 ;
4 }

You can stack them:

Example
0 #let start = i;
1 #requires 0 <= i && i <= n;
2 #invariant 0 <= i && i <= n;
3 #variant n - i;
4 while ( i < n ) {
5 i = i + 1 ;
6 }

All of this is compile-time-only verification syntax , erased in the compiled program.

Cheat Sheet

  • #let name = expr - Ghost name for expr , only for use in specs.
  • #requires P - P must be true before we enter this function/loop.
  • #ensures Q - Q will be true when the function returns.
  • #invariant I - I must hold before the loop, after each iteration, and when it ends.
  • #variant V - V must strictly decrease each time we repeat the loop (and stay in a well-founded set).

All placed before the thing they describe.

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

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.
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 ) {
1 first : T ,
2 second : U
3 }
4
5 let p : Pair ( i32 , string ) = {
6 first : 1 ,
7 second : "hi"
8 } ;
9
10 let x : Option ( i32 ) = Option : : Some ( 3 ) ;

Reference Types

&T is a borrowed reference to T . In Aela, the mut keyword isn't part of a type; it's a marker on a parameter or call argument that grants temporary, mutable access (a "mutable loan") to a value.

  • 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.
On Parameter Definition (fn):
0 fn foo ( mut param : & Type ) - > void {
1 }

This declares: "This function requires a mutable loan for param. I intend to modify the original value that the caller passes."

On Call Argument (call):
0 foo ( mut my_value ) ;

This declares: "I am granting a mutable loan to foo for this function call. I am aware that foo may modify it."

This design makes it explicit at both the function's definition site and the call site exactly where a value is allowed to be changed, aligning with the "in-out parameter" model many developers are familiar with.

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="1868225b33b17">
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

Boolean Condition
0 if ( )
1 [ else ]
Pattern-Match Binding (if-let)
0 if let =
1 [ else ]

Description

Standard conditional branching. if statements have two primary forms:

Boolean Condition: The standard form evaluates a which must result in a bool. If true, the then_statement (usually a block) is executed. If false, the optional else_statement is executed.

Boolean Condition
0 let x : i32 = 10 ;
1
2 if ( x > 0 ) {
3 print ( "Positive" ) ;
4 } else {
5 print ( "Non - positive" ) ;
6 }

Pattern-Match Binding (if-let): This form attempts to match the result of against the given .

If the match is successful, any variables bound in the are introduced, and the then_block is executed. These new variables are only in scope inside this block.

If the match is unsuccessful, the else statement is executed.

Pattern-Match Binding (if-let)
0 let v : Option ( i32 ) = Some ( 42 ) ;
1
2 if let Option : : Some ( value ) = v {
3 // 'value' is bound and only in scope here
4 print ( "Got value : { } " , value ) ;
5 } else {
6 // 'value' is not in scope here
7 print ( "Got None" ) ;
8 }

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 : i32 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

Aela supports two distinct forms of `match` :

  1. Statement `match` — used for control flow and side effects
  2. Expression `match` — used to compute a value

Which form you get is determined syntactically , not by context.

4.1 Statement match

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

A statement `match` is used for control flow. Each arm executes a block of statements and does not produce a value.

Block arms `{ ... }` are allowed return , break , and side effects are allowed * The match itself has no value

This form is typically used for branching logic, validation, logging, or early returns.

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

4.2 Expression match

Example
0 let | var : [ type ] = match ( ) {
1 = > ,
2 . . .
3 = >
4 } ;

An expression `match` computes a value.

  • Each arm must be a single expression
  • Block bodies `{ ... }` are not allowed
  • The match must be exhaustive
  • All arm expressions must unify to a single type

This restriction avoids implicit returns, ambiguous control flow, and complex typing rules.

"one", _ => "other" };" lang="aela" title="Example" id="e30ea1c5c1206">
Example
0 let label : string = match ( value ) {
1 0 = > "zero" ,
2 1 = > "one" ,
3 _ = > "other"
4 } ;

Note

Block arms are not allowed in expression-matches!

1 };" lang="aela" title="Example" id="b51c642e06fd1">
Example
0 let x = match ( value ) {
1 0 = > { print ( "zero" ) ; 0 } , // ERROR
2 _ = > 1
3 } ;

Instead, move side effects outside the expression match.

{} }" lang="aela" title="Example" id="d2a67f0969238">
Example
0 match ( value ) {
1 0 = > print ( "zero" ) ,
2 _ = > { }
3 }

In Aela, blocks are statements , not expressions. A block does not produce a value unless explicitly used as part of a language construct that allows expressions. The reason for this is, we optimize for explicitness, predictability, and bounded complexity. It separates control flow from value computation .

  • Statement `match` - control flow, blocks, side effects
  • Expression `match` - values only, expressions only

NOTE

No implicit block return or tail-values. No semicolon footguns.

Example
0 x // yields
1 x ; // discards

This creates a well-known class of bugs:

  • missing semicolon changes semantics
  • especially dangerous for JS/C-family programmers
  • hard to spot in reviews

Aela eliminates an entire class of bugs here:

  • blocks never yield values
  • expressions yield values
  • the grammar enforces the distinction

Instead of a lint rule its a language guarantee. You don't need a never type. And it keeps things easier to learn.

4.3 Struct Literals in Expression Matches

Brace syntax { ... } is still allowed in expression matches when it is a struct literal, not a block.

Example
0 let p = match ( kind ) {
1 A = > { x : 1 , y : 2 } ,
2 B = > { x : 3 , y : 4 }
3 } ;

If a brace body does not form a valid struct literal, it is rejected with an error.

4.4 Summary

Feature Statement match Expression match
Produces a value NO OK
Allows { ... } blocks OK NO
Allows return OK NO
Used for Control flow Value computation

5. return

Aela requires explicit `return` statements for all function exits. Functions do not implicitly return the value of their final expression. This is an intentional design choice that prioritizes clarity and predictability over implicit behavior.

Syntax

Example
0 return [ ] ;

Examples

Example
0 return ;
1 return x + 1 ;

Description

Exits a function immediately with the return value perscribed by the function signature.

Function boundaries are explicit

Requiring return makes it immediately obvious where a function exits and what value is returned. This is especially important in functions with multiple branches or early exits.

Example
0 fn add ( x : i32 , y : i32 ) - > i32 {
1 return x + y ;
2 }

There is no ambiguity about what value leaves the function.

Avoids semicolon-sensitive behavior

In expression-oriented languages, a missing semicolon can silently change behavior: Aela avoids this entire class of bugs by never treating the final expression of a function as an implicit return.

Example
0 x // yields
1 x ; // discards

Prevents accidental returns

Without implicit returns, writing an expression at the end of a function body does not change control flow: This makes accidental returns impossible and forces intent to be explicit.

Example
0 fn foo ( x : i32 ) - > i32 {
1 x + 42 ; // evaluated, but not returned
2 return x ;
3 }

4. Keeps return semantics simple and consistent

In Aela, return always means "Exit the current function immediately!" It's never overloaded to mean...

  • return from a block
  • yield from a match arm
  • produce the value of an expression

This simplicity avoids subtle interactions with pattern matching, block structure, and type inference.

Summary

Design Choice Result
Explicit return Clear function exits
No implicit function returns No semicolon footguns
return never yields values Simple control-flow reasoning
Match expressions stay value-only No hidden complexity

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.

NOTE

Blocks never yield implicit values!

Example

Example
0 {
1 let x : i32 = 1 ;
2 let y : i32 = 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="ead86df0f5d3d">
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 }

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 ( ) ;

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

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 [ AEA0012 ] : 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 [ public ] fn ( self : & Self , . . . ) - > { . . . }
6 }

Details

  • Methods are private by default. To allow a method or constructor to be called from another module, it must be marked with the public keyword.
  • The fn 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 public 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() -> i32 { 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="cf14d0c6cb7c9">
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 ( ) - > i32 {
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 : i32 ;
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 ( (i32) 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 (integer * 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 + integer
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: i32, elem_size: i32) -> 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: i32, 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: i32) -> 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)) -> i32
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: i32, len: i32) -> &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: i32, len: i32) -> 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 : i32 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.

Atomics

Introduction

Atomic(T) is a fundamental intrinsic type designed for high-performance concurrency. It provides a wrapper around primitive types that guarantees safe access across multiple threads.

Unlike standard variables, operations on Atomic(T) are indivisible. However, atomicity alone is insufficient for correctness; memory ordering is equally critical. This API exposes fine-grained control over how the CPU and compiler are allowed to reorder memory operations, enabling the construction of lock-free data structures and synchronization primitives.

Core Concepts

Atomicity & Type Constraints

An operation is atomic if it is indivisible: a thread observes either the old value or the new value, never a "torn" or intermediate state.

Allowed Types : `T` must be a primitive integer, boolean, pointer, or an `enum` with a fixed underlying integer representation (e.g., `repr(u8)`) where all bit patterns are valid. Alignment : Atomic(T) requires natural alignment for T . Static Check : If alignment is statically known to be insufficient, it is a compile-time error . Dynamic Check : If a pointer is misaligned at runtime, the behavior is a guaranteed trap (panic).

Lock-Free Guarantees

While Atomic(T) enables lock-free algorithms, the operations themselves are not guaranteed to be lock-free on all platforms for all types.

Hardware Support : If the target CPU supports atomic instructions for `sizeof(T)`, operations are compiled to those instructions. Software Fallback : If the hardware lacks support (e.g., 64-bit atomics on 32-bit arch), the runtime may use a hidden global lock / hashed lock pool. Intrinsic Check *: Use std::atomic::is_lock_free() -> bool to query if operations on T are truly lock-free on the current target.

Memory Ordering

The Ordering enum controls how operations synchronize with other threads.

  1. `Relaxed` : No synchronization. Only atomicity is ensured.
  2. `Acquire` : Valid for loads. It ensures that no subsequent memory accesses can be reordered before this operation.
  3. `Release` : Valid for stores. It ensures that no previous memory accesses can be reordered after this operation.
  4. `AcqRel` : Valid for RMW (Read-Modify-Write). Combines Acquire and Release.
  5. `SeqCst` : Strongest ordering. Enforces a total order on all SeqCst operations consistent with program order.

The Synchronization Contract

The primary mechanism for thread coordination is the Acquire-Release pair :

An Acquire operation on an atomic object synchronizes-with a Release operation on the same object if the Acquire reads the value written by that Release (or a value from a release sequence headed by that Release ).

Effect : All memory writes that happened before the Release are guaranteed to be visible to the thread that performed the Acquire .

The Intrinsic API

std::atomic::load

Signature load(ptr: &Atomic(T), order: Ordering) -> T
Constraints order must be Relaxed , Acquire , or SeqCst .
Description Atomically reads the value.

std::atomic::store

Signature store(ptr: &Atomic(T), val: T, order: Ordering)
Constraints order must be Relaxed , Release , or SeqCst .
Description Atomically writes a value.

std::atomic::exchange

Signature exchange(ptr: &Atomic(T), val: T, order: Ordering) -> T
Constraints Any Ordering is valid.
Description Atomically writes val and returns the previous value (the value immediately before the swap).

std::atomic::compare_exchange

Signature compare_exchange(ptr: &Atomic(T), expected: T, desired: T, success: Ordering, failure: Ordering) -> (bool, T)

| Constraints | 1. failure cannot be Release or AcqRel (failure is a load).

  1. failure cannot be stronger than success (e.g., if success is Relaxed , failure cannot be SeqCst ). |
  2. | Description | Atomically checks if *ptr == expected .

Success : Writes desired using success order. Returns (true, old_val) .

Failure : Loads current value using failure order. Returns (false, current_val) .

Note : old_val and current_val represent the value at ptr immediately before the operation.* |

std::atomic::fetch_add / sub / and / or / xor

Signature fetch_op(ptr: &Atomic(T), val: T, order: Ordering) -> T
Constraints Any Ordering is valid.
Description Atomically performs the arithmetic/bitwise operation and returns the previous value.

Usage & Safety

i32 { io::println("Hello World"); // ...normal file code... return 0; } /**  * Test 1: Basic Load and Store  */ #test fn test_atomic_basic(t: &Tap) -> bool {   var val: Atomic(i32) = Atomic(10);   // Test Initial load (Using SeqCst for strongest safety)   t.ok(std::atomic::load(&val, Ordering::SeqCst) == 10, "Atomic initializes correctly");   // Test Store   std::atomic::store(&val, 42, Ordering::SeqCst);   let result = std::atomic::load(&val, Ordering::SeqCst);   t.ok(result == 42, "Atomic store/load persisted value");   return true; } /**  * Test 2: Compare and Exchange (CAS)  */ #test fn test_atomic_cas(t: &Tap) -> bool {   var val: Atomic(i32) = Atomic(100);   // 1. Successful Swap   // Expected: 100, New: 200. Current is 100. -> Should succeed.   let old_val_1 = std::atomic::compare_exchange( &val, 100, 200, Ordering::SeqCst, // Success order Ordering::SeqCst // Failure order );   // Check if the returned value matches our expectation   let success1 = (old_val_1 == 100);   t.ok(success1 == true, "CAS success reported true (old value matched expected)");   t.ok(old_val_1 == 100, "CAS returned original value");   t.ok(std::atomic::load(&val, Ordering::SeqCst) == 200, "CAS success updated memory to 200");   // 2. Failed Swap (Stale read)   // Expected: 100 (stale), New: 300. Current is 200. -> Should fail.   let old_val_2 = std::atomic::compare_exchange( &val, 100, 300, Ordering::SeqCst, Ordering::SeqCst );   let success2 = (old_val_2 == 100);   t.ok(success2 == false, "CAS failure reported false (old value did not match)");   t.ok(old_val_2 == 200, "CAS failure returns current actual value (200)");   t.ok(std::atomic::load(&val, Ordering::SeqCst) == 200, "CAS failure did not update memory");   return true; } /**  * Test 3: Concurrent Increment  */ #test fn test_atomic_concurrency(t: &Tap) -> bool {   var counter: Atomic(i32) = Atomic(0);   let iterations = 50;   // Define the driver as a named async function to satisfy the compiler   async fn driver() -> void {     // Define the worker logic     async fn worker() -> void {       var i = 0;       while (i < iterations) { // Use Relaxed for simple counters where order relative to other vars doesn't matter         std::atomic::fetch_add(&counter, 1, Ordering::Relaxed);         await sleep(1ms);         i += 1;       }     }     // Spawn two workers     let t1 = worker();     let t2 = worker();     // Await them     await t1;     await t2;   }   // Pass the invoked function future to block_on   std::concurrency::block_on(driver());   let final_count = std::atomic::load(&counter, Ordering::SeqCst);   let expected = iterations * 2;   t.ok(final_count == expected, `Concurrent add expected ${expected}, got ${final_count}`);   return true; } #test fn main () -> i32 { // always the test main   let tests: Test[] = [     test_atomic_basic,     test_atomic_cas,     test_atomic_concurrency,   ];   let t: Tap = {};   t.comment("# Testing Atomics\n");   t.plan(9);   t.run(tests); if (t.failed > 0) { return 1; // Standard "Generic Error" code }   return 0; }" lang="ae" title="Example" id="b4f85316c768b">
Example
0 #test import { Test, Tap } from "test";
1 #test import { sleep } from "time";
2 #test import { Ordering } from "sync";
3
4 import io from "io" ;
5
6 fn main ( ) - > i32 {
7 io : : println ( "Hello World" ) ;
8 // ...normal file code...
9 return 0 ;
10 }
11
12 / * *
13   * Test 1 : Basic Load and Store
14   * /
15 #test fn test_atomic_basic(t: &Tap) -> bool {
16   var val : Atomic ( i32 ) = Atomic ( 10 ) ;
17
18   // Test Initial load (Using SeqCst for strongest safety)
19   t . ok ( std : : atomic : : load ( & val , Ordering : : SeqCst ) = = 10 , "Atomic initializes correctly" ) ;
20
21   // Test Store
22   std : : atomic : : store ( & val , 42 , Ordering : : SeqCst ) ;
23   let result = std : : atomic : : load ( & val , Ordering : : SeqCst ) ;
24
25   t . ok ( result = = 42 , "Atomic store / load persisted value" ) ;
26   return true ;
27 }
28
29 / * *
30   * Test 2 : Compare and Exchange ( CAS )
31   * /
32 #test fn test_atomic_cas(t: &Tap) -> bool {
33   var val : Atomic ( i32 ) = Atomic ( 100 ) ;
34
35   // 1. Successful Swap
36   // Expected: 100, New: 200. Current is 100. -> Should succeed.
37   let old_val_1 = std : : atomic : : compare_exchange (
38 & val ,
39 100 ,
40 200 ,
41 Ordering : : SeqCst , // Success order
42 Ordering : : SeqCst // Failure order
43 ) ;
44
45   // Check if the returned value matches our expectation
46   let success1 = ( old_val_1 = = 100 ) ;
47
48   t . ok ( success1 = = true , "CAS success reported true ( old value matched expected ) " ) ;
49   t . ok ( old_val_1 = = 100 , "CAS returned original value" ) ;
50   t . ok ( std : : atomic : : load ( & val , Ordering : : SeqCst ) = = 200 , "CAS success updated memory to 200" ) ;
51
52   // 2. Failed Swap (Stale read)
53   // Expected: 100 (stale), New: 300. Current is 200. -> Should fail.
54   let old_val_2 = std : : atomic : : compare_exchange (
55 & val ,
56 100 ,
57 300 ,
58 Ordering : : SeqCst ,
59 Ordering : : SeqCst
60 ) ;
61
62   let success2 = ( old_val_2 = = 100 ) ;
63
64   t . ok ( success2 = = false , "CAS failure reported false ( old value did not match ) " ) ;
65   t . ok ( old_val_2 = = 200 , "CAS failure returns current actual value ( 200 ) " ) ;
66   t . ok ( std : : atomic : : load ( & val , Ordering : : SeqCst ) = = 200 , "CAS failure did not update memory" ) ;
67
68   return true ;
69 }
70
71 / * *
72   * Test 3 : Concurrent Increment
73   * /
74 #test fn test_atomic_concurrency(t: &Tap) -> bool {
75   var counter : Atomic ( i32 ) = Atomic ( 0 ) ;
76   let iterations = 50 ;
77
78   // Define the driver as a named async function to satisfy the compiler
79   async fn driver ( ) - > void {
80
81     // Define the worker logic
82     async fn worker ( ) - > void {
83       var i = 0 ;
84       while ( i < iterations ) {
85 // Use Relaxed for simple counters where order relative to other vars doesn't matter
86         std : : atomic : : fetch_add ( & counter , 1 , Ordering : : Relaxed ) ;
87         await sleep ( 1ms ) ;
88         i + = 1 ;
89       }
90     }
91
92     // Spawn two workers
93     let t1 = worker ( ) ;
94     let t2 = worker ( ) ;
95
96     // Await them
97     await t1 ;
98     await t2 ;
99   }
100
101   // Pass the invoked function future to block_on
102   std : : concurrency : : block_on ( driver ( ) ) ;
103
104   let final_count = std : : atomic : : load ( & counter , Ordering : : SeqCst ) ;
105   let expected = iterations * 2 ;
106
107   t . ok ( final_count = = expected , `Concurrent add expected ${expected}, got ${final_count}` ) ;
108   return true ;
109 }
110
111 #test fn main () -> i32 { // always the test main
112   let tests : Test [ ] = [
113     test_atomic_basic ,
114     test_atomic_cas ,
115     test_atomic_concurrency ,
116   ] ;
117
118   let t : Tap = { } ;
119   t . comment ( "# Testing Atomics\n");
120   t . plan ( 9 ) ;
121   t . run ( tests ) ;
122
123 if ( t . failed > 0 ) {
124 return 1 ; // Standard "Generic Error" code
125 }
126   return 0 ;
127 }

Concurrency & Parallelism

Aela's model is built on two orthogonal keywords, `async` and `task` , 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 Concepts

It is crucial to understand that async and task are separate modifiers with distinct physical meanings, even though they are designed to feel cohesive.

Keyword Concept Execution Model Primary Use Case
`async` Concurrency Cooperative (Lazy). Runs on the current thread/loop. Pauses at await . I/O-bound operations (Network, Disk, Timers).
`task` Parallelism Preemptive (Hot). Spawns onto a thread pool . Runs in background immediately. CPU-bound operations (Encryption, Data Processing).

async : The Pausable Function

An async function is a state machine. Calling it does not start execution; it returns a Future (a "cold" task). The code only runs when you explicitly await it or pass it to a poller like std::concurrency::select .

task : The Parallel Job

A task function is a unit of parallel work. Calling it immediately submits the work to the runtime's thread pool and returns a Task handle (a "hot" task). You continue executing while the task runs in the background.

Function Modifiers

You can combine these keywords to define exactly how a function behaves.

Declaration Behavior
fn foo() Synchronous. Blocks the caller until finished.
async fn foo() Asynchronous. Returns a Future . Runs on the caller's loop when awaited.
task fn foo() Parallel. Returns a Task . Runs on a thread pool immediately. Cannot await inside (unless also async).

Parallelism: task

While async handles waiting (IO), task handles doing (CPU). Aela uses a Work-Stealing Scheduler to distribute CPU-intensive work across a pool of OS threads.

task fn : Hot Execution

A function marked with task is distinct from a normal function or an async function.

Eager Submission: When you call a `task fn`, it is immediately pushed to the global scheduler queue. You do not need to `await` it to start it. The `Task(T)` Handle: It returns a handle that tracks the running job. If you drop the handle, the task continues running (detached). Isolation: * task functions cannot capture references to the surrounding stack unless those references are Send and Sync . They are effectively "spawned" into a new root scope.

Example
0 // Defined as a parallel job
1 task fn render_frame ( data : Scene ) - > Frame {
2 // This runs on a separate thread
3 return heavy_computation ( data ) ;
4 }
5
6 async fn main ( ) {
7 // Starts running IMMEDIATELY on a worker thread.
8 // Returns a handle, does not block 'main'.
9 let handle = render_frame ( my_scene ) ;
10
11 io : : print ( "Rendering started . . . " ) ;
12
13 // Wait for the result here.
14 let frame = await handle ;
15 }

task { ... } : Structured Parallelism

The task block is a Fork-Join Barrier . It is designed to safely parallelize work within a single function scope.

The Barrier: The block does not finish until every task spawned inside it has completed. Stack Safety: Because the block guarantees that all inner tasks finish before the function continues, inner tasks can safely borrow variables from the parent function . This is impossible with standard "spawn" functions in other languages (which usually require copying/moving data). Work Stealing: * The tasks inside the block are added to the local worker's queue. Other idle threads will "steal" these tasks to help complete the block faster.

Example
0 async fn process_user_request ( uid : i32 ) - > Response {
1 var user : User ;
2 var history : [ Order ] ;
3
4 // The function PAUSES here.
5 // The runtime splits execution into 2 parallel branches.
6 task {
7 // Branch 1: Fetch user (IO + Deserialization)
8 user = await db . fetch_user ( uid ) ;
9
10 // Branch 2: Fetch history
11 history = await db . fetch_history ( uid ) ;
12 }
13 // The block waits at the end.
14 // The function RESUMES here only after both are done.
15
16 return Response ( user , history ) ;
17 }

Pool Control & Oversubscription

The runtime manages the complexity of OS threads so you don't have to.

  1. Oversubscription: The runtime defaults to one thread per CPU core ( std::task::available_parallelism() ). It is designed to be "oversubscribed" safely. You can spawn thousands of task functions; the runtime will queue them and execute them as fast as possible on the fixed number of worker threads.
  2. Backpressure: The scheduler uses a bounded global queue (default capacity: 256 tasks). If you spawn tasks faster than the CPU can process them, calling a task fn will eventually transition from non-blocking to blocking (waiting for a slot in the queue).
  3. Environment Control: For deployment tuning, you can override the thread pool size via environment variable: AELA_TASK_COUNT=8 .

Concurrency: async

Sometimes you need a small unit of async work without defining a whole function, or you want to start async work from an sync function.

async fn : A reusable pausable function.

i32 { var v = 39; await sleep(128ms); v += 1; await sleep(128ms); v += 1; await sleep(128ms); v += 1; return v; } fn main () -> i32 { var x: i32; async fn foo () -> void { x = 22; } std::concurrency::block_on(foo()); io::println("x=\(x)"); return 0; }" lang="ae" title="Example" id="7c8f9c7e6fc9d">
Example
0 import { sleep } from "time" ;
1 import io from "io" ;
2
3 async fn bar ( ) - > i32 {
4 var v = 39 ;
5 await sleep ( 128ms ) ;
6 v + = 1 ;
7 await sleep ( 128ms ) ;
8 v + = 1 ;
9 await sleep ( 128ms ) ;
10 v + = 1 ;
11 return v ;
12 }
13
14 fn main ( ) - > i32 {
15 var x : i32 ;
16
17 async fn foo ( ) - > void {
18 x = 22 ;
19 }
20
21 std : : concurrency : : block_on ( foo ( ) ) ;
22 io : : println ( "x = \ ( x ) " ) ;
23 return 0 ;
24 }

async { ... } : An anonymous Future .

It captures variables from the surrounding scope. Like async fn , it is lazy and it does not run until awaited. All tasks must be completed and handled before before the program will continue.

Example
0 fn main ( ) - > i32 {
1 var x : i32 ;
2
3 async {
4 x = await bar ( ) ;
5 }
6
7 if ( x ! = 42 ) {
8 return 1 ;
9 }
10
11 io : : println ( "x = \ ( x ) " ) ;
12 return 0 ;
13 }

Control Flow

`await` : Pauses the current function, yielding control back to the runtime until the awaited operation completes. `std::concurrency::block_on` : The bridge between synchronous and asynchronous code. It starts a temporary event loop to drive a future to completion. This is typically used only in main or unit tests. `std::concurrency::select` : Races multiple futures. It waits for the first * one to complete and cancels the others.

Example
0 let job = await std : : concurrency : : select ( [
1 // Case 1: We receive a message
2 channel . recv ( ) ,
3
4 // Case 2: We get tired of waiting (Timeout)
5 timer . after ( 500ms )
6 ] ) ;

Safety

Aela treats the scheduler as a "Safety System" rather than an aftermarket part. Because the runtime is integrated into the language, the compiler can provide stronger guarantees than if it took a library-based approach.

  1. Compile-Time Data-Race Prevention: The compiler knows the difference between a local async execution (single-threaded) and a task execution (multi-threaded). It enforces Send and Sync rules strictly at the task boundary.
  2. Single Blocking Bridge: A built-in runtime provides one, official way to handle blocking calls: std::task::run_blocking() . This prevents the "Color of your function" problem from causing deadlocks when libraries mix blocking/non-blocking strategies.
  3. Guaranteed Cleanup: task handles are tied to the runtime. If a Task handle is dropped, the language enforces a consistent policy (detachment), preventing undefined behavior or zombie threads common in ad-hoc library implementations.

Formal Verification

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.

  • #let (the ghost of...)
  • #requires (pre condition)
  • #ensures (post condition)
  • #invariant (can’t change)
  • #variant (must change)

All of them appear before the function/loop they describe. None of these exist at runtime. They’re for the verifier only.

#let — The ghost of...

#let defines a ghost name for an expression, purely for specs.

You can put it right before a function or loop to bind names used in the following specs:

Example
0 #let oldN = n;
1 #requires oldN >= 0;
2 #ensures result == oldN + 1;
3 fn addOne ( n : i32 ) - > result : i32 {
4 return n + 1 ;
5 }

Here:

  • oldN is only visible to the verifier.
  • oldN does not exist in the compiled code.
  • You can use oldN in #requires , #ensures , #invariant , #variant , etc.

Think of #let as: "Give me a ghost name for this value/expression so I can talk about it in my conditions."

#requires — Precondition

#requires means this must be true when we enter this function (or loop). Placed directly above the function:

Example
0 #requires n >= 0;
1 fn factorial ( n : i32 ) - > i32 {
2 // ...
3 }

The verifier checks every call to factorial proves n >= 0 and assumes n >= 0 inside the body. You can also (optionally) apply #requires to loops, if you want to state assumptions at loop entry:

Example
0 #requires n >= 0; // Here `#requires` is about the state at loop entry.
1 #invariant 0 <= i <= n;
2 #variant n - i;
3 while ( i < n ) {
4 i = i + 1 ;
5 }

#ensures — Postcondition

#ensures goes right before the function and says: "This will be true after this function returns."

Example:

Example
0 #requires n >= 0;
1 #ensures result >= n;
2 fn addOne ( n : i32 ) - > i32 {
3 return n + 1 ;
4 }

The verifier proves we assume n >= 0 on entry and the value returned by the function ( result ) always satisfies result >= n .

Summation

Example
0 pure fn sum_range ( a : i32 [ ] , start : i32 , end : i32 ) - > result : i32 {
1 var acc = 0 ;
2 var i = start ;
3 while ( i < end ) {
4 acc = acc + a [ i ] ;
5 i = i + 1 ;
6 }
7 result = acc ;
8 return ;
9 }
10
11 #requires for (let i in 0..std::length(a)) {
12 std : : assert ( a [ i ] > = 0 ) ;
13 }
14 #ensures result == sum_range(a, 0, std::length(a));
15 fn sum ( a : i32 [ ] ) - > result : i32 {
16
17 #let originalLength = std::length(a);
18 #let originalArray = a;
19
20 var total = 0 ;
21 var i = 0 ;
22
23 #invariant 0 <= i && i <= originalLength;
24 #invariant total == sum_range(originalArray, 0, i);
25 #variant originalLength - i;
26 while ( i < originalLength ) {
27 total = total + a [ i ] ;
28 i = i + 1 ;
29 }
30
31 result = total ;
32 return ;
33 }

#invariant

An invariant can’t change (must stay true during loop). It goes immediately before a loop:

Example
0 #invariant 0 <= i && i <= n;
1 while ( i < n ) {
2 i = i + 1 ;
3 }

This means:

  • Before the first iteration: 0 <= i <= n must hold.
  • After every iteration, if we go around again, it must still hold.
  • When the loop exits, 0 <= i <= n is still true.

The invariant describes what must not be broken across iterations. It’s not "the variable can’t change"; it’s this relationship must stay true even as variables change.

#variant

#variant also goes right before the loop , together with #invariant :

Example
0 #invariant 0 <= i && i <= n;
1 #variant n - i;
2 while i < n {
3 i = i + 1 ;
4 }

Here:

  • n - i is the variant expression .
  • The verifier proves:
  • n - i is always ≥ 0 inside the loop.
  • On every iteration that continues the loop, n - i gets strictly smaller .

This proves the loop must terminate .

So:

  • #invariant - this condition must keep holding.
  • #variant - this measure must go down when we repeat.

Your loop may increase i , but your #variant is something that decreases (like n - i ).

Examples

General rule: Directives attach to the next construct.

For functions

Example
0 #let oldN = n;
1 #requires oldN >= 0;
2 #ensures result == oldN + 1;
3 fn addOne ( n : i32 ) - > result : i32 {
4 return n + 1 ;
5 }

For loops

Example
0 #invariant 0 <= i && i <= n;
1 #variant n - i;
2 while ( i < n ) {
3 i = i + 1 ;
4 }

You can stack them:

Example
0 #let start = i;
1 #requires 0 <= i && i <= n;
2 #invariant 0 <= i && i <= n;
3 #variant n - i;
4 while ( i < n ) {
5 i = i + 1 ;
6 }

All of this is compile-time-only verification syntax , erased in the compiled program.

Cheat Sheet

  • #let name = expr - Ghost name for expr , only for use in specs.
  • #requires P - P must be true before we enter this function/loop.
  • #ensures Q - Q will be true when the function returns.
  • #invariant I - I must hold before the loop, after each iteration, and when it ends.
  • #variant V - V must strictly decrease each time we repeat the loop (and stay in a well-founded set).

All placed before the thing they describe.

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

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.
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 ) {
1 first : T ,
2 second : U
3 }
4
5 let p : Pair ( i32 , string ) = {
6 first : 1 ,
7 second : "hi"
8 } ;
9
10 let x : Option ( i32 ) = Option : : Some ( 3 ) ;

Reference Types

&T is a borrowed reference to T . In Aela, the mut keyword isn't part of a type; it's a marker on a parameter or call argument that grants temporary, mutable access (a "mutable loan") to a value.

  • 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.
On Parameter Definition (fn):
0 fn foo ( mut param : & Type ) - > void {
1 }

This declares: "This function requires a mutable loan for param. I intend to modify the original value that the caller passes."

On Call Argument (call):
0 foo ( mut my_value ) ;

This declares: "I am granting a mutable loan to foo for this function call. I am aware that foo may modify it."

This design makes it explicit at both the function's definition site and the call site exactly where a value is allowed to be changed, aligning with the "in-out parameter" model many developers are familiar with.

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="1868225b33b17">
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

Boolean Condition
0 if ( )
1 [ else ]
Pattern-Match Binding (if-let)
0 if let =
1 [ else ]

Description

Standard conditional branching. if statements have two primary forms:

Boolean Condition: The standard form evaluates a which must result in a bool. If true, the then_statement (usually a block) is executed. If false, the optional else_statement is executed.

Boolean Condition
0 let x : i32 = 10 ;
1
2 if ( x > 0 ) {
3 print ( "Positive" ) ;
4 } else {
5 print ( "Non - positive" ) ;
6 }

Pattern-Match Binding (if-let): This form attempts to match the result of against the given .

If the match is successful, any variables bound in the are introduced, and the then_block is executed. These new variables are only in scope inside this block.

If the match is unsuccessful, the else statement is executed.

Pattern-Match Binding (if-let)
0 let v : Option ( i32 ) = Some ( 42 ) ;
1
2 if let Option : : Some ( value ) = v {
3 // 'value' is bound and only in scope here
4 print ( "Got value : { } " , value ) ;
5 } else {
6 // 'value' is not in scope here
7 print ( "Got None" ) ;
8 }

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 : i32 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

Aela supports two distinct forms of `match` :

  1. Statement `match` — used for control flow and side effects
  2. Expression `match` — used to compute a value

Which form you get is determined syntactically , not by context.

4.1 Statement match

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

A statement `match` is used for control flow. Each arm executes a block of statements and does not produce a value.

Block arms `{ ... }` are allowed return , break , and side effects are allowed * The match itself has no value

This form is typically used for branching logic, validation, logging, or early returns.

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

4.2 Expression match

Example
0 let | var : [ type ] = match ( ) {
1 = > ,
2 . . .
3 = >
4 } ;

An expression `match` computes a value.

  • Each arm must be a single expression
  • Block bodies `{ ... }` are not allowed
  • The match must be exhaustive
  • All arm expressions must unify to a single type

This restriction avoids implicit returns, ambiguous control flow, and complex typing rules.

"one", _ => "other" };" lang="aela" title="Example" id="e30ea1c5c1206">
Example
0 let label : string = match ( value ) {
1 0 = > "zero" ,
2 1 = > "one" ,
3 _ = > "other"
4 } ;

Note

Block arms are not allowed in expression-matches!

1 };" lang="aela" title="Example" id="b51c642e06fd1">
Example
0 let x = match ( value ) {
1 0 = > { print ( "zero" ) ; 0 } , // ERROR
2 _ = > 1
3 } ;

Instead, move side effects outside the expression match.

{} }" lang="aela" title="Example" id="d2a67f0969238">
Example
0 match ( value ) {
1 0 = > print ( "zero" ) ,
2 _ = > { }
3 }

In Aela, blocks are statements , not expressions. A block does not produce a value unless explicitly used as part of a language construct that allows expressions. The reason for this is, we optimize for explicitness, predictability, and bounded complexity. It separates control flow from value computation .

  • Statement `match` - control flow, blocks, side effects
  • Expression `match` - values only, expressions only

NOTE

No implicit block return or tail-values. No semicolon footguns.

Example
0 x // yields
1 x ; // discards

This creates a well-known class of bugs:

  • missing semicolon changes semantics
  • especially dangerous for JS/C-family programmers
  • hard to spot in reviews

Aela eliminates an entire class of bugs here:

  • blocks never yield values
  • expressions yield values
  • the grammar enforces the distinction

Instead of a lint rule its a language guarantee. You don't need a never type. And it keeps things easier to learn.

4.3 Struct Literals in Expression Matches

Brace syntax { ... } is still allowed in expression matches when it is a struct literal, not a block.

Example
0 let p = match ( kind ) {
1 A = > { x : 1 , y : 2 } ,
2 B = > { x : 3 , y : 4 }
3 } ;

If a brace body does not form a valid struct literal, it is rejected with an error.

4.4 Summary

Feature Statement match Expression match
Produces a value NO OK
Allows { ... } blocks OK NO
Allows return OK NO
Used for Control flow Value computation

5. return

Aela requires explicit `return` statements for all function exits. Functions do not implicitly return the value of their final expression. This is an intentional design choice that prioritizes clarity and predictability over implicit behavior.

Syntax

Example
0 return [ ] ;

Examples

Example
0 return ;
1 return x + 1 ;

Description

Exits a function immediately with the return value perscribed by the function signature.

Function boundaries are explicit

Requiring return makes it immediately obvious where a function exits and what value is returned. This is especially important in functions with multiple branches or early exits.

Example
0 fn add ( x : i32 , y : i32 ) - > i32 {
1 return x + y ;
2 }

There is no ambiguity about what value leaves the function.

Avoids semicolon-sensitive behavior

In expression-oriented languages, a missing semicolon can silently change behavior: Aela avoids this entire class of bugs by never treating the final expression of a function as an implicit return.

Example
0 x // yields
1 x ; // discards

Prevents accidental returns

Without implicit returns, writing an expression at the end of a function body does not change control flow: This makes accidental returns impossible and forces intent to be explicit.

Example
0 fn foo ( x : i32 ) - > i32 {
1 x + 42 ; // evaluated, but not returned
2 return x ;
3 }

4. Keeps return semantics simple and consistent

In Aela, return always means "Exit the current function immediately!" It's never overloaded to mean...

  • return from a block
  • yield from a match arm
  • produce the value of an expression

This simplicity avoids subtle interactions with pattern matching, block structure, and type inference.

Summary

Design Choice Result
Explicit return Clear function exits
No implicit function returns No semicolon footguns
return never yields values Simple control-flow reasoning
Match expressions stay value-only No hidden complexity

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.

NOTE

Blocks never yield implicit values!

Example

Example
0 {
1 let x : i32 = 1 ;
2 let y : i32 = 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="ead86df0f5d3d">
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 }

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 ( ) ;

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

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 [ AEA0012 ] : 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 [ public ] fn ( self : & Self , . . . ) - > { . . . }
6 }

Details

  • Methods are private by default. To allow a method or constructor to be called from another module, it must be marked with the public keyword.
  • The fn 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 public 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() -> i32 { 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="cf14d0c6cb7c9">
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 ( ) - > i32 {
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 : i32 ;
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 ( (i32) 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 (integer * 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 + integer
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: i32, elem_size: i32) -> 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: i32, 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: i32) -> 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)) -> i32
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: i32, len: i32) -> &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: i32, len: i32) -> 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 : i32 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.

Atomics

Introduction

Atomic(T) is a fundamental intrinsic type designed for high-performance concurrency. It provides a wrapper around primitive types that guarantees safe access across multiple threads.

Unlike standard variables, operations on Atomic(T) are indivisible. However, atomicity alone is insufficient for correctness; memory ordering is equally critical. This API exposes fine-grained control over how the CPU and compiler are allowed to reorder memory operations, enabling the construction of lock-free data structures and synchronization primitives.

Core Concepts

Atomicity & Type Constraints

An operation is atomic if it is indivisible: a thread observes either the old value or the new value, never a "torn" or intermediate state.

Allowed Types : `T` must be a primitive integer, boolean, pointer, or an `enum` with a fixed underlying integer representation (e.g., `repr(u8)`) where all bit patterns are valid. Alignment : Atomic(T) requires natural alignment for T . Static Check : If alignment is statically known to be insufficient, it is a compile-time error . Dynamic Check : If a pointer is misaligned at runtime, the behavior is a guaranteed trap (panic).

Lock-Free Guarantees

While Atomic(T) enables lock-free algorithms, the operations themselves are not guaranteed to be lock-free on all platforms for all types.

Hardware Support : If the target CPU supports atomic instructions for `sizeof(T)`, operations are compiled to those instructions. Software Fallback : If the hardware lacks support (e.g., 64-bit atomics on 32-bit arch), the runtime may use a hidden global lock / hashed lock pool. Intrinsic Check *: Use std::atomic::is_lock_free() -> bool to query if operations on T are truly lock-free on the current target.

Memory Ordering

The Ordering enum controls how operations synchronize with other threads.

  1. `Relaxed` : No synchronization. Only atomicity is ensured.
  2. `Acquire` : Valid for loads. It ensures that no subsequent memory accesses can be reordered before this operation.
  3. `Release` : Valid for stores. It ensures that no previous memory accesses can be reordered after this operation.
  4. `AcqRel` : Valid for RMW (Read-Modify-Write). Combines Acquire and Release.
  5. `SeqCst` : Strongest ordering. Enforces a total order on all SeqCst operations consistent with program order.

The Synchronization Contract

The primary mechanism for thread coordination is the Acquire-Release pair :

An Acquire operation on an atomic object synchronizes-with a Release operation on the same object if the Acquire reads the value written by that Release (or a value from a release sequence headed by that Release ).

Effect : All memory writes that happened before the Release are guaranteed to be visible to the thread that performed the Acquire .

The Intrinsic API

std::atomic::load

Signature load(ptr: &Atomic(T), order: Ordering) -> T
Constraints order must be Relaxed , Acquire , or SeqCst .
Description Atomically reads the value.

std::atomic::store

Signature store(ptr: &Atomic(T), val: T, order: Ordering)
Constraints order must be Relaxed , Release , or SeqCst .
Description Atomically writes a value.

std::atomic::exchange

Signature exchange(ptr: &Atomic(T), val: T, order: Ordering) -> T
Constraints Any Ordering is valid.
Description Atomically writes val and returns the previous value (the value immediately before the swap).

std::atomic::compare_exchange

Signature compare_exchange(ptr: &Atomic(T), expected: T, desired: T, success: Ordering, failure: Ordering) -> (bool, T)

| Constraints | 1. failure cannot be Release or AcqRel (failure is a load).

  1. failure cannot be stronger than success (e.g., if success is Relaxed , failure cannot be SeqCst ). |
  2. | Description | Atomically checks if *ptr == expected .

Success : Writes desired using success order. Returns (true, old_val) .

Failure : Loads current value using failure order. Returns (false, current_val) .

Note : old_val and current_val represent the value at ptr immediately before the operation.* |

std::atomic::fetch_add / sub / and / or / xor

Signature fetch_op(ptr: &Atomic(T), val: T, order: Ordering) -> T
Constraints Any Ordering is valid.
Description Atomically performs the arithmetic/bitwise operation and returns the previous value.

Usage & Safety

i32 { io::println("Hello World"); // ...normal file code... return 0; } /**  * Test 1: Basic Load and Store  */ #test fn test_atomic_basic(t: &Tap) -> bool {   var val: Atomic(i32) = Atomic(10);   // Test Initial load (Using SeqCst for strongest safety)   t.ok(std::atomic::load(&val, Ordering::SeqCst) == 10, "Atomic initializes correctly");   // Test Store   std::atomic::store(&val, 42, Ordering::SeqCst);   let result = std::atomic::load(&val, Ordering::SeqCst);   t.ok(result == 42, "Atomic store/load persisted value");   return true; } /**  * Test 2: Compare and Exchange (CAS)  */ #test fn test_atomic_cas(t: &Tap) -> bool {   var val: Atomic(i32) = Atomic(100);   // 1. Successful Swap   // Expected: 100, New: 200. Current is 100. -> Should succeed.   let old_val_1 = std::atomic::compare_exchange( &val, 100, 200, Ordering::SeqCst, // Success order Ordering::SeqCst // Failure order );   // Check if the returned value matches our expectation   let success1 = (old_val_1 == 100);   t.ok(success1 == true, "CAS success reported true (old value matched expected)");   t.ok(old_val_1 == 100, "CAS returned original value");   t.ok(std::atomic::load(&val, Ordering::SeqCst) == 200, "CAS success updated memory to 200");   // 2. Failed Swap (Stale read)   // Expected: 100 (stale), New: 300. Current is 200. -> Should fail.   let old_val_2 = std::atomic::compare_exchange( &val, 100, 300, Ordering::SeqCst, Ordering::SeqCst );   let success2 = (old_val_2 == 100);   t.ok(success2 == false, "CAS failure reported false (old value did not match)");   t.ok(old_val_2 == 200, "CAS failure returns current actual value (200)");   t.ok(std::atomic::load(&val, Ordering::SeqCst) == 200, "CAS failure did not update memory");   return true; } /**  * Test 3: Concurrent Increment  */ #test fn test_atomic_concurrency(t: &Tap) -> bool {   var counter: Atomic(i32) = Atomic(0);   let iterations = 50;   // Define the driver as a named async function to satisfy the compiler   async fn driver() -> void {     // Define the worker logic     async fn worker() -> void {       var i = 0;       while (i < iterations) { // Use Relaxed for simple counters where order relative to other vars doesn't matter         std::atomic::fetch_add(&counter, 1, Ordering::Relaxed);         await sleep(1ms);         i += 1;       }     }     // Spawn two workers     let t1 = worker();     let t2 = worker();     // Await them     await t1;     await t2;   }   // Pass the invoked function future to block_on   std::concurrency::block_on(driver());   let final_count = std::atomic::load(&counter, Ordering::SeqCst);   let expected = iterations * 2;   t.ok(final_count == expected, `Concurrent add expected ${expected}, got ${final_count}`);   return true; } #test fn main () -> i32 { // always the test main   let tests: Test[] = [     test_atomic_basic,     test_atomic_cas,     test_atomic_concurrency,   ];   let t: Tap = {};   t.comment("# Testing Atomics\n");   t.plan(9);   t.run(tests); if (t.failed > 0) { return 1; // Standard "Generic Error" code }   return 0; }" lang="ae" title="Example" id="b4f85316c768b">
Example
0 #test import { Test, Tap } from "test";
1 #test import { sleep } from "time";
2 #test import { Ordering } from "sync";
3
4 import io from "io" ;
5
6 fn main ( ) - > i32 {
7 io : : println ( "Hello World" ) ;
8 // ...normal file code...
9 return 0 ;
10 }
11
12 / * *
13   * Test 1 : Basic Load and Store
14   * /
15 #test fn test_atomic_basic(t: &Tap) -> bool {
16   var val : Atomic ( i32 ) = Atomic ( 10 ) ;
17
18   // Test Initial load (Using SeqCst for strongest safety)
19   t . ok ( std : : atomic : : load ( & val , Ordering : : SeqCst ) = = 10 , "Atomic initializes correctly" ) ;
20
21   // Test Store
22   std : : atomic : : store ( & val , 42 , Ordering : : SeqCst ) ;
23   let result = std : : atomic : : load ( & val , Ordering : : SeqCst ) ;
24
25   t . ok ( result = = 42 , "Atomic store / load persisted value" ) ;
26   return true ;
27 }
28
29 / * *
30   * Test 2 : Compare and Exchange ( CAS )
31   * /
32 #test fn test_atomic_cas(t: &Tap) -> bool {
33   var val : Atomic ( i32 ) = Atomic ( 100 ) ;
34
35   // 1. Successful Swap
36   // Expected: 100, New: 200. Current is 100. -> Should succeed.
37   let old_val_1 = std : : atomic : : compare_exchange (
38 & val ,
39 100 ,
40 200 ,
41 Ordering : : SeqCst , // Success order
42 Ordering : : SeqCst // Failure order
43 ) ;
44
45   // Check if the returned value matches our expectation
46   let success1 = ( old_val_1 = = 100 ) ;
47
48   t . ok ( success1 = = true , "CAS success reported true ( old value matched expected ) " ) ;
49   t . ok ( old_val_1 = = 100 , "CAS returned original value" ) ;
50   t . ok ( std : : atomic : : load ( & val , Ordering : : SeqCst ) = = 200 , "CAS success updated memory to 200" ) ;
51
52   // 2. Failed Swap (Stale read)
53   // Expected: 100 (stale), New: 300. Current is 200. -> Should fail.
54   let old_val_2 = std : : atomic : : compare_exchange (
55 & val ,
56 100 ,
57 300 ,
58 Ordering : : SeqCst ,
59 Ordering : : SeqCst
60 ) ;
61
62   let success2 = ( old_val_2 = = 100 ) ;
63
64   t . ok ( success2 = = false , "CAS failure reported false ( old value did not match ) " ) ;
65   t . ok ( old_val_2 = = 200 , "CAS failure returns current actual value ( 200 ) " ) ;
66   t . ok ( std : : atomic : : load ( & val , Ordering : : SeqCst ) = = 200 , "CAS failure did not update memory" ) ;
67
68   return true ;
69 }
70
71 / * *
72   * Test 3 : Concurrent Increment
73   * /
74 #test fn test_atomic_concurrency(t: &Tap) -> bool {
75   var counter : Atomic ( i32 ) = Atomic ( 0 ) ;
76   let iterations = 50 ;
77
78   // Define the driver as a named async function to satisfy the compiler
79   async fn driver ( ) - > void {
80
81     // Define the worker logic
82     async fn worker ( ) - > void {
83       var i = 0 ;
84       while ( i < iterations ) {
85 // Use Relaxed for simple counters where order relative to other vars doesn't matter
86         std : : atomic : : fetch_add ( & counter , 1 , Ordering : : Relaxed ) ;
87         await sleep ( 1ms ) ;
88         i + = 1 ;
89       }
90     }
91
92     // Spawn two workers
93     let t1 = worker ( ) ;
94     let t2 = worker ( ) ;
95
96     // Await them
97     await t1 ;
98     await t2 ;
99   }
100
101   // Pass the invoked function future to block_on
102   std : : concurrency : : block_on ( driver ( ) ) ;
103
104   let final_count = std : : atomic : : load ( & counter , Ordering : : SeqCst ) ;
105   let expected = iterations * 2 ;
106
107   t . ok ( final_count = = expected , `Concurrent add expected ${expected}, got ${final_count}` ) ;
108   return true ;
109 }
110
111 #test fn main () -> i32 { // always the test main
112   let tests : Test [ ] = [
113     test_atomic_basic ,
114     test_atomic_cas ,
115     test_atomic_concurrency ,
116   ] ;
117
118   let t : Tap = { } ;
119   t . comment ( "# Testing Atomics\n");
120   t . plan ( 9 ) ;
121   t . run ( tests ) ;
122
123 if ( t . failed > 0 ) {
124 return 1 ; // Standard "Generic Error" code
125 }
126   return 0 ;
127 }

Concurrency & Parallelism

Aela's model is built on two orthogonal keywords, `async` and `task` , 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 Concepts

It is crucial to understand that async and task are separate modifiers with distinct physical meanings, even though they are designed to feel cohesive.

Keyword Concept Execution Model Primary Use Case
`async` Concurrency Cooperative (Lazy). Runs on the current thread/loop. Pauses at await . I/O-bound operations (Network, Disk, Timers).
`task` Parallelism Preemptive (Hot). Spawns onto a thread pool . Runs in background immediately. CPU-bound operations (Encryption, Data Processing).

async : The Pausable Function

An async function is a state machine. Calling it does not start execution; it returns a Future (a "cold" task). The code only runs when you explicitly await it or pass it to a poller like std::concurrency::select .

task : The Parallel Job

A task function is a unit of parallel work. Calling it immediately submits the work to the runtime's thread pool and returns a Task handle (a "hot" task). You continue executing while the task runs in the background.

Function Modifiers

You can combine these keywords to define exactly how a function behaves.

Declaration Behavior
fn foo() Synchronous. Blocks the caller until finished.
async fn foo() Asynchronous. Returns a Future . Runs on the caller's loop when awaited.
task fn foo() Parallel. Returns a Task . Runs on a thread pool immediately. Cannot await inside (unless also async).

Parallelism: task

While async handles waiting (IO), task handles doing (CPU). Aela uses a Work-Stealing Scheduler to distribute CPU-intensive work across a pool of OS threads.

task fn : Hot Execution

A function marked with task is distinct from a normal function or an async function.

Eager Submission: When you call a `task fn`, it is immediately pushed to the global scheduler queue. You do not need to `await` it to start it. The `Task(T)` Handle: It returns a handle that tracks the running job. If you drop the handle, the task continues running (detached). Isolation: * task functions cannot capture references to the surrounding stack unless those references are Send and Sync . They are effectively "spawned" into a new root scope.

Example
0 // Defined as a parallel job
1 task fn render_frame ( data : Scene ) - > Frame {
2 // This runs on a separate thread
3 return heavy_computation ( data ) ;
4 }
5
6 async fn main ( ) {
7 // Starts running IMMEDIATELY on a worker thread.
8 // Returns a handle, does not block 'main'.
9 let handle = render_frame ( my_scene ) ;
10
11 io : : print ( "Rendering started . . . " ) ;
12
13 // Wait for the result here.
14 let frame = await handle ;
15 }

task { ... } : Structured Parallelism

The task block is a Fork-Join Barrier . It is designed to safely parallelize work within a single function scope.

The Barrier: The block does not finish until every task spawned inside it has completed. Stack Safety: Because the block guarantees that all inner tasks finish before the function continues, inner tasks can safely borrow variables from the parent function . This is impossible with standard "spawn" functions in other languages (which usually require copying/moving data). Work Stealing: * The tasks inside the block are added to the local worker's queue. Other idle threads will "steal" these tasks to help complete the block faster.

Example
0 async fn process_user_request ( uid : i32 ) - > Response {
1 var user : User ;
2 var history : [ Order ] ;
3
4 // The function PAUSES here.
5 // The runtime splits execution into 2 parallel branches.
6 task {
7 // Branch 1: Fetch user (IO + Deserialization)
8 user = await db . fetch_user ( uid ) ;
9
10 // Branch 2: Fetch history
11 history = await db . fetch_history ( uid ) ;
12 }
13 // The block waits at the end.
14 // The function RESUMES here only after both are done.
15
16 return Response ( user , history ) ;
17 }

Pool Control & Oversubscription

The runtime manages the complexity of OS threads so you don't have to.

  1. Oversubscription: The runtime defaults to one thread per CPU core ( std::task::available_parallelism() ). It is designed to be "oversubscribed" safely. You can spawn thousands of task functions; the runtime will queue them and execute them as fast as possible on the fixed number of worker threads.
  2. Backpressure: The scheduler uses a bounded global queue (default capacity: 256 tasks). If you spawn tasks faster than the CPU can process them, calling a task fn will eventually transition from non-blocking to blocking (waiting for a slot in the queue).
  3. Environment Control: For deployment tuning, you can override the thread pool size via environment variable: AELA_TASK_COUNT=8 .

Concurrency: async

Sometimes you need a small unit of async work without defining a whole function, or you want to start async work from an sync function.

async fn : A reusable pausable function.

i32 { var v = 39; await sleep(128ms); v += 1; await sleep(128ms); v += 1; await sleep(128ms); v += 1; return v; } fn main () -> i32 { var x: i32; async fn foo () -> void { x = 22; } std::concurrency::block_on(foo()); io::println("x=\(x)"); return 0; }" lang="ae" title="Example" id="7c8f9c7e6fc9d">
Example
0 import { sleep } from "time" ;
1 import io from "io" ;
2
3 async fn bar ( ) - > i32 {
4 var v = 39 ;
5 await sleep ( 128ms ) ;
6 v + = 1 ;
7 await sleep ( 128ms ) ;
8 v + = 1 ;
9 await sleep ( 128ms ) ;
10 v + = 1 ;
11 return v ;
12 }
13
14 fn main ( ) - > i32 {
15 var x : i32 ;
16
17 async fn foo ( ) - > void {
18 x = 22 ;
19 }
20
21 std : : concurrency : : block_on ( foo ( ) ) ;
22 io : : println ( "x = \ ( x ) " ) ;
23 return 0 ;
24 }

async { ... } : An anonymous Future .

It captures variables from the surrounding scope. Like async fn , it is lazy and it does not run until awaited. All tasks must be completed and handled before before the program will continue.

Example
0 fn main ( ) - > i32 {
1 var x : i32 ;
2
3 async {
4 x = await bar ( ) ;
5 }
6
7 if ( x ! = 42 ) {
8 return 1 ;
9 }
10
11 io : : println ( "x = \ ( x ) " ) ;
12 return 0 ;
13 }

Control Flow

`await` : Pauses the current function, yielding control back to the runtime until the awaited operation completes. `std::concurrency::block_on` : The bridge between synchronous and asynchronous code. It starts a temporary event loop to drive a future to completion. This is typically used only in main or unit tests. `std::concurrency::select` : Races multiple futures. It waits for the first * one to complete and cancels the others.

Example
0 let job = await std : : concurrency : : select ( [
1 // Case 1: We receive a message
2 channel . recv ( ) ,
3
4 // Case 2: We get tired of waiting (Timeout)
5 timer . after ( 500ms )
6 ] ) ;

Safety

Aela treats the scheduler as a "Safety System" rather than an aftermarket part. Because the runtime is integrated into the language, the compiler can provide stronger guarantees than if it took a library-based approach.

  1. Compile-Time Data-Race Prevention: The compiler knows the difference between a local async execution (single-threaded) and a task execution (multi-threaded). It enforces Send and Sync rules strictly at the task boundary.
  2. Single Blocking Bridge: A built-in runtime provides one, official way to handle blocking calls: std::task::run_blocking() . This prevents the "Color of your function" problem from causing deadlocks when libraries mix blocking/non-blocking strategies.
  3. Guaranteed Cleanup: task handles are tied to the runtime. If a Task handle is dropped, the language enforces a consistent policy (detachment), preventing undefined behavior or zombie threads common in ad-hoc library implementations.

Formal Verification

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.

  • #let (the ghost of...)
  • #requires (pre condition)
  • #ensures (post condition)
  • #invariant (can’t change)
  • #variant (must change)

All of them appear before the function/loop they describe. None of these exist at runtime. They’re for the verifier only.

#let — The ghost of...

#let defines a ghost name for an expression, purely for specs.

You can put it right before a function or loop to bind names used in the following specs:

Example
0 #let oldN = n;
1 #requires oldN >= 0;
2 #ensures result == oldN + 1;
3 fn addOne ( n : i32 ) - > result : i32 {
4 return n + 1 ;
5 }

Here:

  • oldN is only visible to the verifier.
  • oldN does not exist in the compiled code.
  • You can use oldN in #requires , #ensures , #invariant , #variant , etc.

Think of #let as: "Give me a ghost name for this value/expression so I can talk about it in my conditions."

#requires — Precondition

#requires means this must be true when we enter this function (or loop). Placed directly above the function:

Example
0 #requires n >= 0;
1 fn factorial ( n : i32 ) - > i32 {
2 // ...
3 }

The verifier checks every call to factorial proves n >= 0 and assumes n >= 0 inside the body. You can also (optionally) apply #requires to loops, if you want to state assumptions at loop entry:

Example
0 #requires n >= 0; // Here `#requires` is about the state at loop entry.
1 #invariant 0 <= i <= n;
2 #variant n - i;
3 while ( i < n ) {
4 i = i + 1 ;
5 }

#ensures — Postcondition

#ensures goes right before the function and says: "This will be true after this function returns."

Example:

Example
0 #requires n >= 0;
1 #ensures result >= n;
2 fn addOne ( n : i32 ) - > i32 {
3 return n + 1 ;
4 }

The verifier proves we assume n >= 0 on entry and the value returned by the function ( result ) always satisfies result >= n .

Summation

Example
0 pure fn sum_range ( a : i32 [ ] , start : i32 , end : i32 ) - > result : i32 {
1 var acc = 0 ;
2 var i = start ;
3 while ( i < end ) {
4 acc = acc + a [ i ] ;
5 i = i + 1 ;
6 }
7 result = acc ;
8 return ;
9 }
10
11 #requires for (let i in 0..std::length(a)) {
12 std : : assert ( a [ i ] > = 0 ) ;
13 }
14 #ensures result == sum_range(a, 0, std::length(a));
15 fn sum ( a : i32 [ ] ) - > result : i32 {
16
17 #let originalLength = std::length(a);
18 #let originalArray = a;
19
20 var total = 0 ;
21 var i = 0 ;
22
23 #invariant 0 <= i && i <= originalLength;
24 #invariant total == sum_range(originalArray, 0, i);
25 #variant originalLength - i;
26 while ( i < originalLength ) {
27 total = total + a [ i ] ;
28 i = i + 1 ;
29 }
30
31 result = total ;
32 return ;
33 }

#invariant

An invariant can’t change (must stay true during loop). It goes immediately before a loop:

Example
0 #invariant 0 <= i && i <= n;
1 while ( i < n ) {
2 i = i + 1 ;
3 }

This means:

  • Before the first iteration: 0 <= i <= n must hold.
  • After every iteration, if we go around again, it must still hold.
  • When the loop exits, 0 <= i <= n is still true.

The invariant describes what must not be broken across iterations. It’s not "the variable can’t change"; it’s this relationship must stay true even as variables change.

#variant

#variant also goes right before the loop , together with #invariant :

Example
0 #invariant 0 <= i && i <= n;
1 #variant n - i;
2 while i < n {
3 i = i + 1 ;
4 }

Here:

  • n - i is the variant expression .
  • The verifier proves:
  • n - i is always ≥ 0 inside the loop.
  • On every iteration that continues the loop, n - i gets strictly smaller .

This proves the loop must terminate .

So:

  • #invariant - this condition must keep holding.
  • #variant - this measure must go down when we repeat.

Your loop may increase i , but your #variant is something that decreases (like n - i ).

Examples

General rule: Directives attach to the next construct.

For functions

Example
0 #let oldN = n;
1 #requires oldN >= 0;
2 #ensures result == oldN + 1;
3 fn addOne ( n : i32 ) - > result : i32 {
4 return n + 1 ;
5 }

For loops

Example
0 #invariant 0 <= i && i <= n;
1 #variant n - i;
2 while ( i < n ) {
3 i = i + 1 ;
4 }

You can stack them:

Example
0 #let start = i;
1 #requires 0 <= i && i <= n;
2 #invariant 0 <= i && i <= n;
3 #variant n - i;
4 while ( i < n ) {
5 i = i + 1 ;
6 }

All of this is compile-time-only verification syntax , erased in the compiled program.

Cheat Sheet

  • #let name = expr - Ghost name for expr , only for use in specs.
  • #requires P - P must be true before we enter this function/loop.
  • #ensures Q - Q will be true when the function returns.
  • #invariant I - I must hold before the loop, after each iteration, and when it ends.
  • #variant V - V must strictly decrease each time we repeat the loop (and stay in a well-founded set).

All placed before the thing they describe.

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

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.
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 ) {
1 first : T ,
2 second : U
3 }
4
5 let p : Pair ( i32 , string ) = {
6 first : 1 ,
7 second : "hi"
8 } ;
9
10 let x : Option ( i32 ) = Option : : Some ( 3 ) ;

Reference Types

&T is a borrowed reference to T . In Aela, the mut keyword isn't part of a type; it's a marker on a parameter or call argument that grants temporary, mutable access (a "mutable loan") to a value.

  • 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.
On Parameter Definition (fn):
0 fn foo ( mut param : & Type ) - > void {
1 }

This declares: "This function requires a mutable loan for param. I intend to modify the original value that the caller passes."

On Call Argument (call):
0 foo ( mut my_value ) ;

This declares: "I am granting a mutable loan to foo for this function call. I am aware that foo may modify it."

This design makes it explicit at both the function's definition site and the call site exactly where a value is allowed to be changed, aligning with the "in-out parameter" model many developers are familiar with.

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="1868225b33b17">
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

Boolean Condition
0 if ( )
1 [ else ]
Pattern-Match Binding (if-let)
0 if let =
1 [ else ]

Description

Standard conditional branching. if statements have two primary forms:

Boolean Condition: The standard form evaluates a which must result in a bool. If true, the then_statement (usually a block) is executed. If false, the optional else_statement is executed.

Boolean Condition
0 let x : i32 = 10 ;
1
2 if ( x > 0 ) {
3 print ( "Positive" ) ;
4 } else {
5 print ( "Non - positive" ) ;
6 }

Pattern-Match Binding (if-let): This form attempts to match the result of against the given .

If the match is successful, any variables bound in the are introduced, and the then_block is executed. These new variables are only in scope inside this block.

If the match is unsuccessful, the else statement is executed.

Pattern-Match Binding (if-let)
0 let v : Option ( i32 ) = Some ( 42 ) ;
1
2 if let Option : : Some ( value ) = v {
3 // 'value' is bound and only in scope here
4 print ( "Got value : { } " , value ) ;
5 } else {
6 // 'value' is not in scope here
7 print ( "Got None" ) ;
8 }

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 : i32 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

Aela supports two distinct forms of `match` :

  1. Statement `match` — used for control flow and side effects
  2. Expression `match` — used to compute a value

Which form you get is determined syntactically , not by context.

4.1 Statement match

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

A statement `match` is used for control flow. Each arm executes a block of statements and does not produce a value.

Block arms `{ ... }` are allowed return , break , and side effects are allowed * The match itself has no value

This form is typically used for branching logic, validation, logging, or early returns.

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

4.2 Expression match

Example
0 let | var : [ type ] = match ( ) {
1 = > ,
2 . . .
3 = >
4 } ;

An expression `match` computes a value.

  • Each arm must be a single expression
  • Block bodies `{ ... }` are not allowed
  • The match must be exhaustive
  • All arm expressions must unify to a single type

This restriction avoids implicit returns, ambiguous control flow, and complex typing rules.

"one", _ => "other" };" lang="aela" title="Example" id="e30ea1c5c1206">
Example
0 let label : string = match ( value ) {
1 0 = > "zero" ,
2 1 = > "one" ,
3 _ = > "other"
4 } ;

Note

Block arms are not allowed in expression-matches!

1 };" lang="aela" title="Example" id="b51c642e06fd1">
Example
0 let x = match ( value ) {
1 0 = > { print ( "zero" ) ; 0 } , // ERROR
2 _ = > 1
3 } ;

Instead, move side effects outside the expression match.

{} }" lang="aela" title="Example" id="d2a67f0969238">
Example
0 match ( value ) {
1 0 = > print ( "zero" ) ,
2 _ = > { }
3 }

In Aela, blocks are statements , not expressions. A block does not produce a value unless explicitly used as part of a language construct that allows expressions. The reason for this is, we optimize for explicitness, predictability, and bounded complexity. It separates control flow from value computation .

  • Statement `match` - control flow, blocks, side effects
  • Expression `match` - values only, expressions only

NOTE

No implicit block return or tail-values. No semicolon footguns.

Example
0 x // yields
1 x ; // discards

This creates a well-known class of bugs:

  • missing semicolon changes semantics
  • especially dangerous for JS/C-family programmers
  • hard to spot in reviews

Aela eliminates an entire class of bugs here:

  • blocks never yield values
  • expressions yield values
  • the grammar enforces the distinction

Instead of a lint rule its a language guarantee. You don't need a never type. And it keeps things easier to learn.

4.3 Struct Literals in Expression Matches

Brace syntax { ... } is still allowed in expression matches when it is a struct literal, not a block.

Example
0 let p = match ( kind ) {
1 A = > { x : 1 , y : 2 } ,
2 B = > { x : 3 , y : 4 }
3 } ;

If a brace body does not form a valid struct literal, it is rejected with an error.

4.4 Summary

Feature Statement match Expression match
Produces a value NO OK
Allows { ... } blocks OK NO
Allows return OK NO
Used for Control flow Value computation

5. return

Aela requires explicit `return` statements for all function exits. Functions do not implicitly return the value of their final expression. This is an intentional design choice that prioritizes clarity and predictability over implicit behavior.

Syntax

Example
0 return [ ] ;

Examples

Example
0 return ;
1 return x + 1 ;

Description

Exits a function immediately with the return value perscribed by the function signature.

Function boundaries are explicit

Requiring return makes it immediately obvious where a function exits and what value is returned. This is especially important in functions with multiple branches or early exits.

Example
0 fn add ( x : i32 , y : i32 ) - > i32 {
1 return x + y ;
2 }

There is no ambiguity about what value leaves the function.

Avoids semicolon-sensitive behavior

In expression-oriented languages, a missing semicolon can silently change behavior: Aela avoids this entire class of bugs by never treating the final expression of a function as an implicit return.

Example
0 x // yields
1 x ; // discards

Prevents accidental returns

Without implicit returns, writing an expression at the end of a function body does not change control flow: This makes accidental returns impossible and forces intent to be explicit.

Example
0 fn foo ( x : i32 ) - > i32 {
1 x + 42 ; // evaluated, but not returned
2 return x ;
3 }

4. Keeps return semantics simple and consistent

In Aela, return always means "Exit the current function immediately!" It's never overloaded to mean...

  • return from a block
  • yield from a match arm
  • produce the value of an expression

This simplicity avoids subtle interactions with pattern matching, block structure, and type inference.

Summary

Design Choice Result
Explicit return Clear function exits
No implicit function returns No semicolon footguns
return never yields values Simple control-flow reasoning
Match expressions stay value-only No hidden complexity

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.

NOTE

Blocks never yield implicit values!

Example

Example
0 {
1 let x : i32 = 1 ;
2 let y : i32 = 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="ead86df0f5d3d">
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 }

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 ( ) ;

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

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 [ AEA0012 ] : 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 [ public ] fn ( self : & Self , . . . ) - > { . . . }
6 }

Details

  • Methods are private by default. To allow a method or constructor to be called from another module, it must be marked with the public keyword.
  • The fn 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 public 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() -> i32 { 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="cf14d0c6cb7c9">
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 ( ) - > i32 {
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 : i32 ;
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 ( (i32) 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 (integer * 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 + integer
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: i32, elem_size: i32) -> 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: i32, 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: i32) -> 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)) -> i32
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: i32, len: i32) -> &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: i32, len: i32) -> 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 : i32 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.

Atomics

Introduction

Atomic(T) is a fundamental intrinsic type designed for high-performance concurrency. It provides a wrapper around primitive types that guarantees safe access across multiple threads.

Unlike standard variables, operations on Atomic(T) are indivisible. However, atomicity alone is insufficient for correctness; memory ordering is equally critical. This API exposes fine-grained control over how the CPU and compiler are allowed to reorder memory operations, enabling the construction of lock-free data structures and synchronization primitives.

Core Concepts

Atomicity & Type Constraints

An operation is atomic if it is indivisible: a thread observes either the old value or the new value, never a "torn" or intermediate state.

Allowed Types : `T` must be a primitive integer, boolean, pointer, or an `enum` with a fixed underlying integer representation (e.g., `repr(u8)`) where all bit patterns are valid. Alignment : Atomic(T) requires natural alignment for T . Static Check : If alignment is statically known to be insufficient, it is a compile-time error . Dynamic Check : If a pointer is misaligned at runtime, the behavior is a guaranteed trap (panic).

Lock-Free Guarantees

While Atomic(T) enables lock-free algorithms, the operations themselves are not guaranteed to be lock-free on all platforms for all types.

Hardware Support : If the target CPU supports atomic instructions for `sizeof(T)`, operations are compiled to those instructions. Software Fallback : If the hardware lacks support (e.g., 64-bit atomics on 32-bit arch), the runtime may use a hidden global lock / hashed lock pool. Intrinsic Check *: Use std::atomic::is_lock_free() -> bool to query if operations on T are truly lock-free on the current target.

Memory Ordering

The Ordering enum controls how operations synchronize with other threads.

  1. `Relaxed` : No synchronization. Only atomicity is ensured.
  2. `Acquire` : Valid for loads. It ensures that no subsequent memory accesses can be reordered before this operation.
  3. `Release` : Valid for stores. It ensures that no previous memory accesses can be reordered after this operation.
  4. `AcqRel` : Valid for RMW (Read-Modify-Write). Combines Acquire and Release.
  5. `SeqCst` : Strongest ordering. Enforces a total order on all SeqCst operations consistent with program order.

The Synchronization Contract

The primary mechanism for thread coordination is the Acquire-Release pair :

An Acquire operation on an atomic object synchronizes-with a Release operation on the same object if the Acquire reads the value written by that Release (or a value from a release sequence headed by that Release ).

Effect : All memory writes that happened before the Release are guaranteed to be visible to the thread that performed the Acquire .

The Intrinsic API

std::atomic::load

Signature load(ptr: &Atomic(T), order: Ordering) -> T
Constraints order must be Relaxed , Acquire , or SeqCst .
Description Atomically reads the value.

std::atomic::store

Signature store(ptr: &Atomic(T), val: T, order: Ordering)
Constraints order must be Relaxed , Release , or SeqCst .
Description Atomically writes a value.

std::atomic::exchange

Signature exchange(ptr: &Atomic(T), val: T, order: Ordering) -> T
Constraints Any Ordering is valid.
Description Atomically writes val and returns the previous value (the value immediately before the swap).

std::atomic::compare_exchange

Signature compare_exchange(ptr: &Atomic(T), expected: T, desired: T, success: Ordering, failure: Ordering) -> (bool, T)

| Constraints | 1. failure cannot be Release or AcqRel (failure is a load).

  1. failure cannot be stronger than success (e.g., if success is Relaxed , failure cannot be SeqCst ). |
  2. | Description | Atomically checks if *ptr == expected .

Success : Writes desired using success order. Returns (true, old_val) .

Failure : Loads current value using failure order. Returns (false, current_val) .

Note : old_val and current_val represent the value at ptr immediately before the operation.* |

std::atomic::fetch_add / sub / and / or / xor

Signature fetch_op(ptr: &Atomic(T), val: T, order: Ordering) -> T
Constraints Any Ordering is valid.
Description Atomically performs the arithmetic/bitwise operation and returns the previous value.

Usage & Safety

i32 { io::println("Hello World"); // ...normal file code... return 0; } /**  * Test 1: Basic Load and Store  */ #test fn test_atomic_basic(t: &Tap) -> bool {   var val: Atomic(i32) = Atomic(10);   // Test Initial load (Using SeqCst for strongest safety)   t.ok(std::atomic::load(&val, Ordering::SeqCst) == 10, "Atomic initializes correctly");   // Test Store   std::atomic::store(&val, 42, Ordering::SeqCst);   let result = std::atomic::load(&val, Ordering::SeqCst);   t.ok(result == 42, "Atomic store/load persisted value");   return true; } /**  * Test 2: Compare and Exchange (CAS)  */ #test fn test_atomic_cas(t: &Tap) -> bool {   var val: Atomic(i32) = Atomic(100);   // 1. Successful Swap   // Expected: 100, New: 200. Current is 100. -> Should succeed.   let old_val_1 = std::atomic::compare_exchange( &val, 100, 200, Ordering::SeqCst, // Success order Ordering::SeqCst // Failure order );   // Check if the returned value matches our expectation   let success1 = (old_val_1 == 100);   t.ok(success1 == true, "CAS success reported true (old value matched expected)");   t.ok(old_val_1 == 100, "CAS returned original value");   t.ok(std::atomic::load(&val, Ordering::SeqCst) == 200, "CAS success updated memory to 200");   // 2. Failed Swap (Stale read)   // Expected: 100 (stale), New: 300. Current is 200. -> Should fail.   let old_val_2 = std::atomic::compare_exchange( &val, 100, 300, Ordering::SeqCst, Ordering::SeqCst );   let success2 = (old_val_2 == 100);   t.ok(success2 == false, "CAS failure reported false (old value did not match)");   t.ok(old_val_2 == 200, "CAS failure returns current actual value (200)");   t.ok(std::atomic::load(&val, Ordering::SeqCst) == 200, "CAS failure did not update memory");   return true; } /**  * Test 3: Concurrent Increment  */ #test fn test_atomic_concurrency(t: &Tap) -> bool {   var counter: Atomic(i32) = Atomic(0);   let iterations = 50;   // Define the driver as a named async function to satisfy the compiler   async fn driver() -> void {     // Define the worker logic     async fn worker() -> void {       var i = 0;       while (i < iterations) { // Use Relaxed for simple counters where order relative to other vars doesn't matter         std::atomic::fetch_add(&counter, 1, Ordering::Relaxed);         await sleep(1ms);         i += 1;       }     }     // Spawn two workers     let t1 = worker();     let t2 = worker();     // Await them     await t1;     await t2;   }   // Pass the invoked function future to block_on   std::concurrency::block_on(driver());   let final_count = std::atomic::load(&counter, Ordering::SeqCst);   let expected = iterations * 2;   t.ok(final_count == expected, `Concurrent add expected ${expected}, got ${final_count}`);   return true; } #test fn main () -> i32 { // always the test main   let tests: Test[] = [     test_atomic_basic,     test_atomic_cas,     test_atomic_concurrency,   ];   let t: Tap = {};   t.comment("# Testing Atomics\n");   t.plan(9);   t.run(tests); if (t.failed > 0) { return 1; // Standard "Generic Error" code }   return 0; }" lang="ae" title="Example" id="b4f85316c768b">
Example
0 #test import { Test, Tap } from "test";
1 #test import { sleep } from "time";
2 #test import { Ordering } from "sync";
3
4 import io from "io" ;
5
6 fn main ( ) - > i32 {
7 io : : println ( "Hello World" ) ;
8 // ...normal file code...
9 return 0 ;
10 }
11
12 / * *
13   * Test 1 : Basic Load and Store
14   * /
15 #test fn test_atomic_basic(t: &Tap) -> bool {
16   var val : Atomic ( i32 ) = Atomic ( 10 ) ;
17
18   // Test Initial load (Using SeqCst for strongest safety)
19   t . ok ( std : : atomic : : load ( & val , Ordering : : SeqCst ) = = 10 , "Atomic initializes correctly" ) ;
20
21   // Test Store
22   std : : atomic : : store ( & val , 42 , Ordering : : SeqCst ) ;
23   let result = std : : atomic : : load ( & val , Ordering : : SeqCst ) ;
24
25   t . ok ( result = = 42 , "Atomic store / load persisted value" ) ;
26   return true ;
27 }
28
29 / * *
30   * Test 2 : Compare and Exchange ( CAS )
31   * /
32 #test fn test_atomic_cas(t: &Tap) -> bool {
33   var val : Atomic ( i32 ) = Atomic ( 100 ) ;
34
35   // 1. Successful Swap
36   // Expected: 100, New: 200. Current is 100. -> Should succeed.
37   let old_val_1 = std : : atomic : : compare_exchange (
38 & val ,
39 100 ,
40 200 ,
41 Ordering : : SeqCst , // Success order
42 Ordering : : SeqCst // Failure order
43 ) ;
44
45   // Check if the returned value matches our expectation
46   let success1 = ( old_val_1 = = 100 ) ;
47
48   t . ok ( success1 = = true , "CAS success reported true ( old value matched expected ) " ) ;
49   t . ok ( old_val_1 = = 100 , "CAS returned original value" ) ;
50   t . ok ( std : : atomic : : load ( & val , Ordering : : SeqCst ) = = 200 , "CAS success updated memory to 200" ) ;
51
52   // 2. Failed Swap (Stale read)
53   // Expected: 100 (stale), New: 300. Current is 200. -> Should fail.
54   let old_val_2 = std : : atomic : : compare_exchange (
55 & val ,
56 100 ,
57 300 ,
58 Ordering : : SeqCst ,
59 Ordering : : SeqCst
60 ) ;
61
62   let success2 = ( old_val_2 = = 100 ) ;
63
64   t . ok ( success2 = = false , "CAS failure reported false ( old value did not match ) " ) ;
65   t . ok ( old_val_2 = = 200 , "CAS failure returns current actual value ( 200 ) " ) ;
66   t . ok ( std : : atomic : : load ( & val , Ordering : : SeqCst ) = = 200 , "CAS failure did not update memory" ) ;
67
68   return true ;
69 }
70
71 / * *
72   * Test 3 : Concurrent Increment
73   * /
74 #test fn test_atomic_concurrency(t: &Tap) -> bool {
75   var counter : Atomic ( i32 ) = Atomic ( 0 ) ;
76   let iterations = 50 ;
77
78   // Define the driver as a named async function to satisfy the compiler
79   async fn driver ( ) - > void {
80
81     // Define the worker logic
82     async fn worker ( ) - > void {
83       var i = 0 ;
84       while ( i < iterations ) {
85 // Use Relaxed for simple counters where order relative to other vars doesn't matter
86         std : : atomic : : fetch_add ( & counter , 1 , Ordering : : Relaxed ) ;
87         await sleep ( 1ms ) ;
88         i + = 1 ;
89       }
90     }
91
92     // Spawn two workers
93     let t1 = worker ( ) ;
94     let t2 = worker ( ) ;
95
96     // Await them
97     await t1 ;
98     await t2 ;
99   }
100
101   // Pass the invoked function future to block_on
102   std : : concurrency : : block_on ( driver ( ) ) ;
103
104   let final_count = std : : atomic : : load ( & counter , Ordering : : SeqCst ) ;
105   let expected = iterations * 2 ;
106
107   t . ok ( final_count = = expected , `Concurrent add expected ${expected}, got ${final_count}` ) ;
108   return true ;
109 }
110
111 #test fn main () -> i32 { // always the test main
112   let tests : Test [ ] = [
113     test_atomic_basic ,
114     test_atomic_cas ,
115     test_atomic_concurrency ,
116   ] ;
117
118   let t : Tap = { } ;
119   t . comment ( "# Testing Atomics\n");
120   t . plan ( 9 ) ;
121   t . run ( tests ) ;
122
123 if ( t . failed > 0 ) {
124 return 1 ; // Standard "Generic Error" code
125 }
126   return 0 ;
127 }

Concurrency & Parallelism

Aela's model is built on two orthogonal keywords, `async` and `task` , 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 Concepts

It is crucial to understand that async and task are separate modifiers with distinct physical meanings, even though they are designed to feel cohesive.

Keyword Concept Execution Model Primary Use Case
`async` Concurrency Cooperative (Lazy). Runs on the current thread/loop. Pauses at await . I/O-bound operations (Network, Disk, Timers).
`task` Parallelism Preemptive (Hot). Spawns onto a thread pool . Runs in background immediately. CPU-bound operations (Encryption, Data Processing).

async : The Pausable Function

An async function is a state machine. Calling it does not start execution; it returns a Future (a "cold" task). The code only runs when you explicitly await it or pass it to a poller like std::concurrency::select .

task : The Parallel Job

A task function is a unit of parallel work. Calling it immediately submits the work to the runtime's thread pool and returns a Task handle (a "hot" task). You continue executing while the task runs in the background.

Function Modifiers

You can combine these keywords to define exactly how a function behaves.

Declaration Behavior
fn foo() Synchronous. Blocks the caller until finished.
async fn foo() Asynchronous. Returns a Future . Runs on the caller's loop when awaited.
task fn foo() Parallel. Returns a Task . Runs on a thread pool immediately. Cannot await inside (unless also async).

Parallelism: task

While async handles waiting (IO), task handles doing (CPU). Aela uses a Work-Stealing Scheduler to distribute CPU-intensive work across a pool of OS threads.

task fn : Hot Execution

A function marked with task is distinct from a normal function or an async function.

Eager Submission: When you call a `task fn`, it is immediately pushed to the global scheduler queue. You do not need to `await` it to start it. The `Task(T)` Handle: It returns a handle that tracks the running job. If you drop the handle, the task continues running (detached). Isolation: * task functions cannot capture references to the surrounding stack unless those references are Send and Sync . They are effectively "spawned" into a new root scope.

Example
0 // Defined as a parallel job
1 task fn render_frame ( data : Scene ) - > Frame {
2 // This runs on a separate thread
3 return heavy_computation ( data ) ;
4 }
5
6 async fn main ( ) {
7 // Starts running IMMEDIATELY on a worker thread.
8 // Returns a handle, does not block 'main'.
9 let handle = render_frame ( my_scene ) ;
10
11 io : : print ( "Rendering started . . . " ) ;
12
13 // Wait for the result here.
14 let frame = await handle ;
15 }

task { ... } : Structured Parallelism

The task block is a Fork-Join Barrier . It is designed to safely parallelize work within a single function scope.

The Barrier: The block does not finish until every task spawned inside it has completed. Stack Safety: Because the block guarantees that all inner tasks finish before the function continues, inner tasks can safely borrow variables from the parent function . This is impossible with standard "spawn" functions in other languages (which usually require copying/moving data). Work Stealing: * The tasks inside the block are added to the local worker's queue. Other idle threads will "steal" these tasks to help complete the block faster.

Example
0 async fn process_user_request ( uid : i32 ) - > Response {
1 var user : User ;
2 var history : [ Order ] ;
3
4 // The function PAUSES here.
5 // The runtime splits execution into 2 parallel branches.
6 task {
7 // Branch 1: Fetch user (IO + Deserialization)
8 user = await db . fetch_user ( uid ) ;
9
10 // Branch 2: Fetch history
11 history = await db . fetch_history ( uid ) ;
12 }
13 // The block waits at the end.
14 // The function RESUMES here only after both are done.
15
16 return Response ( user , history ) ;
17 }

Pool Control & Oversubscription

The runtime manages the complexity of OS threads so you don't have to.

  1. Oversubscription: The runtime defaults to one thread per CPU core ( std::task::available_parallelism() ). It is designed to be "oversubscribed" safely. You can spawn thousands of task functions; the runtime will queue them and execute them as fast as possible on the fixed number of worker threads.
  2. Backpressure: The scheduler uses a bounded global queue (default capacity: 256 tasks). If you spawn tasks faster than the CPU can process them, calling a task fn will eventually transition from non-blocking to blocking (waiting for a slot in the queue).
  3. Environment Control: For deployment tuning, you can override the thread pool size via environment variable: AELA_TASK_COUNT=8 .

Concurrency: async

Sometimes you need a small unit of async work without defining a whole function, or you want to start async work from an sync function.

async fn : A reusable pausable function.

i32 { var v = 39; await sleep(128ms); v += 1; await sleep(128ms); v += 1; await sleep(128ms); v += 1; return v; } fn main () -> i32 { var x: i32; async fn foo () -> void { x = 22; } std::concurrency::block_on(foo()); io::println("x=\(x)"); return 0; }" lang="ae" title="Example" id="7c8f9c7e6fc9d">
Example
0 import { sleep } from "time" ;
1 import io from "io" ;
2
3 async fn bar ( ) - > i32 {
4 var v = 39 ;
5 await sleep ( 128ms ) ;
6 v + = 1 ;
7 await sleep ( 128ms ) ;
8 v + = 1 ;
9 await sleep ( 128ms ) ;
10 v + = 1 ;
11 return v ;
12 }
13
14 fn main ( ) - > i32 {
15 var x : i32 ;
16
17 async fn foo ( ) - > void {
18 x = 22 ;
19 }
20
21 std : : concurrency : : block_on ( foo ( ) ) ;
22 io : : println ( "x = \ ( x ) " ) ;
23 return 0 ;
24 }

async { ... } : An anonymous Future .

It captures variables from the surrounding scope. Like async fn , it is lazy and it does not run until awaited. All tasks must be completed and handled before before the program will continue.

Example
0 fn main ( ) - > i32 {
1 var x : i32 ;
2
3 async {
4 x = await bar ( ) ;
5 }
6
7 if ( x ! = 42 ) {
8 return 1 ;
9 }
10
11 io : : println ( "x = \ ( x ) " ) ;
12 return 0 ;
13 }

Control Flow

`await` : Pauses the current function, yielding control back to the runtime until the awaited operation completes. `std::concurrency::block_on` : The bridge between synchronous and asynchronous code. It starts a temporary event loop to drive a future to completion. This is typically used only in main or unit tests. `std::concurrency::select` : Races multiple futures. It waits for the first * one to complete and cancels the others.

Example
0 let job = await std : : concurrency : : select ( [
1 // Case 1: We receive a message
2 channel . recv ( ) ,
3
4 // Case 2: We get tired of waiting (Timeout)
5 timer . after ( 500ms )
6 ] ) ;

Safety

Aela treats the scheduler as a "Safety System" rather than an aftermarket part. Because the runtime is integrated into the language, the compiler can provide stronger guarantees than if it took a library-based approach.

  1. Compile-Time Data-Race Prevention: The compiler knows the difference between a local async execution (single-threaded) and a task execution (multi-threaded). It enforces Send and Sync rules strictly at the task boundary.
  2. Single Blocking Bridge: A built-in runtime provides one, official way to handle blocking calls: std::task::run_blocking() . This prevents the "Color of your function" problem from causing deadlocks when libraries mix blocking/non-blocking strategies.
  3. Guaranteed Cleanup: task handles are tied to the runtime. If a Task handle is dropped, the language enforces a consistent policy (detachment), preventing undefined behavior or zombie threads common in ad-hoc library implementations.

Formal Verification

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.

  • #let (the ghost of...)
  • #requires (pre condition)
  • #ensures (post condition)
  • #invariant (can’t change)
  • #variant (must change)

All of them appear before the function/loop they describe. None of these exist at runtime. They’re for the verifier only.

#let — The ghost of...

#let defines a ghost name for an expression, purely for specs.

You can put it right before a function or loop to bind names used in the following specs:

Example
0 #let oldN = n;
1 #requires oldN >= 0;
2 #ensures result == oldN + 1;
3 fn addOne ( n : i32 ) - > result : i32 {
4 return n + 1 ;
5 }

Here:

  • oldN is only visible to the verifier.
  • oldN does not exist in the compiled code.
  • You can use oldN in #requires , #ensures , #invariant , #variant , etc.

Think of #let as: "Give me a ghost name for this value/expression so I can talk about it in my conditions."

#requires — Precondition

#requires means this must be true when we enter this function (or loop). Placed directly above the function:

Example
0 #requires n >= 0;
1 fn factorial ( n : i32 ) - > i32 {
2 // ...
3 }

The verifier checks every call to factorial proves n >= 0 and assumes n >= 0 inside the body. You can also (optionally) apply #requires to loops, if you want to state assumptions at loop entry:

Example
0 #requires n >= 0; // Here `#requires` is about the state at loop entry.
1 #invariant 0 <= i <= n;
2 #variant n - i;
3 while ( i < n ) {
4 i = i + 1 ;
5 }

#ensures — Postcondition

#ensures goes right before the function and says: "This will be true after this function returns."

Example:

Example
0 #requires n >= 0;
1 #ensures result >= n;
2 fn addOne ( n : i32 ) - > i32 {
3 return n + 1 ;
4 }

The verifier proves we assume n >= 0 on entry and the value returned by the function ( result ) always satisfies result >= n .

Summation

Example
0 pure fn sum_range ( a : i32 [ ] , start : i32 , end : i32 ) - > result : i32 {
1 var acc = 0 ;
2 var i = start ;
3 while ( i < end ) {
4 acc = acc + a [ i ] ;
5 i = i + 1 ;
6 }
7 result = acc ;
8 return ;
9 }
10
11 #requires for (let i in 0..std::length(a)) {
12 std : : assert ( a [ i ] > = 0 ) ;
13 }
14 #ensures result == sum_range(a, 0, std::length(a));
15 fn sum ( a : i32 [ ] ) - > result : i32 {
16
17 #let originalLength = std::length(a);
18 #let originalArray = a;
19
20 var total = 0 ;
21 var i = 0 ;
22
23 #invariant 0 <= i && i <= originalLength;
24 #invariant total == sum_range(originalArray, 0, i);
25 #variant originalLength - i;
26 while ( i < originalLength ) {
27 total = total + a [ i ] ;
28 i = i + 1 ;
29 }
30
31 result = total ;
32 return ;
33 }

#invariant

An invariant can’t change (must stay true during loop). It goes immediately before a loop:

Example
0 #invariant 0 <= i && i <= n;
1 while ( i < n ) {
2 i = i + 1 ;
3 }

This means:

  • Before the first iteration: 0 <= i <= n must hold.
  • After every iteration, if we go around again, it must still hold.
  • When the loop exits, 0 <= i <= n is still true.

The invariant describes what must not be broken across iterations. It’s not "the variable can’t change"; it’s this relationship must stay true even as variables change.

#variant

#variant also goes right before the loop , together with #invariant :

Example
0 #invariant 0 <= i && i <= n;
1 #variant n - i;
2 while i < n {
3 i = i + 1 ;
4 }

Here:

  • n - i is the variant expression .
  • The verifier proves:
  • n - i is always ≥ 0 inside the loop.
  • On every iteration that continues the loop, n - i gets strictly smaller .

This proves the loop must terminate .

So:

  • #invariant - this condition must keep holding.
  • #variant - this measure must go down when we repeat.

Your loop may increase i , but your #variant is something that decreases (like n - i ).

Examples

General rule: Directives attach to the next construct.

For functions

Example
0 #let oldN = n;
1 #requires oldN >= 0;
2 #ensures result == oldN + 1;
3 fn addOne ( n : i32 ) - > result : i32 {
4 return n + 1 ;
5 }

For loops

Example
0 #invariant 0 <= i && i <= n;
1 #variant n - i;
2 while ( i < n ) {
3 i = i + 1 ;
4 }

You can stack them:

Example
0 #let start = i;
1 #requires 0 <= i && i <= n;
2 #invariant 0 <= i && i <= n;
3 #variant n - i;
4 while ( i < n ) {
5 i = i + 1 ;
6 }

All of this is compile-time-only verification syntax , erased in the compiled program.

Cheat Sheet

  • #let name = expr - Ghost name for expr , only for use in specs.
  • #requires P - P must be true before we enter this function/loop.
  • #ensures Q - Q will be true when the function returns.
  • #invariant I - I must hold before the loop, after each iteration, and when it ends.
  • #variant V - V must strictly decrease each time we repeat the loop (and stay in a well-founded set).

All placed before the thing they describe.

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