Language Tour

A quick introduction to Joule's syntax and features through examples.

Variables

Variables are immutable by default. Use mut for mutable bindings.

let x = 42;              // immutable, type inferred as i32
let name: String = "Jo"; // explicit type annotation
let mut count = 0;        // mutable
count = count + 1;

Primitive Types

let a: i8  = -128;        // signed integers: i8, i16, i32, i64, isize
let b: u32 = 42;          // unsigned integers: u8, u16, u32, u64, usize
let c: f64 = 3.14159;     // floats: f16, bf16, f32, f64
let d: bool = true;       // boolean
let e: char = 'A';        // unicode character
let s: String = "hello";  // string
let h: f16 = 0.5f16;      // half-precision (ML inference, signal processing)
let g: bf16 = 0.001bf16;  // brain float (ML training)

Functions

// Basic function with parameters and return type
fn add(a: i32, b: i32) -> i32 {
    a + b   // last expression is the return value
}

// Public function (visible outside the module)
pub fn greet(name: String) {
    println!("Hello, {}", name);
}

// Mutable self parameter for methods that modify state
fn advance(mut self) -> Token {
    let token = self.peek();
    self.pos = self.pos + 1;
    token
}

Structs

pub struct Point {
    pub x: f64,
    pub y: f64,
}

// Construction
let p = Point { x: 3.0, y: 4.0 };

// Field access
let dist = p.x * p.x + p.y * p.y;

Impl Blocks

Methods are defined in impl blocks, separate from the struct definition.

impl Point {
    // Associated function (constructor)
    pub fn new(x: f64, y: f64) -> Point {
        Point { x, y }
    }

    // Method on self
    pub fn distance(self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }

    // Mutable method
    pub fn translate(mut self, dx: f64, dy: f64) {
        self.x = self.x + dx;
        self.y = self.y + dy;
    }
}

let p = Point::new(3.0, 4.0);
let d = p.distance();

Enums

Enums can hold data in each variant, making them sum types (tagged unions).

pub enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
    Triangle { base: f64, height: f64 },
}

let s = Shape::Circle { radius: 5.0 };

Pattern Matching

match is exhaustive -- the compiler ensures you handle every variant.

fn area(shape: Shape) -> f64 {
    match shape {
        Shape::Circle { radius } => {
            3.14159 * radius * radius
        }
        Shape::Rectangle { width, height } => {
            width * height
        }
        Shape::Triangle { base, height } => {
            0.5 * base * height
        }
    }
}

Match with a wildcard:

match token.kind {
    TokenKind::Fn => parse_function(),
    TokenKind::Struct => parse_struct(),
    TokenKind::Enum => parse_enum(),
    _ => parse_expression(),
}

Or Patterns

Match multiple alternatives in a single arm:

match x {
    1 | 2 | 3 => "small",
    4 | 5 | 6 => "medium",
    _ => "large",
}

Range Patterns

Match a range of values:

match score {
    0..=59 => "F",
    60..=69 => "D",
    70..=79 => "C",
    80..=89 => "B",
    90..=100 => "A",
    _ => "invalid",
}

Guard Clauses

Add conditions to match arms:

match value {
    x if x > 0 => "positive",
    x if x < 0 => "negative",
    _ => "zero",
}

Control Flow

// if-else (these are expressions -- they return values)
let max = if a > b { a } else { b };

// while loop
let mut i = 0;
while i < 10 {
    i = i + 1;
}

// for loop
for item in items {
    process(item);
}

// loop (infinite, break to exit)
loop {
    if done() {
        break;
    }
}

Option and Result

Option<T> represents a value that may or may not exist. Result<T, E> represents an operation that can succeed or fail.

// Option
fn find(items: Vec<i32>, target: i32) -> Option<usize> {
    let mut i = 0;
    while i < items.len() {
        if items[i] == target {
            return Option::Some(i);
        }
        i = i + 1;
    }
    Option::None
}

// Handling an Option
match find(items, 42) {
    Option::Some(index) => println!("Found at {}", index),
    Option::None => println!("Not found"),
}

// Result
fn parse_number(s: String) -> Result<i32, String> {
    // ...
    Result::Ok(42)
}

match parse_number(input) {
    Result::Ok(n) => println!("Got: {}", n),
    Result::Err(e) => println!("Error: {}", e),
}

Generics

Functions and types can be parameterized over types.

pub struct Pair<A, B> {
    pub first: A,
    pub second: B,
}

fn swap<A, B>(pair: Pair<A, B>) -> Pair<B, A> {
    Pair { first: pair.second, second: pair.first }
}

Traits

Traits define shared behavior. Types implement traits with impl.

pub trait Display {
    fn to_string(self) -> String;
}

