Optionals & Variants
Optionals
Nullable type, written inline as T?. There is no implicit nullability as values are always initialized.
// 'none' is void? (zeroinitializer)
i32? maybe = none;
maybe = 69;
Unwrapping
// force-unwrap; hard-crashes on none
i32 v = maybe!;
Behaviour on empty access is configurable (flintc --optional <mode>).
Comparing
if maybe == none:
...
if maybe != none:
...
i32? a = 5;
i32? b = 5;
// equal if both none or both hold matching values
if a == b:
...
Null Coalescing
// T? ?? T -> T; never crashes
i32 x = maybe ?? 0;
Switching
switch maybe:
none: print("none\n");
v: print($"value = {v}\n");
v is a mutable reference to the value field (assigning v = 10 writes the optional). Don't touch the switched-on variable inside the value branch, the reference stays valid after maybe = none.
Optional Chaining
// T? -> T?; none if any step was none
i32? x = v2m?.x;
// mix: force-unwrap nm, chain v2m
nm!.v2m?.x;
The chain result is always the rightmost type wrapped in an optional, e.g. T?.
Optionals as References
Complex types in optionals are DIMA-backed references:
MyData? ref = md;
// mutates md
ref!.(x, y) = (7, 6.28);
MyData? ref2 = none;
if true:
MyData val = MyData(10, 3.14, "segfault");
// outlives the scope, refcount bumped
ref2 = val;
Variants
Tagged unions. Storage: { u8 active_type, byte[N] } sized to the largest possible type.
variant MyVariant:
i32, f32, u64;
def main():
MyVariant var = i32(5);
var = f32(3.4);
var = u64(55);
Switching
switch var:
i32(i): print($"i32: {i}\n");
f32(f): print($"f32: {f}\n");
u64(u): print($"u64: {u}\n");
The accessor names are branch-scoped. Variants are passed by reference. Assigning a tuple into a variant requires a temp tuple variable.
Tagged Variants
use Core.print
variant MyVariant:
Int(i32),
Float(f32),
Tuple(i32, f32, bool8),
Empty(void);
def main():
MyVariant var = MyVariant.Int(-5);
var = MyVariant.Tuple(5, 6.9, bool8(u8(33)));
switch var:
MyVariant.Int(i): print($"i = {i}\n");
MyVariant.Float(f): print($"f = {f}\n");
MyVariant.Tuple(t): print($"({t.$0}, {t.$1}, {t.$2})\n");
MyVariant.Empty(): print("is empty\n");
Tags must always be qualified (MyVariant.Int) to disambiguate from types. Empty payload: Empty(void); construct/switch as MyVariant.Empty().
Inline Variants
def print_var(variant<i32, f32, str> var):
switch var:
i32(i): ...
str(s): ...
def main():
variant<i32, f32, str> var = i32(-55);
print_var(var);
Inline variants cannot be tagged, so switches must name the types.
Comparing
// does it hold an i32?
if var == i32:
...
// tagged check
if var == MyVariant.Int:
...
// same type AND same value, checked via memcmp
if var_1 == var_2:
...
// u8, 1-based, readonly
print($"active = {var.active_type}\n");
// type + value check
if var == i32(7):
...
Unwrap & Extract
// force-unwrap; crashes on wrong type
i32 v = var!(i32);
// extract -> i32?, none if wrong type
i32? v = var?(i32);
// tag form, chains with ?.
i32? x = var?(MyVar.Data).x;
Optional Variants
T? where T is a variant is the same struct as internal tag 0 is reserved for none:
MyVar? opt = var;
// These two are equal
f32 x = (opt!)!(MyVar.Float);
f32 y = opt!!(MyVar.Float);