Hello World

Every Fly program starts from a main() function. Import a module with import, then call its functions using the namespace prefix.

import fly.os

void main() {
    os.print("Hello, World!")
}

Variables and types

Variables are declared with their type. An initial value is optional; uninitialized variables default to their zero value.

int count
int x = 10
bool ready = false
string name = "Fly"

Built-in types: bool, byte, short, ushort, int, uint, long, ulong, float, double, string.


Functions

Declare a return type before the function name. Inside the body, assign to the special identifier out to produce the return value.

int double(const int n) {
    out = n * 2
}

void main() {
    int x = double(21)   // x = 42
}

The source reads as return-by-value, but the compiler generates a hidden by-reference output parameter — so no copy is ever made. You get int x = double(21) ergonomics with double(21, &x) performance.

const marks an input parameter (read-only). A function without a return type is void.

For multiple outputs, you can use traditional output parameters (non-const):

minMax(const int a, const int b, int min, int max) {
    if (a < b) {
        min = a
        max = b
    } else {
        min = b
        max = a
    }
}

void main() {
    int lo
    int hi
    minMax(3, 7, lo, hi)   // lo=3, hi=7
}

Or declare multiple return types and use out[0], out[1], …:

int,int minMax(const int a, const int b) {
    if (a < b) { out[0] = a  out[1] = b }
    else       { out[0] = b  out[1] = a }
}

void main() {
    int lo = minMax(3, 7)   // lo = 3 (first return value)
}

Importing modules

Fly supports four import styles, modelled on Java-style imports.

Namespace import — brings the last segment into scope; access symbols with a prefix:

import fly.str              // 'str' is in scope
int n = str.len("hello")    // qualified access

Class import — brings a single class (or enum/struct) directly into scope, no prefix needed. This is the Java-style per-class import:

import fly.data.List        // 'List' is in scope
import fly.data.Map         // 'Map' is in scope

List  l = new List()        // no 'data.' prefix
Map   m = new Map()

The import path follows the namespace hierarchy, not the filesystem. There is no required link between the filename and the class name.

Wildcard import — brings every public symbol (classes, enums, structs, functions) from a namespace directly into scope. The target must be a namespace:

import fly.data.*           // List, Stack, Map, … all in scope
List l = new List()

Alias import — binds the imported namespace or class under a local name:

import fly.str as s
import fly.data.List as L

int n    = s.len("hello")
L myList = new L()

Error handling

Use fail inside a function to signal an error. fail accepts zero to three comma-separated arguments — an integer code, a string message, and/or an object instance, in any combination:

fetch(const string url) {
    if (url == "") {
        fail 404, "Not Found"   // integer code + string message
    }
}

How propagation works: every function has a hidden error-pointer parameter. When fail fires with no enclosing handle in the current function, it writes to the error struct and returns immediately. The calling code resumes at the next instruction — it does not unwind the stack.

void main() {
    fetch("")        // writes error 404; returns, execution continues
    fetch("/ok")     // STILL CALLED — caller is not unwound
    // main exits with code 404
}

Use handle to intercept errors. All calls inside the block share a dedicated error struct. After the block, check if (err):

void main() {
    error err handle {
        fetch("")        // fails and writes error; handle body continues
        fetch("/ok")     // still called (callee fail ≠ jump in caller)
    }
    if (err) {
        // handle the error
    }
}

When fail fires directly inside the handle body (same function), execution jumps immediately past the remaining handle code to the check point:

void main() {
    error err handle {
        if (someCondition) {
            fail 500        // jumps to safe block; neverReached() is skipped
        }
        neverReached()
    }
    if (err) { /* code = 500 */ }
}

If you don't need to name the error, omit the variable:

void main() {
    handle {
        fetch("")   // error is swallowed silently
    }
}

To re-raise an error to the caller, use a bare fail:

wrapper() {
    error err handle {
        fetch("")
    }
    if (err) {
        fail    // propagate to wrapper's caller
    }
}

Structs

A struct holds data fields and can extend one other struct. Structs have no virtual dispatch — access is direct.

