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.
| Qualifier | Storage | Freed when | Copying |
|---|---|---|---|
new unique | heap | variable goes out of scope | compile-time error |
new shared | heap + refcount | last reference exits scope | allowed; increments refcount |
new weak | heap | each holder exits scope | allowed; 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.