Types

Quick Reference

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

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

Nominal & Parameterized Types

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

Example
0 struct Pair ( T , U ) {
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="1f4b8074e734f">
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 : int = 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 ( int ) = 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 : int in 0 . . 10 ) {
1 print ( " { } " , i ) ;
2 }
3
4 for ( var x : string , var y : string in lines ) {
5 print ( " { } { } " , x , y ) ;
6 }

4. match Expression

Syntax

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

Description

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

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

Example

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

5. return

Syntax

Example
0 return [ ] ;

Description

Exits a function immediately with an optional return value.

Examples

Example
0 return ;
1 return x + 1 ;

6. break

Syntax

Example
0 break ;

Description

Terminates the nearest enclosing loop.

7. continue

Syntax

Example
0 continue ;

Description

Skips to the next iteration of the nearest enclosing loop.

8. Blocks and Statement Composition

Syntax

Example
0 {
1 ;
2 . . .
3 }

Description

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

Example

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

9. Expression Statements

Syntax

Example
0 ;

Description

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

Example

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

Optional

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

Declaring an Optional Type

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

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

Creating Optional Values

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

None : The Empty State

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

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

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

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

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

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

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

Using Optional Values

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

Optional Chaining (?.)

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

The result of an optional chain is always another optional.

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

Explicit Checking (Match Statement)

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

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

Interfaces

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

Overview

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

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

The core philosophy is:

Interfaces define abstract contracts or capabilities.

Structs define concrete data structures.

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

Components

The interface Declaration

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

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

Rules:

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

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

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

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

The struct Declaration

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

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

Rules:

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

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

The impl...as... Declaration

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

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

Rules:

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

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

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

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

Interface Types

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

Syntax: &

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

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

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

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

Duration & Instant

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

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

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

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

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

Literals & type

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

Unary

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

Binary with Duration

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

Float scalars

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

Comparison

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

Instant

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

Casting / construction

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

Overflow & division semantics

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

Examples

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

Suffix/literal interaction (clarity)

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

Arenas

Overview

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

The model consists of:

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

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

The Arena

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

Syntax

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

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

Semantics

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

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

The reserve Statement (Transactional Reservation)

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

Syntax

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

Semantics

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

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

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

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

The new Keyword (Allocation)

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

Semantics

The compiler enforces three distinct behaviors for new:

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

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

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

Complete Bare-Metal Example

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

Buffers

Introduction

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

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

Core Concepts

Representation

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

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

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

Ownership

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

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

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

The Intrinsic API

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

std::buffer::alloc

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

std::buffer::write

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

std::buffer::read

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

std::buffer::capacity

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

std::buffer::drop

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

std::buffer::view

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

std::buffer::slice

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

The Safety Model: A Layered Approach

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

Layer 1: The Unsafe Buffer(T) Primitive

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

Layer 2: Compile-Time Safety via the Verifier

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

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

These checks happen entirely at compile time.

Layer 3: Provable Safety via Refinement Types

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

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

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

This system provides two powerful benefits:

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

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

Concurrency

Integrated vs. Aftermarket Most languages treat the scheduler as a library. Aela treats it as a language feature. This allows the compiler to enforce safety rules that libraries cannot.

Aela's concurrency 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 Keywords: async and task

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

async

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

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

task

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

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

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

Defining and Spawning Tasks

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

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

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

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

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

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

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

Structured Parallelism: task { ... } Block

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

Example
0 async fn get_dashboard_data ( ) - > Dashboard {
1 var user : User ;
2 var orders : [ Order ] ;
3
4 task { // work stealing scheduler
5 let user_task = fetch_user_profile ( 123 ) ;
6 let orders_task = fetch_recent_orders ( 123 ) ;
7
8 // This `await` does NOT block the OS thread.
9 // The runtime parks this function and lets the thread
10 // handle another request immediately.
11 user = await user_task ;
12 orders = await orders_task ;
13
14 } // the block waits at the end
15
16 // This line is only reached after BOTH tasks are complete.
17 return Dashboard ( user , orders ) ;
18 }

Integrated Vehicle Safety vs. Aftermarket Parts

Think of it like the safety systems in a car.

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

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

