A modern compiler for the Ferret programming language. Backend: Ferret uses the official QBE backend as a submodule (git://c9x.me/qbe.git).
- It does what it looks like.: The language syntax and semantics are designed to be intuitive and symbol_table.
- Helpful error messages.: The compiler provides clear and actionable error messages to help developers fix issues quickly.
- Nearly 0 runtime error: The type system and compile-time checks aim to eliminate runtime errors as much as possible.
- Fast compilation.: The compiler is optimized for speed to provide a smooth development experience
git submodule update --init --recursiveFetch the latest commit on the tracked branch (set in .gitmodules):
git submodule update --remote --merge qbeSwitch the submodule to another branch and update:
git submodule set-branch --branch <branch> qbe
git submodule update --remote --merge qbePin the submodule to a specific commit:
cd qbe
git checkout <commit-sha>
cd ..
git add qbeAfter any submodule change, commit the updated qbe gitlink and .gitmodules.
Linux/macOS:
./scripts/install-toolchain.shWindows (PowerShell):
.\scripts\install-toolchain.ps1builds the compiler, toolchains and copies libraries from ferret_libs to libs
./scripts/build.sh
Building with output name -o
./bin/ferret -o app_name filename.fer
Without -o flag, output will be the projectname (or parent folder name is projectname is not set)
- use
--verboseto show detailed build steps/logs - use
-kto keep the intermediate files (.mir, .ssa, .s, .o) - use
-tto typecheck only - use
--cfgto stop after CFG analysis (no lowering/codegen)
go build -o ./bin/ferret main.go && ./bin/ferret code.ferNote: flags must be before any non flag arguments in ferret command. like ./bin/ferret -o execname code.fer, but ./bin/ferret code.fer -o execname will not work.
let x := 10; // Type inference
let y: i32 = 20; // Explicit type
const pi := 3.14; // Constant
There is nothing like truethy or falsy values in Ferret. Only bool type is used for boolean logic.
- Assignment/call/return are copy-by-default for implicitly copyable values (primitives and structurally copyable aggregates).
- Assignment/call/return are move-by-default for non-copyable owned values (resource handles, heap owners
#T, dynamic containers, and aggregates containing them). &x/&mut xare borrow operations, and nested references are allowed (no auto-deref).
- Non-copyable values now move implicitly in assignment, call, and return positions.
- If you need to keep using a value after passing it, pass a borrow (
&/&mut) or redesign the API to borrow.
// Basic types
i8, i16, i32, i64 // Signed integers
u8, u16, u32, u64 // Unsigned integers
f32, f64 // Floats
str, bool, byte // String, boolean, byte
// Arrays
[3]i32 // Fixed-size array (compile-time bounds checking)
[]i32 // Dynamic array (auto-grows, no bounds checking)
// Both support negative indexing: arr[-1] accesses last element
// Optional types
?i32 // Nullable integer
// Result types
Error ! Data // Used with functions that can fail
fn get_data() -> str ! i32 { // Error type first, success type second
// ...
if fail {
return "Failed to get data"!; // returning str as error. The `!` operator marks an expression as error
}
return 42; // returning i32 as success value
}
Ferret now supports top-level constraint declarations for upcoming generics work:
constraint numeric = union {
i32, i64, i128, i256
};
constraint signed_like = union {
~i32, ~i64, ~i128, ~i256
};
constraint writer = interface {
Write(buf: []byte) -> i32
};
constraint numeric_writer = numeric & writer;
Rules:
- Use one declaration form:
constraint Name = <expr>; &combines constraints (intersection)~enables underlying-type matching for named/primitive type terms
Generics and generic bounds usage (T: Constraint) are still in progress.
type Point struct {
.x: f64,
.y: f64
};
let p : Point = { .x = 1.0, .y = 2.0 }; // From the value (anonymous struct literal), type is inferred to Point because of the variable type
But what if the type is not specified and we want to create a Point from the literal? Other languages may use this syntax: Point{ x: 1.0, y: 2.0 }. But we keep behaviors consistent and use the same syntax for both cases.
let p2 := { .x = 3.0, .y = 4.0 } as Point; // Here we use `as` to cast the anonymous struct literal to Point.
Every type can be anonymous. Like structs, interface, enums, functions etc.
type Color enum {
Red,
Green,
Blue
};
let color := Color::Red;
The :: operator is used to access static members of types like enums and modules (symbols on other files).
Ferret uses capitalization to control visibility (like Go):
- Uppercase names are exported (public)
- Lowercase names are private
// Module-level symbols
const MAX := 100; // Exported (uppercase)
const internal := 42; // Private (lowercase)
fn Add(a: i32, b: i32) -> i32 { ... } // Exported function
fn helper() { ... } // Private function
type Point struct { // Exported type
.X: i32, // Exported field (uppercase)
.y: i32 // Private field (lowercase)
};
type internal struct { ... }; // Private type
Struct field visibility:
- Uppercase fields (
.X,.Name) are public - accessible everywhere - Lowercase fields (
.x,.name) are private - only accessible:- Within methods of the same type (via receiver)
- In struct literal construction (to provide values)
- Direct field access
obj.fieldon private fields is not allowed outside methods
// In any module
type Point struct {
.X: i32, // public - accessible everywhere
.y: i32 // private - restricted access
};
fn (p: Point) GetY() -> i32 {
return p.y; // ✅ OK - method can access private field
}
fn (p: &Point) SetY(val: i32) {
p.y = val; // ✅ OK - method can modify private field
}
fn test() {
// ✅ OK - struct literal can set all fields
let p := { .X = 10, .y = 20 } as Point;
let x := p.X; // ✅ OK - public field
let y := p.y; // ❌ Error - private field access
let y2 := p.GetY(); // ✅ OK - use method for controlled access
}
Cross-module access:
// In module A
type Point struct {
.X: i32, // public
.y: i32 // private
};
// In module B
import "moduleA";
let p := { .X = 10, .y = 20 } as moduleA::Point; // ✅ Can construct
let x := p.X; // ✅ OK - public field
let y := p.y; // ❌ Error - private field
Enum variants inherit visibility:
type Color enum { Red, Green, Blue }; // Exported enum
// Color::Red, Color::Green, Color::Blue are all exported
type internal enum { A, B }; // Private enum
// internal::A and internal::B are also private
import "std/math";
// The alias 'math' is now reserved
// Cannot declare variables, constants, functions, or types named 'math'
// This would cause a compile error:
// let math := 42; // Error: 'math' is already used as an import alias
// Use custom aliases to avoid conflicts:
import "std/math" as m;
let math := 42; // OK now, 'm' is the import alias
fn add(a: i32, b: i32) -> i32 {
return a + b;
}
Ferret now supports generic syntax and type-checking with <>:
constraint numeric = union { i32, i64 };
fn add<T: numeric>(a: T, b: T) -> T {
return a + b;
}
fn main() -> i32 {
let x := add(1, 2); // inferred T = i32
let y := add<i64>(1 as i64, 2 as i64); // explicit type args
let ignored := y;
return x;
}
Current limitation: generic code generation is not implemented yet. Use -t to type-check generic code:
./bin/ferret -t file.ferlet maybe: ?i32 = None;
let value := maybe ?? 42; // Defaults to 42 if None. Kind of like `??` in other languages. Or we may switch to `??` later. making ?? a ternary operator.
fn divide(a: i32, b: i32) -> i32 ! Error {
if b == 0 {
return Error{ .msg = "Division by zero" }!;
}
return a / b;
}
const result := divide(10, 2) catch err {
println("Error: {}", err.msg);
} 0; // Fallback value
If you return from the handler block, the function will return early with that value and you don't need to provide a fallback value. Also you can use shorthand syntax like,
const result := divide(10, 2) catch 0; // Fallback value
// Fixed-size arrays with compile-time bounds checking
let arr: [5]i32 = [1, 2, 3, 4, 5];
let x := arr[2]; // OK
let y := arr[10]; // Compile error: index out of bounds
// Dynamic arrays auto-grow (no bounds checking)
let dyn := [1, 2, 4]; // size 3
dyn[5] = 43; // grows to [1, 2, 4, 0, 0, 43]
// Negative indexing (both fixed-size and dynamic)
let last := arr[-1]; // Last element
let second_last := arr[-2]; // Second to last
// Dynamic arrays (runtime bounds checking)
let dyn: []i32 = [1, 2, 3];
let val := dyn[100]; // Runtime check, not compile-time
if x < y {
println("x is less");
} else {
println("x is greater or equal");
}
for i, v in arrayLike { // i is read-only (loop index)
println(i, v);
}
// skip index or value using _
for _, v in arrayLike {
println(v);
}
for i, _ in arrayLike {
println(i);
}
for _, _ in arrayLike {
// do something
}
while x < 5 {
print(x);
x = x + 1;
}
// match statement
match value {
1 => println("one"),
2 => println("two"),
_ => println("other") // default case
}
Ferret provides beautiful, helpful error messages:
error[T0001]: type mismatch
--> example.fer:10:15
|
10 | let num: i32 = "hello";
| ~~~~~~~ expected i32, found str
|
= note: type inference determined this expression has type 'str'
= help: convert the string to an integer using the parse function
Compilation failed with 1 error(s)
error: integer literal 200 overflows i8
--> example.fer:13:13
|
12 | let e: i8 = 100; // OK: literal 100 fits in i8
13 | let f: i8 = 200; // Error: 200 doesn't fit in i8 (-128 to 127)
| - ~~~ need at least u8 or i16
| |
| -- type 'i8'
|
= help: i8 can hold values in range: -128 to 127
See LICENSE for details.
Contributions are welcome! Please read the documentation to understand the compiler architecture before submitting PRs.
Feel free to open issues or ask questions in the discussions section. We appreciate your interest in improving Ferret!