Variables and Constants
let
let declares a local binding. Bindings are immutable by default:
let timeout_ms = 250
let name: string = "flint"
Use let mut for a binding you intend to modify:
let mut count: u32 = 0
count = count + 1 // ok
let x = 5
x = 6 // error: cannot assign to immutable binding
Type annotations are optional when the type is obvious from context. Function signatures, struct fields, const, and static always require explicit types.
const
Constants are compile-time values. They always require a type:
const MAX_RETRIES: u32 = 3
const DEFAULT_BAUD: u32 = 115_200
const BANNER: string = "Flint 1.0-dev"
Rules:
- Always immutable. There is no
const mut. - Initializer must be computable at compile time.
- Usable anywhere a value of that type is needed.
- Naming convention:
UPPER_SNAKE_CASE.
static
static is for module-scope storage with a fixed program lifetime:
static BOOT_BANNER: string = "Flint"
Rules:
staticis immutable. There is nostatic mut.- Must have an explicit type.
- Initializer should be compile-time evaluable.
- Lives in read-only image storage on MCU targets.
Use static when a value must exist for the entire program lifetime, not just at compile time.
Mutability
Mutability in Flint attaches to the binding, not the value. A mutable binding lets you reassign it and call mutating methods:
let point = Point { x: 1, y: 2 }
point.x = 3 // error: immutable binding
let mut point = Point { x: 1, y: 2 }
point.x = 3 // ok
point.translate(1, 1) // ok (translate takes mut self)
Assignment operators work as expected on mutable bindings:
let mut n: u32 = 10
n += 5 // ok
n -= 2 // ok
n *= 3 // ok
There is no ++ or --. Use += 1 and -= 1.
Type Inference
Local let bindings may omit the type when it is clear from context:
let x = 42 // i32 (default for unsuffixed integer)
let y = 1.5 // f32 (default for unsuffixed float)
let s = "hello" // string
let pin = gpio.pin(25) // inferred from gpio.pin return type
When inference is ambiguous, you must annotate:
let buf = [0; 64] // error: what is the element type?
let buf: [u8; 64] = [0; 64] // ok
Shadowing
Nested scopes may shadow outer bindings:
let value = 10
{
let value = value * 2 // shadows outer `value` in this scope
log.info(value) // 20
}
log.info(value) // 10 (outer binding restored)
Same-scope shadowing is a compile error:
let port = 8080
let port = 9090 // error: redeclaration in the same scope
let mut x = 5
let x = 10 // error: redeclaration in the same scope
Use if let and match for the common pattern of binding an inner refined value:
let raw = env.lookup("PORT")
if let Some(raw) = raw {
let port = u16.try_from(parse_i32(raw)?)?
use_port(port)
}
Option
Option<T> is how Flint represents a value that might not be there. There is no null in Flint. A function that might return nothing says so explicitly in its return type.
fn find_device(id: u32) -> Option<Device> {
// returns Some(device) or None
}
let device = find_device(42)
// Pattern match to unwrap safely
match device {
Some(d) => d.init(),
None => log.warn("device not found"),
}
// Or use if let for the happy path
if let Some(d) = find_device(42) {
d.init()
}
If you only need to know whether a value is present and do not need to bind it, use is_some() or is_none() instead of if let Some(...).
None can never be dereferenced. The compiler forces you to handle it before using the value. No null pointer exceptions, no segfaults from a forgotten check.
See Error Handling for how Option composes with ? and Result.
Result
Result<T, E> is how Flint represents operations that can fail. There are no exceptions and no try/catch. A function that can fail says so in its return type, and the caller decides what to do.
fn read_sensor() -> Result<u16, Error> {
// returns Ok(reading) or Err(reason)
}
// Handle explicitly
match read_sensor() {
Ok(v) => process(v),
Err(e) => log.error(e),
}
// Or propagate with ?
fn run() -> Result<(), Error> {
let reading = read_sensor()? // returns Err early if it failed
process(reading)
return Ok()
}
? is the idiomatic way to propagate errors up the call stack without nesting. Control flow is never interrupted invisibly. Every exit path is visible in the source.
When the success type is (), Ok() is enough. The unit value is inferred.
See Error Handling for the full picture.
Global Mutable State
Raw mutable globals do not exist in Flint. For global initialization patterns, use core/sync:
use core/sync
// Single-assignment cell, safe to access from any code after init
static DEVICE_ID: sync.OnceCell<u32> = sync.OnceCell.new()
fn init_device() {
DEVICE_ID.set(read_unique_id())
}
core/sync.OnceCell<T> and core/sync.Lazy<T> are the safe patterns for globals that need runtime initialization.