Here are the specific safety wins this integration provides.

  1. Compile-Time Data-Race Prevention

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

  1. A Single, Safe Bridge for Blocking Code

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

  1. Guaranteed Structured Concurrency

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

  1. Predictable Task Lifecycles

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

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

Pool Control & Oversubscription

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

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

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

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

Example
0 async fn io_bound_work1 ( ) - > int { // This already works
1 await doSomething ( ) ; // This already works
2 return 0 ;
3 }
4
5 task fn compute_bound_work1 ( c : & Channel ( int ) ) - > int {
6 c . send ( 42 ) ; // some new data is ready
7 return 0 ; // the task is finished
8 }
9
10 async fn main ( ) {
11 let c : Channel ( int ) = new { } ;
12
13 c . receive = fn ( i : int ) - > {
14 io : : print ( " { } " , i ) ;
15 } ;
16
17 let h = await compute_bound_work1 ( c ) ; // Actually returns `Task(int)` and resolves it
18 let i = await io_bound_work1 ( ) ; // Actually returns `Future(int)` and resolves it
19
20 let a : int [ ] = await std : : all ( [
21 compute_bound_work2 ( ) , // dispatched to the scheduler
22 compute_bound_work2 ( ) , // dispatched to the scheduler
23 io_bound_work2 ( ) // dispatched to the eventloop
24 ] ) ;
25
26 let h2 = compute_bound_work1 ( c ) ; // returns `Task(int)` lazy, not executing.
27 let i2 = await h2 ; // starts executing (ie, `await compute_bound_work1();`)
28 }
Example
0 enum RecvJob {
1 Msg ( Job ) ,
2 Cancelled ,
3 Timeout
4 }
5
6 async fn next_recv ( jobs : & Channel ( Job ) ) - > RecvJob {
7 let j : Job = await jobs . recv ( ) ;
8 return RecvJob : : Msg ( j ) ;
9 }
10
11 async fn on_cancel ( tok : & CancellationToken ) - > RecvJob {
12 await tok . cancelled ( ) ;
13 return RecvJob : : Cancelled ;
14 }
15
16 async fn after ( ms : Int ) - > RecvJob {
17 await timer . after_ms ( ms ) ;
18 return RecvJob : : Timeout ;
19 }
20
21 task fn handle ( job : Job ) - > int {
22 // something interesting and cpu intensive
23 return 0 ;
24 }
25
26 async fn run_worker ( jobs : & Channel , tok : & CancellationToken ) {
27 while ( true ) {
28 // `select` accepts a list of Futures.
29 // It polls them all. The first to complete wins; the others are cancelled.
30
31 let evt = await std : : select ( [
32 // Case 1: A message arrives
33 next_recv ( jobs ) ,
34
35 // Case 2: The token is cancelled
36 tok . on_cancelled ( ) ,
37
38 // Case 3: Timeout
39 timer . after ( 250ms )
40 ] ) ;
41
42 match ( evt ) {
43 RecvJob : : Msg ( job ) = > {
44 // Spin up a CPU task to handle the job
45 spawn handle_job ( job ) ;
46 }
47 RecvJob : : Cancelled = > { break ; }
48 RecvJob : : Timeout = > { io : : print ( "tick" ) ; }
49 }
50 }
51 }

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

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

Nominal & Parameterized Types

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

Example
0 struct Pair ( T , U ) {
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="1f4b8074e734f">
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 : int = 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 ( int ) = 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 : int in 0 . . 10 ) {
1 print ( " { } " , i ) ;
2 }
3
4 for ( var x : string , var y : string in lines ) {
5 print ( " { } { } " , x , y ) ;
6 }

4. match Expression

Syntax

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

Description

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

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

Example

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

5. return

Syntax

Example
0 return [ ] ;

Description

Exits a function immediately with an optional return value.

Examples

Example
0 return ;
1 return x + 1 ;

6. break

Syntax

Example
0 break ;

Description

Terminates the nearest enclosing loop.

7. continue

Syntax

Example
0 continue ;

Description

Skips to the next iteration of the nearest enclosing loop.

8. Blocks and Statement Composition

Syntax

Example
0 {
1 ;
2 . . .
3 }

Description

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

Example

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

9. Expression Statements

Syntax

Example
0 ;

Description

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

Example

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

Optional

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

