Fels Language Overview
Fels is an experimental systems programming language, implemented in Zig. A few principles shape its design:
- No hidden allocations
- Dynamic dispatch is explicit, through
*Interfacevalues - Static polymorphism comes from monomorphized generics
- Allocation zones give you lexical ownership without a GC
- Runtime checks trap out-of-bounds indexing, null pointers and overflow close to their
source. They stay on in release builds too, and
release_fastis the opt-out.
Getting Started
Section titled “Getting Started”To build Fels and run its tests:
zig buildzig build testTo build with the LLVM backend, pass the path to your LLVM:
zig build -Dllvm-path=path/to/llvmThese compiler commands get you started:
fels check <file> Parse and type-checkfels ir <file> Print LLVM IRfels object <file> Emit object filefels build <file> Build an executable for the host OS, or for --target linux / windowsfels run <file> Build it and run it (-- <args> passes arguments to the program)fels build <file> -g With debug info: a .pdb beside a Windows program, DWARF in a Linux onefels build <file> --target uefi A UEFI application (.efi), no operating systemfels build <file> --target freestanding An ELF with no operating system, for a kernelfels build <file> --target aarch64-freestanding The same for 64-bit ARM (also aarch64-uefi)fels build <file> --link-arg <flag> Pass a flag to the linker (e.g. --script=kernel.ld)Hello World
Section titled “Hello World”#import std.io.{ println };
main :: fn() -> i32 { println("hello from Fels"); return 0;}Language at a Glance
Section titled “Language at a Glance”Here is a quick tour of the syntax in one file:
#import std.io.{ IoError, print, println };
// Constants use :: (compile-time binding)MAX_SIZE :: 1024;
// Struct declarationGreeting :: struct { name: []const u8, count: i32,}
// Error typeAppError :: errors { not_found, overflow,}
// Enum with payloadsColor :: enum { red, green, blue, custom: u32,}
// InterfaceSink :: interface { write :: fn(self: *Self, data: []const u8) -> usize ! IoError; flush :: fn(self: *Self) -> void ! IoError;}
// Interface implementationGreeting as Sink { write :: fn(self: *Self, data: []const u8) -> usize ! IoError { return print(data); } flush :: fn(self: *Self) -> void ! IoError {}}
// Function with genericsidentity :: fn(value: $T) -> T { return value;}
// Fallible functionparse :: fn(input: []const u8) -> i32 ! AppError { if input.len == 0 { fail .not_found; } return 42;}
main :: fn() -> i32 { // Type-inferred locals x := 42; name := "Fels";
// Typed locals count: i32 = 10; data: [4]u8 = .{ 1u8, 2u8, 3u8, 4u8 };
// Pointers ptr := &count; *ptr = 20;
// Slices slice := data[:]; first := slice[0]; sub := data[1..3];
// Control flow if x > 0 { println("positive"); } else { println("non-positive"); }
// While loop i := 0; while i < 10 { i += 1; }
// For range for j in 0..10 { // j goes from 0 to 9 }
// For slice for val in slice { // iterate over elements }
// Match color := Color.red; match color { .red => { println("red"); }, .green => { println("green"); }, .blue => { println("blue"); }, .custom |value| => { println("custom {value}"); }, }
// Error handling result := parse("hello") catch err { return 1; };
// Zones (arena allocation) zone scratch: Arena { buf := scratch.new_slice(u8, 64); buf[0] = 42u8; // arena freed at end of zone }
return 0;}Design Philosophy
Section titled “Design Philosophy”- Systems language: there is no GC, and you manage memory by hand through zones
- Everything explicit: no allocation is hidden from you, and dispatch is always explicit
- Safety by default: every build except
release_fastcatches the faults listed above at runtime - Simple module system: each file is a module, and
pubcontrols visibility - Growing incrementally: the semantics get completed before more syntax is added
Stability
Section titled “Stability”From version 0.13 on, the syntax is settled: what the syntax reference shows keeps
its form and its meaning. A new feature comes as a form no program uses today, an attribute
(#name) or a builtin, not as a new meaning for something you can already write.
The checks go on getting stricter where the compiler accepted something it should not have, as
0.13 did for writing through a []const T, for numbers of two types that could lose a value,
and for sub-byte integers. The CHANGELOG lists each such change with what to
write instead. While the major version is 0, the standard library’s functions may still change
in a minor version.
Where to Go Next
Section titled “Where to Go Next”- Syntax Reference - Syntax Reference
- Type System - Type System
- Functions & Control Flow - Functions & Control Flow
- Error Handling - Error Handling
- Memory & Zones - Memory Management & Zones
- Modules, Interfaces, and Generics - Modules, Interfaces, and Generics
- Standard Library - Standard Library
- Attributes and Builtins - Attributes & Builtins
- Compile-time execution - Comptime VM
- The Fels Build System (build.fels) - Build System
- C interoperability - C Interop &
#extern - Reflection, Any, and constraints - Reflection & Generic Constraints
- Metaprogramming: Macros, Quote/Insert, and Code Generation - Metaprogramming (
#quote/#insert/macros) - Linux / ELF / SysV backend - Linux/ELF Backend (core implemented)
- Tooling and the language server - Tooling & LSP
- Packages - Packages: fels.pkg, fels.lock and capabilities
- Testing: #test and fels test - Testing &
#test - Targets, ABIs & support tiers - Targets, ABIs & support tiers