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:
- Value Types: Stored on the stack, can only be mutated if declared
mutable
. Examples:int
,struct
. - Reference Types: Potentially stored on the heap. Whether you can rebind the reference is a local mutability property, and whether the underlying data can change is a global mutability property.
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:
while
do-while
for
(C-style)for (T x : collection)
loop
(infinite untilbreak
)loop(n)
(fixedn
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
public
- accessible from any file/packageprivate
- accessible only within the same classprotected
- accessible within the same package and subclassesstatic
- belongs to the class itself (no instance needed)mutable
- allows a class to be extended
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:
@Doc(&string)
– Adds a doc comment to types, functions, or fields.@Arg(&string, &string)
– Describes a function’s parameter (name and description).@ArgTemplate(&string, &string)
– Describes a template parameter.
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.