Declaring an Optional Type

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

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

Creating Optional Values

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

None : The Empty State

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

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

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

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

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

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

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

Using Optional Values

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

Optional Chaining (?.)

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

The result of an optional chain is always another optional.

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

Explicit Checking (Match Statement)

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

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

Interfaces

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

Overview

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

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

The core philosophy is:

Interfaces define abstract contracts or capabilities.

Structs define concrete data structures.

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

Components

The interface Declaration

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

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

Rules:

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

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

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

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

The struct Declaration

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

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

Rules:

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

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

The impl...as... Declaration

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

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

Rules:

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

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

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

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

Interface Types

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

Syntax: &

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

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

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

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

Duration & Instant

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

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

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

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

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

Literals & type

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

Unary

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

Binary with Duration

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

Float scalars

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

Comparison

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

Instant

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

Casting / construction

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

Overflow & division semantics

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

Examples

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

Suffix/literal interaction (clarity)

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

Arenas

Overview

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

The model consists of:

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

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

The Arena

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

Syntax

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

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

Semantics

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

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

The reserve Statement (Transactional Reservation)

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

Syntax

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

Semantics

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

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

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

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

The new Keyword (Allocation)

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

Semantics

The compiler enforces three distinct behaviors for new:

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

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

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

Complete Bare-Metal Example

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

Buffers

Introduction

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

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

Core Concepts

Representation

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

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

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

Ownership

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

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

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

The Intrinsic API

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

std::buffer::alloc

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

std::buffer::write

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

std::buffer::read

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

std::buffer::capacity

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

std::buffer::drop

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

std::buffer::view

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

std::buffer::slice

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

The Safety Model: A Layered Approach

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

Layer 1: The Unsafe Buffer(T) Primitive

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

Layer 2: Compile-Time Safety via the Verifier

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

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

These checks happen entirely at compile time.

Layer 3: Provable Safety via Refinement Types

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

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

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

This system provides two powerful benefits:

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

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

Concurrency

Integrated vs. Aftermarket Most languages treat the scheduler as a library. Aela treats it as a language feature. This allows the compiler to enforce safety rules that libraries cannot.

Aela's concurrency 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 Keywords: async and task

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

async

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

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

task

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

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

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

Defining and Spawning Tasks

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

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

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

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

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

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

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

Structured Parallelism: task { ... } Block

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

Example
0 async fn get_dashboard_data ( ) - > Dashboard {
1 var user : User ;
2 var orders : [ Order ] ;
3
4 task { // work stealing scheduler
5 let user_task = fetch_user_profile ( 123 ) ;
6 let orders_task = fetch_recent_orders ( 123 ) ;
7
8 // This `await` does NOT block the OS thread.
9 // The runtime parks this function and lets the thread
10 // handle another request immediately.
11 user = await user_task ;
12 orders = await orders_task ;
13
14 } // the block waits at the end
15
16 // This line is only reached after BOTH tasks are complete.
17 return Dashboard ( user , orders ) ;
18 }

Integrated Vehicle Safety vs. Aftermarket Parts

Think of it like the safety systems in a car.

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

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

Here are the specific safety wins this integration provides.

  1. Compile-Time Data-Race Prevention

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

  1. A Single, Safe Bridge for Blocking Code

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

  1. Guaranteed Structured Concurrency

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

  1. Predictable Task Lifecycles

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

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

Pool Control & Oversubscription

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

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

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

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