impl Display for Point {
    fn to_string(self) -> String {
        "(" + self.x.to_string() + ", " + self.y.to_string() + ")"
    }
}

Collections

// Vec -- dynamic array
let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);
v.push(3);
let first = v[0];       // indexing
let len = v.len();       // length

// HashMap -- key-value store
let mut map: HashMap<String, i32> = HashMap::new();
map.insert("alice", 42);
map.insert("bob", 17);

Closures

Anonymous functions that can capture variables from their enclosing scope:

let double = |x: i32| -> i32 { x * 2 };
let result = double(21);  // 42

// Closures capture variables
let multiplier = 3;
let multiply = |x: i32| -> i32 { x * multiplier };

Range-Based For Loops

Iterate over numeric ranges with ..:

// Exclusive range: 0, 1, 2, ..., 9
for i in 0..10 {
    println!("{}", i);
}

// Use in accumulation
let mut sum = 0;
for i in 1..101 {
    sum = sum + i;
}
// sum = 5050

Iterator Methods

Vec supports functional-style iterator methods:

let numbers = vec![1, 2, 3, 4, 5];

// Transform elements
let doubled = numbers.map(|x: i32| -> i32 { x * 2 });

// Filter elements
let evens = numbers.filter(|x: i32| -> bool { x % 2 == 0 });

// Check conditions
let has_negative = numbers.any(|x: i32| -> bool { x < 0 });
let all_positive = numbers.all(|x: i32| -> bool { x > 0 });

// Reduce to single value
let sum = numbers.fold(0, |acc: i32, x: i32| -> i32 { acc + x });

Option and Result Methods

Rich combinator APIs for safe value handling:

let opt: Option<i32> = Option::Some(42);

// Query
let is_there = opt.is_some();     // true
let is_empty = opt.is_none();     // false

// Extract with default
let val = opt.unwrap_or(0);       // 42

// Transform
let doubled = opt.map(|x: i32| -> i32 { x * 2 });  // Some(84)

// Chain operations
let result = opt.and_then(|x: i32| -> Option<i32> {
    if x > 0 { Option::Some(x * 10) } else { Option::None }
});

Pipe Operator

The pipe operator |> passes the result of the left expression as the first argument to the right function. It makes data transformation pipelines readable:

// Without pipe -- deeply nested calls
let result = to_uppercase(trim(read_file("data.txt")));

// With pipe -- reads left to right
let result = read_file("data.txt")
    |> trim
    |> to_uppercase;

// Works with closures and multi-argument functions
let processed = data
    |> filter(|x| x > 0)
    |> map(|x| x * 2)
    |> fold(0, |acc, x| acc + x);

Union Types

Union types allow a value to be one of several types, checked at compile time:

type JsonValue = i64 | f64 | String | bool | Vec<JsonValue>;

fn process(value: JsonValue) {
    match value {
        x: i64 => println!("integer: {}", x),
        x: f64 => println!("float: {}", x),
        s: String => println!("string: {}", s),
        b: bool => println!("bool: {}", b),
        arr: Vec<JsonValue> => println!("array of {}", arr.len()),
    }
}

Algebraic Effects

Effects declare the side effects a function may perform, tracked by the type system:

effect Log {
    fn log(message: String);
}

effect Fail {
    fn fail(reason: String) -> !;
}

fn process(data: Vec<u8>) -> Result<Output, Error> with Log, Fail {
    Log::log("Processing started");
    if data.is_empty() {
        Fail::fail("empty input");
    }
    // ...
}

Effects are handled at the call site:

handle process(data) {
    Log::log(msg) => {
        println!("[LOG] {}", msg);
        resume;
    }
    Fail::fail(reason) => {
        Result::Err(Error::new(reason))
    }
}

Supervisors

Supervisors manage the lifecycle of concurrent tasks with automatic restart strategies:

use std::concurrency::Supervisor;

let sup = Supervisor::new(RestartStrategy::OneForOne);

sup.spawn("worker-1", || {
    // If this task panics, only this task is restarted
    process_queue()
});

sup.spawn("worker-2", || {
    process_events()
});

sup.run();

Parallel For

Parallel iteration over collections with automatic work distribution:

// Parallel map over a vector
let results = parallel for item in data {
    heavy_computation(item)
};

// With explicit chunk size
let processed = parallel(chunk_size: 1024) for row in matrix {
    transform(row)
};

The compiler tracks energy consumption across all parallel branches and sums them for the total budget.

Computation Builders

Computation builders provide a monadic syntax for composing complex operations:

let result = async {
    let data = fetch(url).await;
    let parsed = parse(data).await;
    transform(parsed)
};

let query = query {
    from users
    where age > 18
    select name, email
    order_by name
};

Const Functions

Functions that can be evaluated at compile time:

