Items

Items are the top-level declarations in a Joule program.

Functions

fn name(param: Type, param2: Type) -> ReturnType {
    body
}

Visibility

pub fn public_function() { }     // visible outside module
fn private_function() { }        // module-private (default)

Parameters

Parameters are passed by value (move) by default:

fn process(data: Vec<u8>) {
    // data is moved into this function
}

Use references for borrowing:

fn inspect(data: &Vec<u8>) {
    // data is borrowed immutably
}

fn modify(data: &mut Vec<u8>) {
    // data is borrowed mutably
}

Self Parameter

Methods take self as their first parameter:

impl Point {
    fn distance(self) -> f64 { }          // takes ownership
    fn inspect(self) -> f64 { }           // immutable self
    fn translate(mut self, dx: f64) { }   // mutable self
}

Generic Functions

fn first<T>(items: Vec<T>) -> Option<T> {
    if items.len() > 0 {
        Option::Some(items[0])
    } else {
        Option::None
    }
}

Extern Functions

Functions implemented outside Joule (FFI):

extern fn sqrt(x: f64) -> f64;
extern fn malloc(size: usize) -> *mut u8;

Const Functions

Functions that can be evaluated at compile time are declared with const fn:

const fn max(a: i32, b: i32) -> i32 {
    if a > b { a } else { b }
}

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

// Use at compile time
const MAX_SIZE: i32 = max(100, 200);
const FACT_10: i32 = factorial(10);

Const Function Restrictions

const fn bodies are restricted to operations the compiler can evaluate:

  • Arithmetic operations on primitive types
  • Control flow (if, match, recursion)
  • Local variable bindings
  • Calling other const fn functions

The following are not allowed in const fn:

  • Heap allocation (Vec::new(), Box::new())
  • I/O operations
  • Mutable static state
  • Non-const function calls

Comptime Blocks

For more complex compile-time computation, use comptime blocks:

comptime {
    let table = generate_sin_table(1024);
}

// table is available as a compile-time constant
fn fast_sin(x: f64) -> f64 {
    let index = (x * 1024.0 / TAU) as usize;
    table[index % 1024]
}

Comptime blocks execute during compilation and make their results available as constants in runtime code. The HIR const evaluator handles arithmetic, control flow, and function calls within comptime blocks.

Structs

Named product types with fields:

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

Field Visibility

Fields are private by default. Use pub to make them accessible:

pub struct Config {
    pub name: String,       // public
    secret_key: String,     // private
}

Generic Structs

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

Enums

Sum types (tagged unions) with variants:

pub enum Color {
    Red,
    Green,
    Blue,
}

Variants with Data

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

Tuple Variants

pub enum Option<T> {
    Some(T),
    None,
}

pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

Generic Enums

pub enum Either<L, R> {
    Left(L),
    Right(R),
}

Impl Blocks

Associate methods with a type:

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

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

Multiple impl blocks are allowed for the same type:

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

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

Traits

Define shared behavior:

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

pub trait Clone {
    fn clone(self) -> Self;
}

Trait Implementation

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

Trait Bounds

fn print_all<T: Display>(items: Vec<T>) {
    for item in items {
        println!("{}", item.to_string());
    }
}

Dynamic Dispatch

Use dyn Trait for runtime polymorphism:

fn print_shape(shape: &dyn Display) {
    println!("{}", shape.to_string());
}

Type Aliases

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

Modules

Module Declarations

Modules organize code into separate files. The mod keyword declares a module:

mod lexer;      // loads from lexer.joule or lexer/mod.joule
mod parser;
mod typeck;

File Resolution

When the compiler encounters mod foo;, it searches for:

  1. foo.joule in the same directory as the current file
  2. foo/mod.joule for modules with sub-modules

Public Module Re-exports

pub mod utils;      // re-exports utils module to parent

Inline Modules

Modules can be defined inline within a file:

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

// Use items from inline module
let clamped = helpers::clamp(value, 0, 100);

Visibility

pub mod public_module { }
mod private_module { }

Use Declarations

Import items into scope:

// Import specific items
use crate::ast::{File, AstItem, Visibility};

// Import all items from a module
use crate::prelude::*;

// Standard library imports
use std::collections::HashMap;
use std::math::*;

// Import with alias
use crate::ast::File as AstFile;

Stdlib Path

The --stdlib-path CLI flag specifies the location of the standard library. The builtin registry includes modules for math, statistics, and compute:

use std::math::*;         // sin, cos, sqrt, etc.
use std::statistics::*;   // mean, median, std_dev

Let Statements

Variable bindings:

let x = 42;                    // immutable, type inferred
let y: f64 = 3.14;            // immutable, explicit type
let mut z = 0;                 // mutable
let (a, b) = (1, 2);          // destructuring