Example
0 async fn io_bound_work1 ( ) - > int { // This already works
1 await doSomething ( ) ; // This already works
2 return 0 ;
3 }
4
5 task fn compute_bound_work1 ( c : & Channel ( int ) ) - > int {
6 c . send ( 42 ) ; // some new data is ready
7 return 0 ; // the task is finished
8 }
9
10 async fn main ( ) {
11 let c : Channel ( int ) = new { } ;
12
13 c . receive = fn ( i : int ) - > {
14 io : : print ( " { } " , i ) ;
15 } ;
16
17 let h = await compute_bound_work1 ( c ) ; // Actually returns `Task(int)` and resolves it
18 let i = await io_bound_work1 ( ) ; // Actually returns `Future(int)` and resolves it
19
20 let a : int [ ] = await std : : all ( [
21 compute_bound_work2 ( ) , // dispatched to the scheduler
22 compute_bound_work2 ( ) , // dispatched to the scheduler
23 io_bound_work2 ( ) // dispatched to the eventloop
24 ] ) ;
25
26 let h2 = compute_bound_work1 ( c ) ; // returns `Task(int)` lazy, not executing.
27 let i2 = await h2 ; // starts executing (ie, `await compute_bound_work1();`)
28 }
Example
0 enum RecvJob {
1 Msg ( Job ) ,
2 Cancelled ,
3 Timeout
4 }
5
6 async fn next_recv ( jobs : & Channel ( Job ) ) - > RecvJob {
7 let j : Job = await jobs . recv ( ) ;
8 return RecvJob : : Msg ( j ) ;
9 }
10
11 async fn on_cancel ( tok : & CancellationToken ) - > RecvJob {
12 await tok . cancelled ( ) ;
13 return RecvJob : : Cancelled ;
14 }
15
16 async fn after ( ms : Int ) - > RecvJob {
17 await timer . after_ms ( ms ) ;
18 return RecvJob : : Timeout ;
19 }
20
21 task fn handle ( job : Job ) - > int {
22 // something interesting and cpu intensive
23 return 0 ;
24 }
25
26 async fn run_worker ( jobs : & Channel , tok : & CancellationToken ) {
27 while ( true ) {
28 // `select` accepts a list of Futures.
29 // It polls them all. The first to complete wins; the others are cancelled.
30
31 let evt = await std : : select ( [
32 // Case 1: A message arrives
33 next_recv ( jobs ) ,
34
35 // Case 2: The token is cancelled
36 tok . on_cancelled ( ) ,
37
38 // Case 3: Timeout
39 timer . after ( 250ms )
40 ] ) ;
41
42 match ( evt ) {
43 RecvJob : : Msg ( job ) = > {
44 // Spin up a CPU task to handle the job
45 spawn handle_job ( job ) ;
46 }
47 RecvJob : : Cancelled = > { break ; }
48 RecvJob : : Timeout = > { io : : print ( "tick" ) ; }
49 }
50 }
51 }

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

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

Nominal & Parameterized Types

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

Example
0 struct Pair ( T , U ) {
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="1f4b8074e734f">
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 : int = 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 ( int ) = 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 : int in 0 . . 10 ) {
1 print ( " { } " , i ) ;
2 }
3
4 for ( var x : string , var y : string in lines ) {
5 print ( " { } { } " , x , y ) ;
6 }

4. match Expression

Syntax

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

Description

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

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

Example

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

5. return

Syntax

Example
0 return [ ] ;

Description

Exits a function immediately with an optional return value.

Examples

Example
0 return ;
1 return x + 1 ;

6. break

Syntax

Example
0 break ;

Description

Terminates the nearest enclosing loop.

7. continue

Syntax

Example
0 continue ;

Description

Skips to the next iteration of the nearest enclosing loop.

8. Blocks and Statement Composition

Syntax

Example
0 {
1 ;
2 . . .
3 }

Description

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

Example

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

9. Expression Statements

Syntax

Example
0 ;

Description

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

Example

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

Optional

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

Declaring an Optional Type

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

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

Creating Optional Values

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

None : The Empty State

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

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

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

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

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

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

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

Using Optional Values

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

Optional Chaining (?.)

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

The result of an optional chain is always another optional.

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

Explicit Checking (Match Statement)

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

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

Interfaces

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

Overview

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

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

The core philosophy is:

Interfaces define abstract contracts or capabilities.

Structs define concrete data structures.

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

Components

The interface Declaration

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

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

Rules:

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

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

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

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

The struct Declaration

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

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

Rules:

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

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

The impl...as... Declaration

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

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

Rules:

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

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

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

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

Interface Types

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

Syntax: &

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

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

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

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

Duration & Instant

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

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

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

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

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

Literals & type

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

Unary

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

Binary with Duration

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

Float scalars

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

Comparison

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

Instant

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

Casting / construction

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

Overflow & division semantics

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

Examples

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

Suffix/literal interaction (clarity)

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

Arenas

Overview

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

The model consists of:

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

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

The Arena

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

Syntax

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

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

Semantics

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

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

