Linux-only programming language with LLVM backend, C runtime, and full FFI for .so libraries (CUDA/NCCL-capable).
This README has two main parts:
- Language guide – how to write and run Fusion programs.
- Build guide – how to build the compiler and runtime from source.
For a deeper architecture and project roadmap, see fusion-design-document.md.
Fusion is a small, systems-style language designed to call into existing C APIs (including CUDA and NCCL) with as little ABI friction as possible. Programs are compiled to LLVM IR and JIT-executed against a C runtime that handles printing, file I/O, dynamic loading (dlopen/dlsym), and libffi-based FFI calls.
At a high level, a .fusion file looks like this:
- Optional
opaqueandstructtype declarations. - One or more
extern libandextern fndeclarations describing C functions in.solibraries. - Zero or more user
fndefinitions. - A sequence of top-level
letbindings,if/elif/elsestatements,forloops, assignments, and expressions that are executed in order.
The compiler synthesizes a fusion_main function that evaluates the top-level items sequentially; user-defined functions are called from that top-level code or from other functions.
Create example.fusion:
struct Point { x: f64; y: f64; };
extern lib "libm.so.6" {
fn cos(x: f64) -> f64;
};
fn shift(v: f64) -> f64 {
let x = v + 1;
return x;
}
let z = cos(1.0);
print(z);
let p = heap(Point);
p.x = 3.0;
p.y = 4.0;
print(p.x);
print(p.y);
let x = z + 2;
if (x > 0) {
print(1);
} else {
print(0);
}
print(shift(x));
Build Fusion (see the build section below), then run:
./build/compiler/fusion run example.fusionor equivalently:
./build/compiler/fusion example.fusionfusion will:
- Lex and parse the source,
- Run semantic checks (types, arity, FFI declarations, etc.),
- Generate LLVM IR and JIT it,
- Execute the synthesized
fusion_mainthat runs your top-level code.
Top-level items are processed in the following order:
opaquetype declarations, e.g.:opaque cudaStream_t;structdefinitions, e.g.:struct Point { x: f64; y: f64; };extern libandextern fndeclarations:or a block form:extern lib "libm.so.6"; extern fn cos(x: f64) -> f64;extern lib "x.so" { fn foo() -> void; fn bar() -> i64; };import libdeclarations (for multi-file programs) andexport struct/export fn(for library modules).- User-defined functions:
fn f(x: i64) -> i64 { if (x > 0) { return 1; } return 0; } - A sequence of top-level items executed in order:
letbindings, e.g.let x = 1 + 2;if/elif/elsestatementsforloops- Assignments, e.g.
a[0] = 42; - Standalone expressions (commonly
print(...)calls)
The last expression is not special; there is no REPL-style implicit print. All output is explicit via print.
- Comments start with
#and run to the end of the line:# This is a comment - Literals:
- Integer literals:
0,1,42 - Float literals:
0.0,3.14,1e-3 - String literals:
"hello","path/to/file.txt"
- Integer literals:
- Punctuation:
- Parentheses
(), braces{}, brackets[] - Comma
,, semicolon;, colon:
- Parentheses
- Operators:
- Arithmetic:
+,-,*,/ - Comparison:
==,!=,<,<=,>,>=
- Arithmetic:
- Statement termination:
- Inside blocks and at top level, most statements end with
;. ifandforintroduce blocks with{ ... }instead of;at the end of the header line.
- Inside blocks and at top level, most statements end with
Fusion’s core value-level types correspond directly to FFI-level types:
i8,i32,i64– signed integersu32,u64– unsigned integersf32,f64– floating-point numbersptr– raw pointer (opaque at the language level; useptrwherever you would passconst char*orvoid*in C)void– used as a function return type
String literals have pointer type and can be passed where a ptr is expected.
There is currently no dedicated bool type. Conditions are numeric:
- Comparison operators yield an
i64“boolean” (0 or 1) used inifandforconditions.
Opaque types are names for ABI-level pointers that Fusion never inspects:
opaque cudaStream_t;
Struct types are C-layout records:
struct Point {
x: f64;
y: f64;
};
Struct field types must be primitive FFI types (e.g. i64, f64, ptr). Layout (size, alignment, offsets) is computed according to the SysV AMD64 ABI and is validated in tests.
When structs or opaque types flow across the FFI boundary, they are represented as pointers:
- In
extern fndeclarations, parameters and returns that use a named type (opaque or struct) are lowered to pointer types at the ABI level.
- Integer literals default to
i64. - Float literals default to
f64. - Binary arithmetic (
+,-,*,/):- If either operand is
f64, the result isf64. - Otherwise, the result is
i64.
- If either operand is
This means that:
1 + 2isi64cos(1.0) + 2isf64and staysf64(tests assert that result is not truncated).
Common expression forms include:
- Arithmetic:
1 + 2 * 3,x - 4,y / 2.0 - Comparison:
x > 0,a == b,p != q - Variable references:
x,point - Function calls:
print(1),cos(0.0),f(x, y) - Array indexing:
a[0],points[i] - Pointer operations and loads (covered later)
Fusion supports explicit casts using as:
let x = 1 as f64;
let y = 3.14 as i64;
let p = some_ptr as ptr;
let s = some_ptr as ptr;
Allowed casts:
- To numeric:
as i64,as i32,as f64,as f32– source must be one of those numeric types. - To pointer-like:
as ptr– source must already be a pointer.
Invalid casts (e.g. casting a non-pointer to ptr, or a pointer to a numeric type) are rejected by semantic analysis.
let introduces a new immutable binding:
let a = 1;
let b = 2;
let sum = a + b;
- At top level,
letbindings live in a single global scope (no shadowing). - Inside functions and blocks (
if/else/forbodies), bindings live in nested scopes tracked by the compiler. Duplicate names in the same scope are rejected.
Syntax:
if (x > 0) {
print(1);
} elif (x < 0) {
print(-1);
} else {
print(0);
}
elifis syntactic sugar for a nestedifin theelsebranch.- Conditions must be comparison expressions over numeric types or pointer equality/inequality (
==/!=). - Pointer comparisons are restricted to
==and!=.
Fusion uses C-style for (init; cond; update) { body } loops:
for (let i = 0; i < 5; i = i + 1) {
print(i);
}
let arr = heap_array(i64, 3);
arr[0] = 1;
arr[1] = 2;
arr[2] = 3;
for (let i = 0; i < len(arr); i = i + 1) {
let x = arr[i];
print(x);
}
print(0);
The loop variable’s type is inferred from the iterable:
- init (optional):
let i = 0ori = 0. cond: e.g.i < n. update (optional): e.g.i = i + 1. - Use
len(arr)to get array length.
Assignments are standalone statements:
let x = 1;
x = x + 1;
let a = heap_array(i64, 3);
a[0] = 42;
Targets:
- A variable:
x = expr; - An indexed array element:
a[i] = expr;
Assignments must be type-compatible. Some limited pointer/int compatibility is allowed where it matches ABI usage (e.g. ptr vs i64 in certain contexts), otherwise the compiler reports an error.
Function definition syntax:
fn sign(x: i64) -> i64 {
if (x > 0) { return 1; }
elif (x < 0) { return -1; }
else { return 0; }
}
print(sign(5));
- Parameters are typed:
name: type. - Return type appears after
->. - Return types and parameter types must be FFI types (
i32/i64/f32/f64/ptr/void) or named struct/opaque types (lowered to pointers). returnis only valid inside functions and must match the declared return type.
These are built-in functions parsed specially by the compiler:
let p = heap(Point);
let xs = heap_array(i64, 10);
let buf = heap_array(i8, 1024); # raw byte buffer
For stack-allocated storage (lifetime limited to the current function):
let p = stack(i64);
let arr = stack_array(f64, 5);
heap(T)/stack(T):Tcan be a primitive (i8,i32,i64,f32,f64),ptr, or a user-defined struct/opaque name.- Returns a
ptrpointing to storage for oneT.
heap_array(T, count)/stack_array(T, count):Tas above,countmust bei64.- Returns a
ptrto an array ofcountelements.
Heap allocations can be freed with free(ptr) and free_array(ptr). Use as_heap(ptr) or as_array(ptr, elem_type) when the compiler cannot infer the allocation origin.
All intrinsics lower to runtime allocations (heap) or LLVM alloca (stack) implemented in the C runtime.
Core pointer operations:
let p = heap(i64);
store(p, 42);
let v = load(p);
let q = addr_of(v); # address of a variable
let f = load_f64(fp); # load as f64
let ip = load_ptr(pp); # load pointer
addr_of(var)– pointer to a variable binding.load(p)– loadi64from a pointer.load_f64(p)– loadf64from a pointer.load_i32(p)– loadi32from a pointer.load_ptr(p)– loadptrfrom a pointer.store(p, value)– store a value through a pointer.
Array indexing:
let a = heap_array(i64, 3);
a[0] = 10;
a[1] = 20;
a[2] = 30;
print(a[0]);
- Base expression must be a pointer produced by
heap_arrayorstack_array(or aletbinding of such). - Index expression must be
i64. - The element type is inferred from the array.
Struct declarations:
struct Point { x: f64; y: f64; };
Usage with dot notation:
let p = heap(Point);
p.x = 3.0;
p.y = 4.0;
print(p.x);
print(p.y);
expr.field– read or write a struct field via dot notation.- The base must be a pointer to a struct.
- For low-level access,
load_field(p, StructName, field)andstore_field(p, StructName, field, value)are also available.
Layout is computed from the struct definition according to the platform ABI and is shared with the FFI system.
print is a built-in with two forms:
print(42); # default stream
print(3.14);
print("hello");
print(42, 1); # optional stream id (i64)
Supported value types:
i64(and other integers widened toi64)f64(and other floats widened tof64)ptr(including string literals and pointers to C strings)
The second argument, when present, must be i64 and is interpreted as a stream identifier by the runtime (e.g., stdout/stderr).
len(arr) returns the length (element count) of an array as i64:
let arr = heap_array(i64, 5);
for (let i = 0; i < len(arr); i = i + 1) {
arr[i] = i;
print(arr[i]);
}
len(arr)–arrmust be a pointer to an array (fromheap_arrayorstack_array). Returns the stored length asi64.
The runtime exposes helpers for converting between strings and numbers:
let line = read_line();
let n = from_str(line, i64);
let s = to_str(n);
print(s);
read_line():- Reads a single line from stdin.
- Returns a
ptr(null-terminated string managed by the runtime).
to_str(x):xmust bei64orf64.- Returns a
ptrto a string representation of the number.
from_str(s, T):smust be a pointer (ptr) to a string.Tisi64orf64.- Returns a numeric value,
0/0.0on invalid input.
The + operator concatenates two ptr (string) values and returns a new ptr:
let s = to_str(42) + " items";
print(s);
let combined = to_str(100) + " " + to_str(2.5);
print(combined);
Both operands must be ptr; the result is ptr. String literals, to_str results, read_line results, and any other ptr-typed values can be combined this way.
Parameter types, struct field types, and casts support an optional [T] annotation on ptr to communicate the pointed-to type:
fn f(p: ptr[Point]) -> void {
print(p.x);
}
struct List {
next: ptr[List];
value: i64;
};
let q = some_ptr as ptr[Point];
ptr[void] is equivalent to bare ptr (opaque pointer). This annotation is purely syntactic — at the ABI and codegen level ptr[T] is lowered identically to ptr.
Note: -> ptr[T] in a return-type position has different semantics (array-element return), which is an existing, unchanged feature.
The C runtime provides simple file APIs surfaced through built-ins:
let path = "data.txt";
let mode = "r";
let f = open(path, mode);
# First, get line count (or use eof_file to loop until EOF)
let total_lines = line_count_file(f);
close(f);
let f2 = open(path, mode);
for (let i = 0; i < total_lines; i = i + 1) {
let line = read_line_file(f2);
print(line);
}
close(f2);
Available operations:
open(path, mode):- Both arguments must be pointer/string values.
- Returns a file handle pointer (opaque).
close(handle)– closes the file handle.read_line_file(handle)– returns next line as a pointer/string.write_file(handle, value):valuecan bei64,f64, or pointer/string.
eof_file(handle)– returnsi64(0 = not EOF, non-zero = EOF).line_count_file(handle)– returnsi64count of lines read so far.write_bytes(handle, buf, n)– writenbytes frombufto file.read_bytes(handle, buf, n)– read up tonbytes intobuf.
These functions are type-checked by the compiler to ensure handles are pointers and value types are compatible.
get_func_ptr(fn)– returns aptrto a user-defined function.call(fp, arg1, arg2, ...)– invokes a function through a pointer (e.g. fromget_func_ptr).rt_panic(msg)– terminates the program with an error message (msgis aptrto a string).
Fusion supports splitting code across multiple .fusion files via import lib and export:
# vec.fusion – library module
export struct Vector { x: f64; y: f64; };
export fn make_vec(x: f64, y: f64) -> Vector {
let p = heap(Vector);
p.x = x;
p.y = y;
return p;
}
# main.fusion – imports the library
import lib "vec" {
struct Vector;
fn make_vec(x: f64, y: f64) -> Vector;
};
let v = make_vec(1.0, 2.0);
print(v.x);
print(v.y);
import lib "name" { ... }– loadsname.fusion(orname/directory) and imports the declared structs and functions.export struct/export fn– makes a struct or function visible to importers.- The import block lists only the struct names and function signatures; the implementation lives in the library file.
You can declare external libraries in two ways.
Simple form:
extern lib "libm.so.6";
extern fn cos(x: f64) -> f64;
The compiler assigns an internal name like __lib0 and binds cos to that library.
Block form with explicit functions:
extern lib "mylib.so" {
fn foo() -> void;
fn bar(x: i64) -> i64;
};
All functions inside the block are associated with the same library.
You can also alias the library name:
extern lib "libm.so.6" as libm;
extern fn cos(x: f64) -> f64;
Syntax:
extern lib "libm.so.6";
extern fn cos(x: f64) -> f64;
or inside a block, as shown above.
Parameter and return types:
- Keyword types (
i32,i64,u32,u64,f32,f64,ptr,void) map directly to FFI primitive kinds. - Named types (structs and opaques) are treated as pointers at the ABI layer.
The compiler verifies:
- That every
extern fnrefers to a declared library. - That names used as struct/opaque types are known.
Fusion’s FFI mapping is defined by the runtime (rt_ffi_type_kind_t) and the semantic layer. In practice:
| Fusion type | C / ABI type |
|---|---|
i8 |
int8_t |
i32 |
int32_t |
i64 |
int64_t / long long |
f32 |
float |
f64 |
double |
ptr |
void* |
| (strings) | use ptr; C side is const char* or void* as appropriate |
| struct/opaque name | pointer to that type (T*) |
The platform assumptions for v1 are:
- Linux x86-64
- SysV AMD64 ABI
- ELF shared libraries (
.so)
Putting it all together:
extern lib "libm.so.6";
extern fn cos(x: f64) -> f64;
let v = cos(0.0);
print(v);
At runtime, the generated code:
- Uses the C runtime to
dlopen("libm.so.6"). - Uses
dlsymto find thecossymbol. - Uses
libffito prepare and invoke a call with onedoubleargument anddoublereturn. - Unmarshals the result back into a Fusion
f64.
More advanced examples (structs by pointer, out-parameters) are covered in tests and example .fusion programs such as example.fusion, example_points.fusion, and train_moons.fusion.
The fusion CLI is built in build/compiler/fusion. It supports:
fusion --help/fusion -h– show help and usage.fusion --version/fusion -v– show compiler and LLVM version.fusion run file.fusion– compile and JIT-run a.fusionfile.fusion file.fusion– shorthand forfusion run file.fusion.
On each run, the compiler:
- Lexes the input.
- Parses into an AST.
- Runs semantic checks (types, FFI declarations, built-in usage, etc.).
- Lowers to LLVM IR, verifies the module, and JITs it.
- Executes
fusion_main.
Errors are reported with a message and (where possible) line/column information from the parser or semantic analyzer.
Some common classes of semantic errors:
- Undefined variable:
let x = 1; print(y); # y is undefined - Wrong arity:
print(1, 2, 3); # print only accepts 1 or 2 arguments - Invalid FFI declaration:
- Extern function refers to an unknown library.
- Extern function uses an unknown struct/opaque type name.
- Invalid
lenargument:let n = 5; let x = len(n); # len expects a pointer (array) - Invalid casts or mis-typed pointer operations:
load/load_f64/load_ptr/load_i32require pointer arguments.storerequires a pointer as the first argument.astarget type must be one of the supported numeric or pointer-like types.
The error messages are designed to be explicit about which built-in or function caused the problem.
Conceptually, Fusion’s execution stack looks like this:
flowchart TD
source["FusionSource(.fusion)"] --> lexer[Lexer]
lexer --> parser[Parser]
parser --> ast[AST]
ast --> sema[SemanticAnalysis]
sema --> codegen[LLVMIRGeneration]
codegen --> jit[LLVMJIT]
jit --> runtimeCRuntime["CRuntime(rt_*)"]
runtimeCRuntime --> ffiEngine["FFIEngine(libffi+libdl)"]
ffiEngine --> soLibs["SharedLibraries(.so)"]
- The compiler frontend (lexer, parser, semantic analysis) is implemented in C++.
- The backend uses LLVM to build and JIT modules.
- The C runtime (
runtime_c) provides printing, file I/O, dynamic loading, and FFI services. - The FFI engine uses
libffiandlibdlto dynamically call into arbitrary.solibraries.
For much more detail on this architecture and the long-term roadmap (including CUDA/NCCL integration and potential bindgen tooling), read fusion-design-document.md.
The simplest way to configure, build, and run all tests:
./make.shFlags:
./make.sh— configure (if needed), build, and run all tests./make.sh -r— clean rebuild from scratch (removesbuild/first)./make.sh -d— debug build with AddressSanitizer (-fsanitize=address -g)./make.sh -n— use Ninja instead of Make (requiresninja-build)
Or manually:
cmake -B build -S .
cmake --build build -j$(nproc)
ctest --test-dir build --output-on-failureSeveral options can speed up rebuilds:
- Ninja generator —
./make.sh -norcmake -B build -S . -G Ninja. Faster incremental builds than Make. - ccache — Enabled by default (
-DFUSION_USE_CCACHE=ON). Install withsudo apt install ccache. Runccache -sto check hit rates. - Faster linker (mold/lld) —
cmake -B build -S . -DFUSION_USE_FAST_LINKER=ON. Triesmoldfirst, thenlld. Install withsudo apt install moldorsudo apt install lld. - Precompiled headers — LLVM headers in
codegen.cppare precompiled automatically via CMake'starget_precompile_headers, speeding up rebuilds of codegen.
fusion --help— Run./build/compiler/fusion --helpfor usage.- Runtime —
build/runtime_c/libruntime.soandbuild/runtime_c/libruntime.aare produced. - Tests —
ctest --test-dir buildruns the C test runner and thefusion --helptest.
Fusion ships a Language Server Protocol implementation in the lsp/ directory. After building, the binary is at build/lsp/fusion_lsp.
Build the LSP server only:
cmake --build build --target fusion_lspFeatures:
- Hover documentation
- Go to definition
- Document and workspace symbols
- Completion (keywords, functions, variables)
- Signature help
- Document highlight and references
- Rename symbol
- Semantic token coloring (keywords, types, functions, variables, parameters)
- Folding ranges
- Inlay hints (parameter names at call sites)
- Diagnostics (errors from the compiler, with multi-error support)
VS Code / Cursor: See vscode-fusion/ for the extension. Install it from the vscode-fusion folder (Developer: Install Extension from Location...). The extension auto-detects build/fusion_lsp relative to the workspace root, or you can set fusion.serverPath in VS Code settings.
On systems where libffi or zlib are not available as dev packages (e.g. no sudo), you can install them under ~/.local and point the build there.
1. Install libffi
mkdir -p "$HOME/src" "$HOME/.local"
cd "$HOME/src"
curl -LO https://github.com/libffi/libffi/releases/download/v3.4.6/libffi-3.4.6.tar.gz
tar -xzf libffi-3.4.6.tar.gz
cd libffi-3.4.6
./configure --prefix="$HOME/.local"
make -j
make install2. Install zlib (if you see link errors for compress2, uncompress, crc32)
cd "$HOME/src"
curl -LO https://zlib.net/zlib-1.3.1.tar.gz
tar -xzf zlib-1.3.1.tar.gz
cd zlib-1.3.1
./configure --prefix="$HOME/.local"
make -j
make install3. Use them when building
Source the env script so CMake and the compiler find headers and libs in ~/.local:
source ./env_local_deps.sh
./test.shOr for a manual build: source ./env_local_deps.sh then run cmake -B build, cmake --build build, etc.
Sanity check (after sourcing): pkg-config --modversion libffi and pkg-config --modversion zlib should print versions.
Option A via CMake: If you prefer not to install libffi manually, you can have CMake download and build it into the build tree:
cmake -B build -S . -DFUSION_FETCH_LIBFFI=ON
cmake --build buildThis uses ExternalProject to fetch libffi 3.4.6 and install it under build/_deps/libffi-install (no ~/.local or env_local_deps.sh needed).
LLVM is required to build Fusion. If LLVM is not installed, CMake will try to find it; on Linux, if not found, CMake can download a pre-built LLVM into build/_deps/llvm on first configure (set FUSION_DOWNLOAD_LLVM=ON, which is the default on Linux).
Auto-download (no sudo): On Linux, if LLVM is not found, CMake downloads a pre-built LLVM (x86_64 Linux) into build/_deps/llvm. To disable auto-download and supply LLVM yourself:
cmake -B build -S . -DFUSION_DOWNLOAD_LLVM=OFFTo use a different LLVM version when auto-downloading:
cmake -B build -S . -DFUSION_LLVM_VERSION=18.1.7If LLVM cannot be found or downloaded, configure will fail with an error.