const fn factorial(n: i32) -> i32 {
    if n <= 1 { 1 } else { n * factorial(n - 1) }
}

// Evaluated at compile time
const FACT_10: i32 = factorial(10);

Comptime Blocks

Execute arbitrary code at compile time:

comptime {
    let lookup = generate_lookup_table(256);
    // lookup is available as a constant in runtime code
}

Modules and Imports

// Import specific items
use crate::ast::{File, AstItem};
use std::collections::HashMap;

// Module declarations (loads from separate file)
mod lexer;      // loads from lexer.joule or lexer/mod.joule
mod parser;
mod typeck;

// Public module re-export
pub mod utils;

// Inline module
mod helpers {
    pub fn clamp(x: i32, lo: i32, hi: i32) -> i32 {
        if x < lo { lo } else if x > hi { hi } else { x }
    }
}

// Glob import from stdlib
use std::math::*;

Async/Await with Channels

Asynchronous programming with channels for communication:

use std::concurrency::{spawn, channel};

async fn fetch_and_process(url: String) -> Result<Data, Error> {
    let response = http::get(url).await?;
    let data = parse(response.body()).await?;
    Result::Ok(data)
}

// Bounded channels for backpressure
let (tx, rx) = channel(capacity: 100);

spawn(|| {
    for item in source {
        tx.send(item);
    }
});

while let Option::Some(item) = rx.recv() {
    process(item);
}

Smart Pointers

Manage shared ownership and heap allocation:

// Box — heap allocation, required for recursive types
let b = Box::new(42);

// Rc — single-threaded shared ownership
let shared = Rc::new(vec![1, 2, 3]);
let copy = shared.clone();   // reference count +1

// Arc — thread-safe shared ownership
let data = Arc::new(vec![1, 2, 3]);
spawn(|| { let local = data.clone(); });

// Cow — clone-on-write (free reads, allocate on mutation)
let text = Cow::borrowed("hello");

See Smart Pointers for full documentation.

Const-Generic Types

Types with compile-time integer parameters:

// SmallVec — inline buffer, heap only when overflow
let mut sv: SmallVec[i32; 8] = SmallVec::new();
sv.push(42);    // inline — no heap allocation

// Simd — portable SIMD vectors
let a: Simd[f32; 4] = Simd::from_array([1.0, 2.0, 3.0, 4.0]);
let b: Simd[f32; 4] = Simd::splat(2.0);
let c = a.mul(&b);  // [2.0, 4.0, 6.0, 8.0] — single instruction

// NDArray — multi-dimensional arrays
let mat: NDArray[f64; 2] = NDArray::zeros([3, 4]);
let val = mat[1, 2];

Box (Heap Allocation)

Box<T> puts data on the heap. Required for recursive types.

pub enum Expr {
    Literal(i32),
    Add {
        left: Box<Expr>,
        right: Box<Expr>,
    },
}

Type Aliases

pub type Token = Spanned<TokenKind>;
pub type ParseResult<T> = Result<T, ParseError>;

Energy Budgets

Joule's defining feature. Declare the maximum energy a function is allowed to consume:

#[energy_budget(max_joules = 0.0001)]
fn efficient_add(x: i32, y: i32) -> i32 {
    x + y
}

The compiler estimates energy consumption at compile time. If a function exceeds its budget, compilation fails.

Power and thermal budgets are also available:

#[energy_budget(max_joules = 0.0002)]
#[thermal_aware]
fn thermal_safe_compute(n: i32) -> i32 {
    let result = n * n;
    result + 1
}

Compile with --energy-check to enable verification:

joulec program.joule -o program --energy-check

See Energy System Guide for a deep dive.

Testing with Energy

Write tests that verify both correctness and energy consumption:

#[test]
fn test_sort_energy() {
    let data = vec![5, 3, 1, 4, 2];
    let sorted = sort(data);
    assert_eq!(sorted, vec![1, 2, 3, 4, 5]);
}

#[bench]
fn bench_matrix_multiply() {
    let a = Matrix::random(100, 100);
    let b = Matrix::random(100, 100);
    let _ = a.multiply(b);
}

Run with:

joulec program.joule --test    # runs tests with energy reporting
joulec program.joule --bench   # runs benchmarks with energy reporting

Built-in Macros

Joule provides built-in macros for common operations:

// Output
println!("Hello, {}!", name);       // print with newline
print!("no newline");               // print without newline

// Formatting
let s = format!("{} + {} = {}", a, b, a + b);

// Collections
let nums = vec![1, 2, 3, 4, 5];

// Assertions (for testing)
assert!(x > 0);
assert_eq!(result, expected);

For FFI with C libraries, use extern declarations:

extern fn sqrt(x: f64) -> f64;

What's Next