The reserve Statement (Transactional Reservation)

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

Syntax

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

Semantics

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

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

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

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

The new Keyword (Allocation)

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

Semantics

The compiler enforces three distinct behaviors for new:

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

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

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

Complete Bare-Metal Example

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

Buffers

Introduction

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

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

Core Concepts

Representation

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

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

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

Ownership

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

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

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

The Intrinsic API

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

std::buffer::alloc

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

std::buffer::write

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

std::buffer::read

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

std::buffer::capacity

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

std::buffer::drop

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

std::buffer::view

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

std::buffer::slice

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

The Safety Model: A Layered Approach

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

Layer 1: The Unsafe Buffer(T) Primitive

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

Layer 2: Compile-Time Safety via the Verifier

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

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

These checks happen entirely at compile time.

Layer 3: Provable Safety via Refinement Types

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

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

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

This system provides two powerful benefits:

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

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

Concurrency

Integrated vs. Aftermarket Most languages treat the scheduler as a library. Aela treats it as a language feature. This allows the compiler to enforce safety rules that libraries cannot.

Aela's concurrency 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 Keywords: async and task

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

async

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

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

task

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

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

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

Defining and Spawning Tasks

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

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

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

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

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

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

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

Structured Parallelism: task { ... } Block

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

Example
0 async fn get_dashboard_data ( ) - > Dashboard {
1 var user : User ;
2 var orders : [ Order ] ;
3
4 task { // work stealing scheduler
5 let user_task = fetch_user_profile ( 123 ) ;
6 let orders_task = fetch_recent_orders ( 123 ) ;
7
8 // This `await` does NOT block the OS thread.
9 // The runtime parks this function and lets the thread
10 // handle another request immediately.
11 user = await user_task ;
12 orders = await orders_task ;
13
14 } // the block waits at the end
15
16 // This line is only reached after BOTH tasks are complete.
17 return Dashboard ( user , orders ) ;
18 }

Integrated Vehicle Safety vs. Aftermarket Parts

Think of it like the safety systems in a car.

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

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

Here are the specific safety wins this integration provides.

  1. Compile-Time Data-Race Prevention

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

  1. A Single, Safe Bridge for Blocking Code

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

  1. Guaranteed Structured Concurrency

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

  1. Predictable Task Lifecycles

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

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

Pool Control & Oversubscription

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

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

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

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

Example
0 async fn io_bound_work1 ( ) - > int { // This already works
1 await doSomething ( ) ; // This already works
2 return 0 ;
3 }
4
5 task fn compute_bound_work1 ( c : & Channel ( int ) ) - > int {
6 c . send ( 42 ) ; // some new data is ready
7 return 0 ; // the task is finished
8 }
9
10 async fn main ( ) {
11 let c : Channel ( int ) = new { } ;
12
13 c . receive = fn ( i : int ) - > {
14 io : : print ( " { } " , i ) ;
15 } ;
16
17 let h = await compute_bound_work1 ( c ) ; // Actually returns `Task(int)` and resolves it
18 let i = await io_bound_work1 ( ) ; // Actually returns `Future(int)` and resolves it
19
20 let a : int [ ] = await std : : all ( [
21 compute_bound_work2 ( ) , // dispatched to the scheduler
22 compute_bound_work2 ( ) , // dispatched to the scheduler
23 io_bound_work2 ( ) // dispatched to the eventloop
24 ] ) ;
25
26 let h2 = compute_bound_work1 ( c ) ; // returns `Task(int)` lazy, not executing.
27 let i2 = await h2 ; // starts executing (ie, `await compute_bound_work1();`)
28 }
Example
0 enum RecvJob {
1 Msg ( Job ) ,
2 Cancelled ,
3 Timeout
4 }
5
6 async fn next_recv ( jobs : & Channel ( Job ) ) - > RecvJob {
7 let j : Job = await jobs . recv ( ) ;
8 return RecvJob : : Msg ( j ) ;
9 }
10
11 async fn on_cancel ( tok : & CancellationToken ) - > RecvJob {
12 await tok . cancelled ( ) ;
13 return RecvJob : : Cancelled ;
14 }
15
16 async fn after ( ms : Int ) - > RecvJob {
17 await timer . after_ms ( ms ) ;
18 return RecvJob : : Timeout ;
19 }
20
21 task fn handle ( job : Job ) - > int {
22 // something interesting and cpu intensive
23 return 0 ;
24 }
25
26 async fn run_worker ( jobs : & Channel , tok : & CancellationToken ) {
27 while ( true ) {
28 // `select` accepts a list of Futures.
29 // It polls them all. The first to complete wins; the others are cancelled.
30
31 let evt = await std : : select ( [
32 // Case 1: A message arrives
33 next_recv ( jobs ) ,
34
35 // Case 2: The token is cancelled
36 tok . on_cancelled ( ) ,
37
38 // Case 3: Timeout
39 timer . after ( 250ms )
40 ] ) ;
41
42 match ( evt ) {
43 RecvJob : : Msg ( job ) = > {
44 // Spin up a CPU task to handle the job
45 spawn handle_job ( job ) ;
46 }
47 RecvJob : : Cancelled = > { break ; }
48 RecvJob : : Timeout = > { io : : print ( "tick" ) ; }
49 }
50 }
51 }

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

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

