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
- Energy System Guide -- Deep dive into energy budgets
- Compiler Reference -- CLI flags and options
- JIT Compilation -- Interactive development
- Polyglot Energy Analysis -- Measure energy in Python/JS/C
- Standard Library -- All 110+ modules
- Language Reference -- Formal specification