Toucan Documentation

1. Toucan Homepage

Toucan is a statically typed, memory-safe programming language designed with a focus on readability and explicitness. Its primary goal is to merge many beloved features from various programming languages without causing contradictions or complexity. With its clear and unambiguous syntax, Toucan ensures that developers can write code that is both easy to maintain and straightforward to understand.

Toucan is suitable for general-purpose programming, from low-level systems tasks to high-level application development. Its memory safety features help prevent common pitfalls (like null-pointer dereferencing or buffer overflows), providing a robust environment for reliable software. As a statically typed language, all types are determined at compile time, improving performance and reliability. Toucan emphasizes explicit design, reducing ambiguity and making it easier to debug and maintain code.

Overall, Toucan is intended to be approachable for beginners and experienced developers alike, yet powerful enough for complex tasks.

2. Basic Syntax

Hello World

void main()
{
    println!("Hello, world!");
}

By default, the main function uses void for its return type, indicating no value is returned.

$ rainforest build hello.toucan
$ ./hello
Hello, world!

In larger projects, the main function must be declared public so the build system recognizes it.

package org.alex_hashtag.Main;

public void main()
{
    println!("Hello, World!");
}

Notice the package name (org.alex_hashtag.Main) matches the file location. If your file is src/org/alex_hashtag/Main.toucan, then package org.alex_hashtag.Main must appear at the top.

Project Setup

Toucan projects are typically built with the Rainforest tool. You create a rainforest.toml describing the project’s configuration, including dependencies, the main file, optimization settings, etc.

# Toucan Build Configuration

[project]
compiler = "1.0.0"
root = "MyToucanProject"
main = "src/org/alex_hashtag/Main.toucan"

# Dependencies
dependancies = [
    "alex_hashtag::example_dependancy1::1.0.0",
    "alex_hashtag::example_dependancy2::0.5.10"
]

# Detailed Metadata
[metadata]
name = "Toucan Example"
description = "Example project for Toucan"
authors = ["Alexander"]
emails = ["alex_hashtag@toucan.wiki"]
website = "https://toucan.wiki"
version = "1.0.0"
repository = "https://github.com/Alex-Hashtag/ToucanCompilerJava"

# Build Configuration
[build]
optimization = "-O3"   # -O0, -O1, -O2, -Os, -Oz, -O3, -O4
executable = "build/output"
logs = "build/logs"
intermediates = "build/intermediate"
os = "linux"
architecture = "x86_64"
debug_symbols = false
$ rainforest build
$ ./build/output/toucan_exe
Hello, World!

Comments

Comments work similarly to many other C-like languages:

void main()
{
    // Single-line comment

    /*
       Multi-line comment
    */

    println!("Hello, Toucan!");
}

Types and Values

void main()
{
    // Integers
    int32 a = -1;
    uint16 b = 2;
    int c = 3; // Alias for int32

    // Floats
    float32 d = 2.5;
    float64 e = 2.25;

    // Boolean
    bool f = true and false;

    // String
    string str = "Hello, World";

    // Multiline string
    string mult_str = """
        This is a multiline
        string literal
    """;

    // Type inference
    var inferred = 2.0;
}
Type Description
int8 signed 8-bit integer
uint8 unsigned 8-bit integer
int16 signed 16-bit integer
uint16 unsigned 16-bit integer
int32 signed 32-bit integer
uint32 unsigned 32-bit integer
int64 signed 64-bit integer
uint64 unsigned 64-bit integer
int128 signed 128-bit integer
uint128 unsigned 128-bit integer
usize unsigned pointer-sized integer
float16 16-bit floating point (IEEE-754)
float32 32-bit floating point (IEEE-754)
float64 64-bit floating point (IEEE-754)
float80 80-bit floating point extended precision
float128 128-bit floating point (IEEE-754)
char ASCII character
rune Unicode64 character
string Unicode8 string
bool true or false
void Used for type erasure
type The type of types
byte Alias for int8
int Alias for int32

Operators