Nominal & Parameterized Types

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

Example
0 struct Pair ( T , U ) {
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="1f4b8074e734f">
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 : int = 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 ( int ) = 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 : int in 0 . . 10 ) {
1 print ( " { } " , i ) ;
2 }
3
4 for ( var x : string , var y : string in lines ) {
5 print ( " { } { } " , x , y ) ;
6 }

4. match Expression

Syntax

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

Description

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

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

Example

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

5. return

Syntax

Example
0 return [ ] ;

Description

Exits a function immediately with an optional return value.

Examples

Example
0 return ;
1 return x + 1 ;

6. break

Syntax

Example
0 break ;

Description

Terminates the nearest enclosing loop.

7. continue

Syntax

Example
0 continue ;

Description

Skips to the next iteration of the nearest enclosing loop.

8. Blocks and Statement Composition

Syntax

Example
0 {
1 ;
2 . . .
3 }

Description

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

Example

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

9. Expression Statements

Syntax

Example
0 ;

Description

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

Example

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

Optional

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

Declaring an Optional Type

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

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

Creating Optional Values

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

None : The Empty State

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

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

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

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

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

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

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

Using Optional Values

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

Optional Chaining (?.)

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

The result of an optional chain is always another optional.

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

Explicit Checking (Match Statement)

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

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

Interfaces

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

Overview

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

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

The core philosophy is:

Interfaces define abstract contracts or capabilities.

Structs define concrete data structures.

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

Components

The interface Declaration

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

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

Rules:

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

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

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

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

The struct Declaration

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

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

Rules:

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

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

The impl...as... Declaration

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

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

Rules:

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

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

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

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

Interface Types

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

Syntax: &

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

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

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

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

Duration & Instant

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

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

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

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

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

Literals & type

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

Unary

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

Binary with Duration

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

Float scalars

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

Comparison

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

Instant

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

Casting / construction

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

Overflow & division semantics

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

Examples

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

Suffix/literal interaction (clarity)

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

Arenas

Overview

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

The model consists of:

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

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

The Arena

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

Syntax

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

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

Semantics

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

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

The reserve Statement (Transactional Reservation)

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

Syntax

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

Semantics

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

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

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

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

The new Keyword (Allocation)

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

Semantics

The compiler enforces three distinct behaviors for new:

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

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

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

Complete Bare-Metal Example

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

Buffers

Introduction

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

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

Core Concepts

Representation

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

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

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

Ownership

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

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

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

The Intrinsic API

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

std::buffer::alloc

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

std::buffer::write

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

std::buffer::read

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

std::buffer::capacity

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

std::buffer::drop

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

std::buffer::view

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

std::buffer::slice

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

The Safety Model: A Layered Approach

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

Layer 1: The Unsafe Buffer(T) Primitive

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

Layer 2: Compile-Time Safety via the Verifier

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

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

These checks happen entirely at compile time.

Layer 3: Provable Safety via Refinement Types

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

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

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

This system provides two powerful benefits:

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

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

Concurrency

Integrated vs. Aftermarket Most languages treat the scheduler as a library. Aela treats it as a language feature. This allows the compiler to enforce safety rules that libraries cannot.