Plain new on a struct allocates on the stack. The variable is freed automatically when the scope exits — do not call delete.

struct Point {
    int x
    int y
}

struct Point3D : Point {
    int z
}

void main() {
    Point3D p = new Point3D()   // stack allocation
    p.x = 1
    p.y = 2
    p.z = 3
}   // p freed automatically — no delete needed

Classes

A class adds virtual method dispatch via vtable. It can extend a struct (inheriting its fields) and implement one or more interfaces.

Plain new on a class allocates on the heap. You must call delete to free it.

interface Drawable {
    draw()
}

class Circle : Point, Drawable {
    int radius

    draw() {
        // render the circle
    }
}

void main() {
    Circle c = new Circle()   // heap allocation
    c.x = 0
    c.y = 0
    c.radius = 5
    c.draw()
    delete c   // free the heap memory
}

Visibility modifiers for members: public, private, protected.
Use static for class-level fields and methods.


Memory management

Instead of managing new/delete manually, use a smart allocation qualifier. All three qualifiers work on both structs and classes.

QualifierStorageFreed whenCopying
new uniqueheapvariable goes out of scopecompile-time error
new sharedheap + refcountlast reference exits scopeallowed; increments refcount
new weakheapeach holder exits scopeallowed; no refcount — first to exit frees, rest dangle

new unique — single owner

process() {
    Point p = new unique Point()   // heap-allocated
    p.x = 10
    p.y = 42
}   // free(p) emitted automatically — no delete

unique ownership cannot be copied. Attempting to assign a unique variable to another variable is a compile-time error.

new shared — reference-counted

The runtime stores an 8-byte reference count immediately before the object data. Copies increment the count; each scope exit decrements it. When the count reaches zero the entire block is freed.

process() {
    Point a = new shared Point()   // refcount = 1
    Point b = a                    // refcount = 2
    // …
}   // refcount → 0 → freed automatically

new weak — untracked alias

No reference count. Every holder calls free() at its own scope exit. Use only when you know the lifetime is not ambiguous.

process() {
    Point a = new weak Point()
    Point b = a   // b and a share the same pointer
}   // first scope exit frees the data; other becomes dangling

Generics

Fly supports generic classes and generic functions via monomorphization. Each instantiation with a different concrete type produces a separate, fully optimized implementation at compile time — no type erasure, no boxing overhead.

Generic classes

Declare type parameters in angle brackets after the class name:

public class Wrapper<T> {
    private T value

    public Wrapper(T v) {
        value = v
    }

    public T get() {
        out = value
    }

    public void set(T v) {
        value = v
    }
}

Instantiate by supplying concrete type arguments:

import fly.data.wrapper

void main() {
    Wrapper<string> ws = new Wrapper<string>("hello")
    string s = ws.get()   // s = "hello"

    Wrapper<int> wi = new Wrapper<int>(42)
    int n = wi.get()       // n = 42
}

Wrapper<string> and Wrapper<int> are completely separate types in the generated code.

Generic functions

Type parameters appear between the function name and its parameter list:

T identity<T>(const T v) {
    out = v
}

void main() {
    int    i = identity(10)     // T inferred as int
    string s = identity("fly")  // T inferred as string
}

The compiler infers the type argument from the argument expression, so explicit identity<int>(10) is optional.

Managing a list of strings — fly.data.List<string> pattern

fly.data.List stores raw long values. To keep a typed list of strings, wrap each string in a Wrapper<string> and store the wrapper reference in the list:

import fly.data.list
import fly.data.wrapper

void main() {
    List lst = new List()

    Wrapper<string> a = new Wrapper<string>("apple")
    Wrapper<string> b = new Wrapper<string>("banana")
    Wrapper<string> c = new Wrapper<string>("cherry")

    lst.add(a)
    lst.add(b)
    lst.add(c)

    int total = lst.size()   // total = 3
    for int i = 0; i < total; i++ {
        Wrapper<string> item = lst.get(i)
        string text = item.get()
        // use text …
    }

    lst.free()
}

This pattern generalises to any heap-allocated type: Wrapper<int>, Wrapper<MyClass>, and so on.