void main()
{
    int a = (1 + 2) * 3 - 12;
    int b = a << 2;
    bool c = (a == b) or (a == (b >> 2));
}
Name Syntax
Addition a + b, a += b, a++
Subtraction a - b, a -= b, a--
Negation -a
Multiplication a * b, a *= b
Division a / b, a /= b
Modulo a % b, a %= b
Bit Shift Left a << b, a <<= b
Bit Shift Right a >> b, a >>= b
Unsigned Shift Right a >>> b, a >>>= b
Bitwise And a & b, a &= b
Bitwise Or a | b, a |= b
Bitwise Xor a ^ b, a ^= b
Bitwise Not ~a
Logical And and
Logical Or or
Boolean Not !a
Equality a == b
Inequality a != b
Greater Than a > b
Greater or Equal a >= b
Less Than a < b
Lesser or Equal a <= b
Address Of &a

Mutable and Const

By default, Toucan variables cannot be mutated. To enable mutation, mark them mutable. The compiler forces you to explicitly acknowledge mutability, preventing accidental side effects.

void main()
{
    mutable int a = 10;
    a += 10;
    echo(a);
}
$ rainforest build mut.toucan
$ ./mut
20

For performance-sensitive constants, you may use const. This signals the compiler to treat them as compile-time literals, not allocating them on the stack or heap (unless forced by other contexts).

void main()
{
    const int a = 10;
    echo(a);
}

Comparison of Const vs. Stack Allocation

Below is a simplified LLVM IR snippet (optimizations off) showing the difference between a const integer vs. a stack-allocated integer:

Const

; ModuleID = 'const_example'
@.str = private unnamed_addr constant [4 x i8] c"%d\n", align 1

declare i32 @printf(i8*, ...)

define void @main() {
entry:
    ; Print literal 10
    call i32 (i8*, ...) @printf(
        i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i32 0, i32 0),
        i32 10
    )
    ret void
}

Stack Immutable

; ModuleID = 'variable_example'
@.str = private unnamed_addr constant [4 x i8] c"%d\n", align 1

declare i32 @printf(i8*, ...)

define void @main() {
entry:
    ; Allocate space on stack
    %a = alloca i32, align 4
    store i32 10, i32* %a
    %0 = load i32, i32* %a, align 4
    call i32 (i8*, ...) @printf(
        i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i32 0, i32 0),
        i32 %0
    )
    ret void
}

Inner Scope

Toucan supports extra scoping blocks to limit variable lifetimes or produce temporary results with yield:

void main()
{
    {
        int a = 10;
        println!(a);
    }

    int c = {
        int b = 16;
        yield b + 16;
    };

    println!(c); // 32
}

3. Data Structures Basics

Value & Reference Types

Toucan, like Java/C#, distinguishes value vs. reference types. However, Toucan extends this concept with local vs. global mutability:

Arrays

Arrays are declared with square brackets. Below are examples of stack vs. heap arrays, mutable vs. immutable:

void main()
{
    // Stack-allocated, immutable
    int[] immutable_stack_array = int[]{1, 2, 3, 4};

    // Stack-allocated, mutable
    mutable int[] mutable_stack_array = int[]{1, 2, 3, 4};

    // Stack array with size, no initial values
    mutable int[] mutable_stack_array_of_size_4 = int[4];

    // Shorthand for specifying size & initializing all elements to 0
    mutable int[4] shorthand_mut_stack_array_4 = 0;

    // Heap-allocated, immutable reference
    &int[] immutable_heap_array = int[]{1, 2, 3, 4};

    // Heap-allocated, globally mutable
    mutable &int[] mutable_heap_array = int[]{1, 2, 3, 4};

    // Null reference
    &int[] null_array = null;

    // Changing an element (only possible if declared mutable)
    mutable_stack_array[0] = 20;

    println!(mutable_stack_array[0]);
}

Multi-Dimensional Arrays