Aela's concurrency 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 Keywords: async and task

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

async

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

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

task

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

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

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

Defining and Spawning Tasks

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

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

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

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

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

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

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

Structured Parallelism: task { ... } Block

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

Example
0 async fn get_dashboard_data ( ) - > Dashboard {
1 var user : User ;
2 var orders : [ Order ] ;
3
4 task { // work stealing scheduler
5 let user_task = fetch_user_profile ( 123 ) ;
6 let orders_task = fetch_recent_orders ( 123 ) ;
7
8 // This `await` does NOT block the OS thread.
9 // The runtime parks this function and lets the thread
10 // handle another request immediately.
11 user = await user_task ;
12 orders = await orders_task ;
13
14 } // the block waits at the end
15
16 // This line is only reached after BOTH tasks are complete.
17 return Dashboard ( user , orders ) ;
18 }

Integrated Vehicle Safety vs. Aftermarket Parts

Think of it like the safety systems in a car.

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

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

Here are the specific safety wins this integration provides.

  1. Compile-Time Data-Race Prevention

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

  1. A Single, Safe Bridge for Blocking Code

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

  1. Guaranteed Structured Concurrency

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

  1. Predictable Task Lifecycles

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

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

Pool Control & Oversubscription

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

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

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

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

Example
0 async fn io_bound_work1 ( ) - > int { // This already works
1 await doSomething ( ) ; // This already works
2 return 0 ;
3 }
4
5 task fn compute_bound_work1 ( c : & Channel ( int ) ) - > int {
6 c . send ( 42 ) ; // some new data is ready
7 return 0 ; // the task is finished
8 }
9
10 async fn main ( ) {
11 let c : Channel ( int ) = new { } ;
12
13 c . receive = fn ( i : int ) - > {
14 io : : print ( " { } " , i ) ;
15 } ;
16
17 let h = await compute_bound_work1 ( c ) ; // Actually returns `Task(int)` and resolves it
18 let i = await io_bound_work1 ( ) ; // Actually returns `Future(int)` and resolves it
19
20 let a : int [ ] = await std : : all ( [
21 compute_bound_work2 ( ) , // dispatched to the scheduler
22 compute_bound_work2 ( ) , // dispatched to the scheduler
23 io_bound_work2 ( ) // dispatched to the eventloop
24 ] ) ;
25
26 let h2 = compute_bound_work1 ( c ) ; // returns `Task(int)` lazy, not executing.
27 let i2 = await h2 ; // starts executing (ie, `await compute_bound_work1();`)
28 }
Example
0 enum RecvJob {
1 Msg ( Job ) ,
2 Cancelled ,
3 Timeout
4 }
5
6 async fn next_recv ( jobs : & Channel ( Job ) ) - > RecvJob {
7 let j : Job = await jobs . recv ( ) ;
8 return RecvJob : : Msg ( j ) ;
9 }
10
11 async fn on_cancel ( tok : & CancellationToken ) - > RecvJob {
12 await tok . cancelled ( ) ;
13 return RecvJob : : Cancelled ;
14 }
15
16 async fn after ( ms : Int ) - > RecvJob {
17 await timer . after_ms ( ms ) ;
18 return RecvJob : : Timeout ;
19 }
20
21 task fn handle ( job : Job ) - > int {
22 // something interesting and cpu intensive
23 return 0 ;
24 }
25
26 async fn run_worker ( jobs : & Channel , tok : & CancellationToken ) {
27 while ( true ) {
28 // `select` accepts a list of Futures.
29 // It polls them all. The first to complete wins; the others are cancelled.
30
31 let evt = await std : : select ( [
32 // Case 1: A message arrives
33 next_recv ( jobs ) ,
34
35 // Case 2: The token is cancelled
36 tok . on_cancelled ( ) ,
37
38 // Case 3: Timeout
39 timer . after ( 250ms )
40 ] ) ;
41
42 match ( evt ) {
43 RecvJob : : Msg ( job ) = > {
44 // Spin up a CPU task to handle the job
45 spawn handle_job ( job ) ;
46 }
47 RecvJob : : Cancelled = > { break ; }
48 RecvJob : : Timeout = > { io : : print ( "tick" ) ; }
49 }
50 }
51 }

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