Fun Language Reference (Comprehensive)
June 14, 2026 · View on GitHub
Overview
Fun is a statically-typed, C-transpiling language focused on performance and clarity. The compiler emits readable C and relies on the system C toolchain for linking and execution.
Numeric Model
numis a signed 64-bit integer (Cint64_t).decis a 64-bit floating-point number (Cdouble).- Fixed-width integers:
i8,i16,i32,i64,u8,u16,u32,u64. - Fixed-width floats:
f32(float),f64(double). - Arbitrary-width integers:
iN/uN(emitted as_BitInt(N)/unsigned _BitInt(N)when width is not a standard 8/16/32/64). binis boolean (bool).chris a character (char).stris a null-terminated string (char*).rawis an opaque/void type (voidin C; useraw*forvoid*).
Source Structure
- File extension:
.fn - Statements end with
;. - Blocks are delimited with
{}. - Comments:
//single-line.
Documentation Comments
- The website reference uses regular
//comments that appear immediately above declarations. - This applies to module summaries, public symbols, compound fields, and quirk members.
- For best results, keep comments short and declaration-specific.
- Example:
// Optional value container.
pub compound Option<T> {
// True when a value is present.
bin has;
// Stored value.
T value;
}
// Value that can render itself as text.
pub quirk Display {
// Produce a textual representation.
to_string() str;
}
Imports
imp std.c.io;imports a C header signature module.imp std.string;imports Fun stdlib modules.- Relative imports are supported (e.g.,
imp ..foo.bar;). - Import aliases are supported:
imp mod1 as one;then useone.symbol. - Aliases are the supported way to avoid duplicate exported symbol collisions across imported modules.
- Example:
imp mod1 as one;imp mod2 as two;num a = one.pick();num b = two.pick();
- Example:
Types
Built-in Types
num,dec,f32,f64,i8,i16,i32,i64,u8,u16,u32,u64,iN,uN,bin,chr,str,raw
Arrays
- Syntax:
num[] arr = [1, 2, 3]; - Array literals require uniform element types.
Pointers
- Pointer depth:
Type*. - Example:
Node* next;.
Compounds (Structs)
compound Point {
num x;
num y;
}
fun main() {
Point p;
p.x = 1;
p.y = 2;
}
A field whose name begins with _ is module-private: it is accessible only
from code in the same module as the compound's declaration (including the type's
own impl methods via self._field). Other modules must use public accessors.
compound Account {
num id; // public
num _balance; // private to this module
}
impl Account {
pub balance() num { ret self._balance; } // ok: same module
}
// Another module: `acc._balance` is rejected; `acc.balance()` works.
Quirks (Interfaces)
quirk Shape {
area() num;
}
compound Square {
num side;
}
impl Square as Shape {
area() num { ret self.side * self.side; }
}
fun main() {
Square s;
s.side = 4;
num area = s.area();
_ = area;
}
Notes:
- Quirk methods obey visibility: private methods are callable inside the same module only.
print_fmt("{}", value)usesDisplay.to_string()when that method is accessible at the call site.- If
Display.to_string()exists but is private from the caller module,{}falls back to pointer-style formatting.
Implementations
quirk Shape {
area() num;
}
compound Point {
num x;
num y;
}
compound Rectangle {
num w;
num h;
}
impl Point {
translate(num dx, num dy) {
self.x += dx;
self.y += dy;
}
}
impl Rectangle as Shape {
area() num { ret self.w * self.h; }
}
fun main() {
Point p;
p.x = 1;
p.y = 2;
p.translate(3, 4);
Rectangle r;
r.w = 3;
r.h = 4;
num area = r.area();
_ = area;
}
Generics
- Compounds and impls can be generic:
compound Vec<T> { ... }. - Use
Vec<num>etc. where required. - Impl type parameters can be constrained with
:and|.- Example:
impl Vec<T: num | dec> { ... }. - This lets one impl body work for a fixed set of concrete numeric types.
- Example:
compound Vec<T> {
T[] data;
num len;
}
impl Vec<T: num | dec> {
pub sum() T {
T out = self.zero_value();
num i = 0;
for i < self.len {
out = out + self.data[i];
i = i + 1;
}
ret out;
}
}
Variables
- Variables can be explicitly typed or inferred with
let.
fun add(num a, num b) num {
ret a + b;
}
fun main() {
num x = 1;
str name = "fun";
let count = add(1, 2);
_ = x;
_ = name;
_ = count;
}
Type Inference (let)
letrequires an initializer; the type is inferred from the expression.- Literal inference:
1->num,1.5->dec,"text"->str,'a'->chr,true->bin.
- Array literals infer element type and become
T[]:[1, 2, 3]->num[][Point{x = 1, y = 2}]->Point[]
- Function calls infer the return type:
let p = make_point(1, 2);->Point
- Member access uses the receiver's type:
let x = p.x;->num
- Indexing an array yields its element type:
let v = nums[i];->numwhennumsisnum[]let x = points[0].x;->numwhenpointsisPoint[]
- Numeric expressions prefer
decwhen any operand isdec(otherwisenum).
compound Point { num x; num y; }
fun make_point(num x, num y) Point {
ret Point{x = x, y = y};
}
fun main() {
let n = 42; // num
let d = 3.5; // dec
let s = "hello"; // str
let c = 'Z'; // chr
let b = true; // bin
let nums = [1, 2, 3]; // num[]
let p = make_point(1, 2); // Point
let x = p.x; // num
let px = nums[0]; // num
}
Functions
fun add(num a, num b) num {
ret a + b;
}
fun main() {
num total = add(1, 2);
_ = total;
}
- No nested function declarations.
- Use
retfor return. - Generic functions are supported:
fun id<T>(T x) T { ret x; }
fun main() {
num v = id(1);
_ = v;
}
- Default parameter values: parameters may have defaults (
= expr); a call may omit trailing defaulted arguments.
fun connect(str host, num port = 8080, bin tls = false) {
// implementation
}
fun main() {
connect("a"); // port 8080, tls false
connect("a", 9000); // tls false
connect("a", 9000, true);
}
- Rules: defaults must be trailing (no required param after a defaulted one); a
default expression is evaluated at the call site, so it cannot reference
selfor an earlier parameter (nil, literals, globals, and enum variants are fine). Applies to free functions and methods, including generic,async, and pointer-receiver methods. - A non-
voidfunction that can fall off the end without returning triggers themissing_returnwarning (always checked; conservative — trailingret, exhaustiveif/elif/else, default-branchfit, and infinite loops all count as returning).
Async / Await
- Declare async functions with
async fun. awaitis only valid insideasync funbodies.- Calls to async functions and async quirk methods must be awaited.
- Await targets must resolve to async calls.
compound Counter {
num base;
}
quirk AsyncCounter {
async add(num x) num;
}
impl Counter as AsyncCounter {
async add(num x) num { ret self.base + x; }
}
async fun main() {
Counter c;
c.base = 41;
num out = await c.add(1);
_ = out;
}
Concurrency: fork & channels
fork <async-call>;spawns a virtual thread (fire-and-forget) onto an M:N scheduler — a CPU-count-sized pool of OS worker threads runs many cheap tasks.mainautomatically waits for allforked tasks to finish before returning.- Channel operators (sugar over
std.channel):ch <- v≡ch.send(v)(send)<-ch≡ch.recv()(receive; lossy — usech.recv_into(&out)for errors)
<-is the glued two-character operator;a < -b(with a space) is still a comparison against a negation.- Result-style send/recv:
ch.recv_result()returnsRecvResult<T>(Ok(T)/Closed/Timeout/Cancelled/Error(num)) andch.send_result(v)returnsSendResult(Ok/Closed/Full/Cancelled/Error(num)) —fiton them instead of decoding a numeric status. Timeout/token andtry_*/*_asyncvariants exist for both. std.taskWaitGroup:wait_group_new(n)+wg.done()in each task +wg.wait()blocks until allnforked tasks complete.
imp std.channel;
async fun produce(Channel<num>* out, num v) {
out <- v * v;
}
fun main() num {
Channel<num> ch = channel_new_cap(0, 8);
fork produce(&ch, 2);
fork produce(&ch, 3);
ret (<-ch) + (<-ch); // 4 + 9 = 13 (arrival order)
}
Control Flow
If / Elif / Else
fun main() {
num x = 1;
if x > 0 {
_ = x;
} elif x == 0 {
_ = x;
} else {
_ = x;
}
}
For
- Range:
for i : 0..10 { ... } - Array:
for item : arr { ... } - Indexed:
for i, item :: arr { ... }(arrays andVeconly — the source must be indexable) - Iterator:
for item : collection { ... }— any value whose type implements theIteratorquirk, or exposes aniter()method returning one, can be iterated. The compiler desugars it to the iterator'snext()/Optionprotocol, soVec,Set, andMap(over its keys) all work:for n : my_vec { ... },for x : my_set { ... },for key : my_map { ... }.break/continuein the body target the loop as usual. - Map pairs:
for k, v :: map { ... }binds each key tokand its value tov. - While-style (condition):
for i < len { ... } - Infinite loop:
for true { ... } - This style is used in stdlib (for example
std/string.fn,std/net.fn, andstd/fs.fn).
Fit (Pattern Matching)
enum Color { Red, Green, Blue }
fun main() {
Color c = .Red;
fit c {
.Red -> { _ = c; },
.Green -> { _ = c; },
_ -> { _ = c; }
}
}
- Missing variants may produce warnings unless
_is present.
Data-carrying enums (tagged unions)
A variant may carry a positional payload, making the enum a tagged union (sum
type). Construct longhand Enum.Variant(args) or shorthand .Variant(args)
(when the expected enum type is known). Pattern matching destructures the payload
into locals.
compound Vec2 { num x; num y; }
enum Shape {
Circle(num), // primitive payload
Rect(num, num), // multiple payload fields
At(Vec2), // compound payload (by value)
Empty, // payload-free variant
}
fun area(Shape s) num {
fit s {
Shape.Circle(r) -> { ret r * r; } // r binds the payload
Shape.Rect(w, h) -> { ret w * h; } // w, h bind the payload
Shape.At(p) -> { ret p.x + p.y; } // p is the compound payload
Shape.Empty -> { ret 0; }
}
ret -1;
}
fun main() {
Shape s = .Circle(5); // shorthand construction
_ = area(s);
}
- A tagged-union enum lowers to a C
struct { Enum_tag tag; union { ... } payload; }. - Payloads may be primitives, compounds (by value), or monomorphized generic
instances (
Boxed(Box<num>)). fitexhaustiveness (fit_non_exhaustive) covers data variants too; cover all variants or add_.
Defer
- Expression:
defer close(fd); - Block:
defer { cleanup(); } - Runs in LIFO order when the current lexical scope exits.
- Function-scope defer runs before
retand before implicit function end. - Loop-body defer runs at the end of each iteration.
continueandbreakrun defers from the current iteration before transferring control.
Inline Assembly
fun main() {
num x = 21 + 21;
num y = 0;
asm arch x86_64 volatile (out y: "=r" = y; in x: "r" = x; clobber "memory") {
movq %[x], %[y]
};
asm arch aarch64 volatile (out y: "=r" = y; in x: "r" = x; clobber "memory") {
mov %[y], %[x]
};
_ = y;
}
asm arch x86_64 { ... };guards by target architecture.- Use arch guards when the inline assembly differs by ISA (x86_64 vs aarch64).
- Operand names use the
%[name]syntax inside templates. - Outputs come first, then inputs, then clobbers (GCC-style extended asm).
volatileprevents the compiler from removing or reordering the asm.- Prefer the string form when you need precise escaping or newlines:
asm "...";. - Always list
"memory"in clobbers when the asm reads/writes memory not mentioned in operands.
Inline Assembly Pitfalls
- ISA mismatch: AArch64 register names (
x0) will not assemble on x86_64. Guard by arch. - Operand order: x86_64 uses
movq src, dst(AT&T syntax) while AArch64 usesmov dst, src. - Missing size suffix: x86_64
movneeds a size suffix (movb/movw/movl/movq). - Implicit clobbers: If the asm touches memory not listed in operands, include
"memory". - Named receiver errors: If you move values into locals via asm, ensure
outtargets are assigned to named locals. - Block contents are preserved as raw text (including whitespace/comments).
- Fun does not validate asm syntax inside the block; final validity is determined by the selected C toolchain assembler/dialect.
- Example:
jmp $can fail under clang/GAS inline asm, while local-label form (1: ... jmp 1b) is often accepted in that dialect. - Pass computed values through operands, e.g.
num x = 21 + 21; num y = 0; asm volatile (out y: "=r" = y; in x: "r" = x) { mov %[x], %[y] };. - Use string form for explicit escaping:
asm "...";.
C Interop
- Import C headers via
imp std.c.*;. - C constants (e.g.,
NULL,INT_MAX) are allowed when headers are imported. nummaps toint64_t; forprintf, usePRId64(from<inttypes.h>) or cast tolong longand use%lld.
C Compiler Selection
By default, fun uses zig cc. You can override the compiler with environment variables:
FUN_CC: compiler command. If it contains{src}and{out}, it is treated as a full template.FUN_CC_ARGS: extra arguments appended after the base command.
Examples:
FUN_CC=clangFUN_CC=zigandFUN_CC_ARGS="cc"FUN_CC="clang -O2 {src} -o {out}"
Runtime Backend Selection
std.runtime_backend selects the runtime backend with this precedence:
FUN_RUNTIME_BACKEND(posix/windowsor1/2)FUN_RUNTIME_OS(posix/unix/windows)- Host hints from environment (
OS,OSTYPE) - Fallback to
posix
std.thread_runtime and std.sync_runtime follow the same selector.
Formatting
fun -fmt -in file.fnformats a file in place.fun -fmt-all -in file.fnformats local imports (skipsstd.*).- Asm block contents are preserved as raw text.
Tooling (fls)
- Language Server for diagnostics and formatting.
- On save with format-on-save enabled, fls uses a single
fun -fmt-diag -no-execsubprocess to format the file and collect diagnostics simultaneously, avoiding the cost of two sequential compiler invocations. - When format-on-save is disabled, diagnostics use
fun -no-execunder the hood. - A 1500 ms debounce prevents redundant diagnostic subprocess launches when formatting already ran one on the same save event.
VS Code Debug Experience
- ▶ Run and ⚙ Debug code lenses appear above every
fun main(declaration. - ▶ Run compiles and runs the file in an integrated terminal.
- ⚙ Debug performs a three-step build:
fun -g -no-exec -outf→ C compiler with-g//Zi→ native debugger launch. -gembeds#line N "file.fn"directives in the generated C so DWARF maps directly to Fun source lines.- Breakpoints, call stack, and step-through work on
.fnfiles without any manual configuration. - Variable types are remapped from C (
int64_t,char*,bool, …) to Fun (num,str,bin, …) via a DAP message tracker. - Internal C boilerplate frames (
__fun_async_entry_*, etc.) are marked secondary and collapsed in the call stack. - Temp
.cand compiled binary files are created in the OS temp directory and deleted automatically when the session ends. - Debugger auto-detection order:
fun.debugger.typesetting → CodeLLDB → cpptools → platform default (CodeLLDB on macOS/Linux, cpptools on Windows). - On Windows: tries
clang-cl(DWARF, works with CodeLLDB) thencl.exe(CodeView, works with cpptools MSVC engine).
Standard Library (high level)
std.array: array helpersstd.io: file helpers + print utilitiesstd.vec: dynamic vectorsstd.map: generic maps (Map<K, V>) with typed keys/values and bytewise hashed lookups by defaultstd.set: sets built on mapsstd.option: genericOption<T>containerstd.result: genericResult<T>containerstd.collections: collection quirks (len/is_empty)std.string: string helpersstd.channel: bounded blocking channels (ring buffer) with timeout send/recv, non-blockingtry_send/try_recv, cancellation-aware send/recv helpers (send_with_cancel,recv_into_with_cancel, token variants*_with_token, timeout variants), default-branch select helpers (select_recv_default_with,select_recv3_rr_default_with), cancellation-aware select APIs (*_with_cancel, token variants*_with_token), dedicated cancel tokens (ChannelCancelToken,channel_cancel_token_*), status helper symbols (channel_rc_*), select index helpers (channel_select_index_*), and channel-level/per-call select wait-slice/backoff tuning (timeout and blocking variants), with synchronization routed throughstd.sync_runtimestd.thread: POSIX-backed thread helpers (thread_new, method and helper forms for start/join/detach)std.runtime_backend: shared runtime backend selector helpers (runtime_backend_*) with environment overrides (FUN_RUNTIME_BACKEND,FUN_RUNTIME_OS)std.thread_backend_posix: POSIX thread backend module (thread_backend_posix_*) used bystd.thread_runtimestd.thread_backend_windows: Windows thread backend module (thread_backend_windows_*) with direct thread lifecycle operations overstd.c.thread_windowsstd.thread_runtime: backend-facing thread runtime shim (runtime_thread_*) plus backend selector helpers (thread_runtime_backend_*), routed throughstd.runtime_backendand backend modulesstd.thread_pool: POSIX-backed thread pool helpers (thread_pool_new, start_all/join_all/detach_all) routed throughstd.thread_runtimestd.sync_backend_posix: POSIX sync backend module (sync_backend_posix_*) used bystd.sync_runtimestd.sync_backend_windows: Windows sync backend module (sync_backend_windows_*) with direct mutex/condvar operations overstd.c.thread_windowsstd.sync_runtime: backend-facing sync runtime shim (runtime_mutex_*,runtime_condvar_*) plus backend selector helpers (sync_runtime_backend_*), routed throughstd.runtime_backendand backend modulesstd.sync: POSIX-backed mutex/condition variable helpers (method and helper forms)std.json: typed JSON via theJsonValuedata enum (Null/Bool/Num/Str/Array/Object);parse(str) -> Result<JsonValue>, Option-returning accessors (as_num/as_str/as_bool/as_array/get(key)/index(i)/len/is_null), andto_string/stringify. Structured (de)serialization of your own compounds via theToJson/FromJsonquirks (hand-implemented — Fun has no reflection).std.toml: typed flatkey = valueTOML via theTomlValueenum (Str/Int/Float/Bool);parse_document, typedget(key) -> Option<TomlValue>,as_int/as_float/as_str/as_bool, andstringify.std.serde: the text-layerSerialize/Deserializequirks +to_string/from_string, shared byJsonValueandTomlDoc.std.time,std.rand,std.math,std.path,std.net, etc.std.sys: environment and process helpers (sys_exit,sys_abort,sys_system)std.net: URL parsing + pure Fun POSIX TCP/HTTP helpers (POSIX sockets)
CLI
fun -in <input_file> [-out <output_file>] [-no-exec] [-outf] [-ast] [-help]
Errors and Warnings
- Type mismatches, unknown symbols, and incomplete quirk implementations are errors.
- Pointer-return, fit exhaustiveness, redundant fit branches, unreachable statements, constant assertions, and optional unused-* diagnostics are emitted as warnings.
Warning IDs
return_local_ptrfit_non_exhaustivefit_unreachable_branchunreachable_codeassert_constantunused_variable(with-warn-unused)unused_import(with-warn-unused)unused_function(with-warn-unused)unused_compound(with-warn-unused)missing_return(non-voidfunction may reach its end without returning; always checked)
Warning Control Statements
allow <warning_id>, "reason";expect <warning_id>, "reason";
Rules:
- Valid inside function bodies. At module scope, only
unused_variable,unused_import,unused_function, andunused_compoundmay be controlled, and they apply to the next top-level declaration or import in file order. reasonmust be a string literal.allowsuppresses the next warning emitted with the given ID.expectsuppresses the next warning emitted with the given ID and fails compilation if that warning is never emitted.
Example:
fun main() {
bin x = true;
allow fit_non_exhaustive, "temporary while migrating branches";
fit x {
true -> { }
}
}
Expect example:
fun main() {
bin x = true;
expect fit_non_exhaustive, "guard intentional partial fit during migration";
fit x {
true -> { }
}
}
Additional examples:
- examples/advanced/warning_allow.fn
- examples/advanced/warning_expect.fn
- examples/advanced/return_local_ptr_allow.fn
- examples/advanced/unused_variable_warning.fn
- examples/advanced/unused_variable_allow.fn
- examples/advanced/unused_variable_expect.fn
- examples/advanced/unused_import_warning.fn
- examples/advanced/unused_import_allow.fn
- examples/advanced/unused_import_expect.fn
- examples/advanced/unused_function_warning.fn
- examples/advanced/unused_function_allow.fn
- examples/advanced/unused_function_expect.fn
- examples/advanced/unused_compound_warning.fn
- examples/advanced/unused_compound_allow.fn
- examples/advanced/unused_compound_expect.fn
- examples/advanced/fit_unreachable_branch_warning.fn
- examples/advanced/unreachable_code_warning.fn
- examples/advanced/assert_constant_warning.fn
- examples/error_cases/warning_expect_unmet.fn (expected compile failure)