void main()
{
    int[][] array2d = int[][] {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
}

Structs

Structs are value types with no constructors. They have default field values, which can be overridden when you initialize them.

struct Person
{
    string name = nullString!(64);
    uint8 age = 0;
    uint8[10] social_security_number = 0;
}

void main()
{
    Person person = Person{
        .name = "Dave";
        .social_security_number = int[]{0,1,2,3,4,5,6,7,8,9}
    }; // Age is default 0

    mutable Person mutable_person = Person{
        .name = "Alex";
        .age = 12;
    };

    mutable_person.age = 18;
}

4. Control Flow

If-Else Statement & Expression

Toucan allows if to be used either as a statement or an expression that yields a value.

void main()
{
    mutable int num = readConsole!(int);

    // Statement form
    if (num > 0)
        println!("Positive");
    else if (num < 0)
        println!("Negative");
    else
        println!("Zero");

    // Expression form
    int out = if (num == 10) 12 else 13;

    int out2 = if (num == 9)
    {
        println!("The number is 9");
        yield 8;
    }
    else
    {
        println!("Definitely not 9");
        yield 7;
    };
}

Loops

Toucan offers multiple looping constructs:

  1. while
  2. do-while
  3. for (C-style)
  4. for (T x : collection)
  5. loop (infinite until break)
  6. loop(n) (fixed n iterations)
void main()
{
    mutable int i = 0;

    while (i < 10)
    {
        println!("Hello World");
        i++;
    }

    i = 0;
    do
    {
        println!("Hello again");
        i++;
    }
    while (i < 10);

    for (i = 0; i < 10; i++)
    {
        println!("Index: " + i);
    }

    List<int32> items = list![10, 20, 30];
    for (int num : items)
    {
        println!("Item: " + num);
    }

    loop
    {
        println!("Infinite-ish loop");
        if (someCondition()) break;
    }

    loop (3)
        println!("Repeating 3 times");
}

Switch Statement & Expression

Toucan's switch can also be used as an expression:

uint8 x = readConsole!(uint8);

switch (x)
{
    1 -> println!("Value is 1");
    2 -> println!("Value is 2");
    _ -> println!("Some other value");
};

uint8 out = switch (x)
{
    1 -> 10;
    2 ->
    {
        println!("It's a 2!");
        yield 20;
    };
    _ -> 0;
};

5. Functions

Associated Functions

Functions are declared with a return type, then a name and parameter list. A void return means nothing is returned.

int32 sum(int32 a, int32 b)
{
    return a + b;
}

// Shorthand for trivial function
int32 getNum() return 12;

void main()
{
    println!(sum(5, 10));
    println!(getNum());
}

Parameters must be marked mutable if you plan to change them inside the function. If you want to mutate a stack-allocated value in-place, pass it by mutable &T.

// Example of an "out" style parameter (not recommended, but possible)
void increaseAge(mutable uint32 inc, mutable &Person target)
{
    target.age += inc;
}

struct Person
{
    string name = nullString!(64);
    uint8 age = 0;
}

void main()
{
    mutable Person p = Person{ .name="Bob", .age=10 };
    increaseAge(5, p);  // Person's age is now 15
}

Function Overloading

Toucan allows overloading by parameter types, and (unusually) by return type (though that can cause ambiguities).

int32 sum(int32 a, int32 b) return a + b;
int32 sum(int32 a, int32 b, int32 c) return a + b + c;
float64 sum(float64 a, float64 b) return a + b;

float32 sum(float... arr)
{
    float32 s = 0;
    for (mutable int32 i = 0; i < arr.length; i++)
    {
        s += arr[i];
    }
    return s;
}

int32 giveNum() return 1;
float32 giveNum() return 1.0;

void main()
{
    println!( sum(5, 10) );
    println!( sum(5, 10, 15) );
    println!( sum(5.0, 10.0) );
    println!( giveNum(int32) );
    println!( giveNum(float32) );
}

Recursion

uint64 factorial(uint64 n)
{
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

void main()
{
    println!( factorial(5) ); // 120
}

Generic Functions

Toucan supports generics with a template<T> syntax, generating specialized versions for primitive types at compile time, and handling reference types with a dynamic approach if needed.

template<T>
void printThree(T a, T b, T c)
{
    println!(a);
    println!(b);
    println!(c);
}

template<T>
T sumAll(T... items)
{
    T s = 0;
    for (T item : items)
    {
        s += item;
    }
    return s;
}

void main()
{
    printThree(1, 2, 3);
    printThree(1.1, 2.2, 3.3);

    println!( sumAll(1, 2, 3, 4) );
    println!( sumAll(1.5, 2.5, 3.5) );
}

Anonymous Functions / Function Pointers

Use lambda syntax (x,y) -> ... to create inline functions. They can be stored or passed to other functions as lambda<Return, Param...>.

void executeFunction(lambda<int,int,int> f, int x, int y)
{
    println!("Result: " + f(x, y));
}

void main()
{
    executeFunction((a, b) -> a + b, 3, 4);  // 7
    executeFunction((a, b) -> a * b, 3, 4);  // 12
}

Const (Pure) Functions

A const function can be evaluated at compile time, provided it meets certain restrictions: no mutable references, no unknown-iteration loops, no side effects, and no heap allocations.

const int32 add(int32 x, int32 y) return x + y;

const int factorial(uint32 n) return switch (n) {
    0 -> 1;
    _ -> n * factorial(n - 1);
};

Inline Functions

Marking a function inline hints that the compiler should inline it:

inline int32 square(int x) return x * x;
inline float64 cube(float64 x) return x * x * x;

6. Type System

Toucan provides a range of tools to define and organize data, from traits to enums to user-defined type aliases.

Traits

Traits group method signatures (and possibly default implementations). Any type that implements a trait must supply the trait’s methods (unless they also have default bodies).

trait Shape
{
    float64 area();
}

trait Drawable implements Shape
{
    void draw()
    {
        println!("Drawing shape with area: " + this.area());
    }
}

struct Circle
{
    float64 radius;
}

// Implement multiple traits at once
implement Shape, Drawable for Circle
{
    public float64 area()
    {
        return 3.14159 * this.radius * this.radius;
    }
}

Casting

Casting is not built-in via operators ((T) value) but rather done through a trait, e.g. Castable<T>. You define how your type (or a foreign type) converts to another type.

struct uint96
{
    uint32 high;
    uint64 low;
}

implement Castable<uint96> for int64
{
    uint96 cast()
    {
        // Example: store entire int64 into 'low'
        return {0, this};
    }
}

Typedef

typedef introduces a new name for an existing type, optionally restricting casting. This helps enforce domain-specific usage:

typedef PageCount = uint32;
typedef ISBNNumber = uint32;

struct Book
{
    ISBNNumber isbn = 0;
    PageCount pages = 0;
}

Typedef can also be templated or marked private:

typedef NonZeroI64 = private int64
{
    public static Result<NonZeroI64, Error> makeNonZero(int64 x)
    {
        if (x == 0) return FAILURE(SomeError);
        return SUCCESS(NonZeroI64(x));
    }
}

Unions

A var<T1, T2, ...> can hold one of multiple possible types (similar to a sum type). Here, as(Type) attempts to interpret the underlying data as Type.

void main()
{
    var<int64, float64> a = 200000;
    println!(a.as(int64));
    println!(a.as(float64));
}

Enums

Unlike var<...>, an enum has a fixed set of variants, each possibly carrying data. An enum is stored on the stack (a value type).

enum Message
{
    QUIT,
    MOVE(int32 x = 0, int32 y = 0),
    WRITE(string text = ""),
    CHANGE_COLOR(int32 r, int32 g, int32 b)
}

void main()
{
    Message msg = MOVE(10, 20);
    switch (msg)
    {
        QUIT -> println!("QUIT");
        MOVE(x, y) -> println!("Move by " + x + "," + y);
        WRITE(t) -> println!("Message: " + t);
        CHANGE_COLOR(r, g, b) -> println!("Color: " + r + "," + g + "," + b);
    };
}

Generics

Most type definitions (typedef, enum, trait, class) support template<T> parameters. (Structs do not, by design.)

Object-Oriented Programming

Classes

Classes are reference types that bundle fields and methods. By default, classes cannot be extended unless marked mutable. Constructors are defined using the constructor keyword.

class Person
{
    private string name = nullString!(64);
    private mutable uint8 age = 0;
    private float32 height = 1.75;

    public constructor new(string name, uint8 age, float32 height)
    {
        this.name = name;
        this.age = age;
        this.height = height;
    }

    public void printDetails()
    {
        println!("Name: " + this.name);
        println!("Age: " + this.age);
        println!("Height: " + this.height);
    }

    public void ageUp()
    {
        this.age++;
    }
}

Multiple constructors can exist by giving them different names or signatures:

class Person
{
    private string name = "";
    private int8 age = 0;

    public constructor new(string name)
    {
        this.name = name;
    }

    public constructor newFull(string name, int8 age)
    {
        this.name = name;
        this.age = age;
    }
}

Access Modifiers

Inheritance

Declare mutable on a class if it can be extended. Use extends to create a subclass. The super keyword lets you call superclass methods, or call its constructor with this = super.new(...).

mutable class Animal
{
    int32 health = 0;

    constructor new(int32 h)
    {
        this.health = h;
    }

    public void makeSound(&string sound)
    {
        println!(sound);
    }
}

class Dog extends Animal
{
    string name = "Unknown";

    constructor new(int32 health, string dogName)
    {
        // Call parent's constructor
        this = super.new(health);
        this.name = dogName;
    }

    @Override
    public void makeSound(&string sound)
    {
        // Extend or override the parent method
        super.makeSound("Dog " + this.name + " says: " + sound);
    }

    public void bark()
    {
        this.makeSound("Woof!");
    }
}

Abstract Classes

Mark a class abstract to prevent direct instantiation and force subclasses to override certain methods:

abstract class Creature
{
    abstract void eat();
    abstract void sleep();
}

Classes with Traits

A class can implement any number of traits:

public trait AnimalTrait
{
    void eat();
    void sleep();
}

public trait Walkable
{
    void walk();
}

public class Dog implements AnimalTrait, Walkable
{
    public void eat()   { println!("Eating..."); }
    public void sleep() { println!("Zzzz..."); }
    public void walk()  { println!("Walking..."); }
}

7. Error Management

Toucan uses errors as values rather than exceptions. When a function can fail, it returns Result<T, E>, forcing explicit handling of both success and error cases.

error ArithmeticError
{
    DivideByZeroError("Cannot divide by zero"),
    OperationWithInfinityError("Cannot use infinity in arithmetic '%c'", char op=' '),
    NaNError("Requires a number in operation '%c'", char op=' ');
}

// Equivalent to:
enum ArithmeticError implements Error
{
    DivideByZeroError(),
    OperationWithInfinityError(char op=' '),
    NaNError(char op=' ');

    @Override
    string getErrorMessage()
    {
        return switch (this)
        {
            DivideByZeroError -> "Cannot divide by zero";
            OperationWithInfinityError(o) -> "Cannot use infinity in arithmetic '" + o + "'";
            NaNError(o) -> "NaN not allowed in arithmetic '" + o + "'";
        };
    }
}

Using Result<T,E>

Result<float64, ArithmeticError> safeDivide(float64 a, float64 b)
{
    if (b == 0)
        return FAILURE(ArithmeticError.DivideByZeroError);
    if (a == Double.INFINITY || b == Double.INFINITY)
        return FAILURE(ArithmeticError.OperationWithInfinityError('/'));
    if (a == Double.NaN || b == Double.NaN)
        return FAILURE(ArithmeticError.NaNError('/'));

    return SUCCESS(a / b);
}

void main()
{
    switch (safeDivide(10, 3))
    {
        FAILURE(ArithmeticError.DivideByZeroError) ->
            println!("Divide By Zero!");

        FAILURE(ArithmeticError.OperationWithInfinityError) ->
            println!("Infinity not allowed!");

        FAILURE(ArithmeticError.NaNError) ->
            println!("NaN not allowed!");

        SUCCESS(x) ->
            println!("Result: " + x);
    };
}

.catch(...) Method

A .catch method on Result<T,E> can simplify error handling. You must provide a function or fallback value:

float64 res = safeDivide(10, 0).catch(err => {
    println!("Caught error: " + err.getErrorMessage());
    return 0.0;
});

float64 val = safeDivide(10, 0).catch(err => {
    // Another approach
    println!("Error, returning default");
    return 5.0;
});

println!(res); // 0
println!(val); // 5

Option<T> is also used for potentially absent values (SOME(T) or NONE).

8. Modules and Packages

Each source file starts with a package <name>. Imports refer to the package path plus the file name. For example:

package org.alex_hashtag.utils;

import toucan.std.collections.lists.ArrayList;
// or import toucan.std.collections.lists.*  (not recommended)

Below is a simple project structure:

MyToucanProject/
├── rainforest.toml
├── src/
│   └── org/
│       └── alex_hashtag/
│           ├── Main.toucan
│           └── utils/
│               └── Helper.toucan
└── build/
    ├── output/
    ├── logs/
    └── intermediate/

An example Main.toucan might be:

package org.alex_hashtag.Main;

import org.alex_hashtag.utils.Helper;

public void main()
{
    println!("=== Toucan Demo ===");

    Helper.greet("Toucan");
}

The Helper file could be:

package org.alex_hashtag.utils;

public class Helper
{
    public static void greet(string name)
    {
        println!("Greetings from " + name + "!");
    }
}

9. Systems Programming

Toucan aims to allow low-level operations within a safe framework, including explicit memory management and direct OS interaction.

typeof and sizeof

Use sizeof to get the size in bytes of a type at compile time, and typeof to inspect a variable’s type.

template<T>
T computeValue(T value)
{
    return switch (typeof value)
    {
        int32   -> value * 2 + 10;
        float64 -> value * value;
        bool    -> !value;
        _       -> value;
    };
}

void main()
{
    println!( computeValue(42) );      // 94
    println!( computeValue(6.0) );     // 36
    println!( computeValue(true) );    // false
}

Unsafe

Certain operations (like direct pointer usage or system calls) live behind an unsafe boundary. You must explicitly acknowledge that you’re doing something potentially dangerous.

unsafe
{
    // Raw pointer or memory access logic
    let ptr = sys.malloc(16);
    sys.assign<int32>(ptr, 42);
    int32 val = sys.dereference(int32, ptr);
    sys.free(ptr);
}

System Operations

The sys namespace contains variables and functions for OS-level work (file descriptors, memory, processes, etc.).

template<T implements Default>
T readStdin()
{
    unsafe
    {
        &string buff = sys.read(sys.stdin_fd, 1024);
        switch (buff.toStringArray("\\s")[0].parse(T))
        {
            SUCCESS(res) -> return res;
            FAILURE(_)    -> return T.default();
        }
    }
}

Variables Table

Variable Description Type Default Value
sys.stdin_fd File descriptor for standard input int32 0
sys.stdout_fd File descriptor for standard output int32 1
sys.stderr_fd File descriptor for standard error int32 2
sys.errno Error number of the last system call that failed int32 0
sys.page_size The system memory page size (in bytes) usize Platform-specific
sys.max_fd The maximum number of file descriptors int32 Platform-specific

Functions Table

Function Description Parameters Return Type
sys.malloc(size) Allocates a block of memory. size: usize usize (Pointer)
sys.free(ptr) Frees a previously allocated block of memory. ptr: usize void
sys.dereference(T, ptr) Dereferences data from a memory address. T: type, ptr: usize T
sys.assign<T>(ptr, value) Writes data to a memory address. ptr: usize, value: T void
sys.cast<T>(U, value) Casts a value from type T to type U safely. T: type, U: type, value: T U
sys.call(id, ...args) Executes a system call based on the provided ID and arguments. id: int32, ...args: Any int64
sys.open(path, mode) Opens a file with the specified mode. path: &string, mode: &string int32 (FileDescriptor)
sys.close(fd) Closes a file descriptor. fd: int32 int32
sys.read(fd, size) Reads data from a file descriptor. fd: int32, size: usize &string
sys.write(fd, data) Writes data to a file descriptor. fd: int32, data: &string int64
sys.stat(path) Retrieves file metadata. path: &string usize (Pointer to Stat)
sys.unlink(path) Deletes a file. path: &string int32
sys.rename(old_path, new_path) Renames or moves a file. old_path: &string, new_path: &string int32
sys.fork() Creates a new process. None int32
sys.exec(path, args) Replaces the current process image. path: &string, args: &string[] int32
sys.wait(pid) Waits for a process to terminate. pid: int32 int32
sys.signal(sig, handler) Sets up a signal handler. sig: int32, handler: lambda<void> int32
sys.threadCreate(entryPoint) Creates a new thread executing the provided function. entryPoint: lambda<void> int32 (Thread)
sys.threadJoin(thread) Waits for the specified thread to finish execution. thread: int32 int32

10. Macros (Metaprogramming)

Declarative Macros

Toucan’s declarative macro system (inspired by Rust) helps avoid repetitive code. You can place macros in the same source file or in dedicated .toucanmacro files (if your build system requires separate pre-compilation).

1. Basic Macro Example

macro repeat_hello
{
    (expression $count) -> {
        loop ($count)
        {
            println!("Hello, world!");
        }
    };
}

void main()
{
    repeat_hello!(3);
}

2. Multiple Patterns

macro mathOperation
{
    (add, expression $x, expression $y) -> {
        println!($x + " + " + $y + " = " + ($x + $y));
    };

    (subtract, expression $x, expression $y) -> {
        println!($x + " - " + $y + " = " + ($x - $y));
    };
}

void main()
{
    mathOperation!(add, 5, 3);       // 5 + 3 = 8
    mathOperation!(subtract, 8, 2);  // 8 - 2 = 6
}

3. Macro for Creating Classes

macro createClass
{
    (identifier $name, type $T identifier $field = expression $expr) -> {
        class $name
        {
            public $T $field = $expr;

            public constructor new($T $field)
            {
                this.$field = $field;
            }
        }
    };
}

createClass!(Person, string name = nullBytes!(sizeof string));

void main()
{
    Person p = Person.new("Alice");
    println!("Name: " + p.name);
}

4. Repetition and Capture

macro sum
{
    (expression $x) -> {
        yield $x;
    };
    (expression $x, $(expression $rest),+) -> {
        $x + sum!($($rest),+)
    };
}

void main()
{
    let result = sum!(1, 2, 3, 4, 5);
    println!("The sum is: " + result);  // 15
}

Annotations

The other type of macro in Toucan are annotations, which are functions defined in the toucanmacro files. They are executed by the compiler on its internal representation of the code—before it’s translated into LLVM IR. Annotations can attach metadata or modify the behavior of code blocks.

annotation Getter(String field_name)
{
    public void apply(mutable Class cl)
    {
        Variable variable = switch (cl.getFields().stream().find(x -> x.getVariable().getName().equals(field_name)))
        {
            NONE -> ErrorManager.panic("There is no field with the name " + field_name + " in class " + cl.getName());
            SOME(a) -> a;
        };
        Method method = Method.new("get" + field_name.toPascalCase(), variable.getType(), List.empty());
        Method.addStatement(Statement.parse("return this." + field_name + ";"));
        cl.addMethod(method);
    }
}

@Getter("examp")
public class Example
{
    private int examp;
}

Documentation Generation

Toucan also supports documentation annotations that let you embed descriptive comments directly into your source code. These annotations include:

When building a library, a documentation tool can extract these annotations to generate HTML documentation (similar to Rust’s rustdoc or JavaDocs). This documentation will include details about classes, methods, parameters, and more.

Example: Documented Calculator Class

@Doc("Calculator class that provides basic arithmetic operations.")
public class Calculator
{
    @Doc("Default precision used for floating-point calculations.")
    private int32 precision = 2;

    @Doc("Constructor for Calculator.")
    @Arg("precision", "Sets the number of decimal places for floating-point results.")
    public constructor new(int32 precision)
    {
        this.precision = precision;
    }

    @Doc("Adds two integers together.")
    @Arg("a", "The first operand.")
    @Arg("b", "The second operand.")
    public int32 add(int32 a, int32 b) return a + b;

    @Doc("Subtracts the second integer from the first.")
    @Arg("a", "The number from which to subtract.")
    @Arg("b", "The number to subtract.")
    public int32 subtract(int32 a, int32 b) return a - b;

    @Doc("Multiplies two integers.")
    @Arg("a", "The first factor.")
    @Arg("b", "The second factor.")
    public int32 multiply(int32 a, int32 b) return a * b;

    @Doc("""
         Divides the first number by the second.
         Returns a failure if division by zero is attempted.
         """)
    @Arg("a", "The dividend.")
    @Arg("b", "The divisor.")
    public Result<float64, DivisionError> divide(float64 a, float64 b)
    {
        if (b == 0)
            return FAILURE(DivisionError.DivideByZero);
        return SUCCESS(a / b);
    }
}

Sample Generated Documentation

When you run a doc generation tool (e.g. rainforest doc), the annotations from your source are converted into an HTML documentation page. Below is an example of what a generated documentation page might look like:

Calculator

Calculator is a class that provides basic arithmetic operations.

Constructors

new(precision: int32)

Constructor for Calculator.

  • precision: Sets the number of decimal places for floating‑point results.

Methods

add(a: int32, b: int32) → int32

Adds two integers together.

  • a: The first operand.
  • b: The second operand.

subtract(a: int32, b: int32) → int32

Subtracts the second integer from the first.

  • a: The number from which to subtract.
  • b: The number to subtract.

multiply(a: int32, b: int32) → int32

Multiplies two integers together.

  • a: The first factor.
  • b: The second factor.

divide(a: float64, b: float64) → Result<float64, DivisionError>

Divides the first number by the second. Returns an error if division by zero is attempted.

  • a: The dividend.
  • b: The divisor.