Flash

Cookbook

Small, copyable recipes for everyday Flash.

README · Vision · Tutorial · Reference · Cookbook · Setup · Versioning · Changelog · License

--- Where the [Reference](REFERENCE.md) documents what every form means, this page shows the handful of patterns that make Flash code pleasant to write and to reread. Every example on this page is real, compiled code: the recipes live verbatim in [`examples/register/cookbook.flash`](examples/register/cookbook.flash), where the compiler's own test gates build and run them, so a snippet here cannot quietly rot. The examples share one preamble. `use core` imports the [standard library](std/README.md); `use std` reaches Zig's standard library directly — that is where the `Allocator` type itself lives, and aliasing it once keeps signatures short: ```flash use std use core const Allocator = std.mem.Allocator ``` ## Contents 1. [One arena per job](#1-one-arena-per-job) 2. [Error handling](#2-error-handling) 3. [Cast chains](#3-cast-chains) 4. [Bit manipulation](#4-bit-manipulation) 5. [Struct literals and the formatter](#5-struct-literals-and-the-formatter) 6. [JSON](#6-json) --- ## 1. One arena per job Flash has no garbage collector and no hidden allocations — every function that allocates takes an `Allocator`. The pattern that keeps this from becoming bookkeeping: give each bounded job its own arena. Allocate freely inside the job without tracking individual lifetimes, and one `deinit` releases everything at once. Whatever must outlive the arena is copied out to the caller's allocator before the function returns. ```flash pub fn greeting(child Allocator, name []u8, attempt u32) ![]u8 { var arena = core.arena.ArenaAllocator.init(child) defer arena.deinit() alloc := arena.allocator() line := try core.fmt.allocPrint(alloc, "hello {s}, attempt {d}", .{ name, attempt }) return child.dupe(u8, line) } ``` This is also the contract the standard library leans on: `core.json.parse`, for instance, never frees a partial tree on failure — hand it an arena and the question disappears. ## 2. Error handling Errors are values from named sets. Originate one with `error.Name`, propagate with `try`, and recover at the call site with `catch` — the three spellings cover almost everything: ```flash pub const ConfigError = error{ NotFound, OutOfMemory } pub fn valueOf(table [][]u8, key []u8) ConfigError![]u8 { for line in table { if core.mem.indexOfScalar(u8, line, '=') |eq| { if core.mem.eql(u8, line[0..eq], key) { return line[eq + 1 ..] } } } return error.NotFound } pub fn portOrDefault(s []u8) u16 { return core.fmt.parseInt(u16, s, 10) catch 8080 } ``` When a function builds something in steps, `errdefer` undoes the finished steps if a later one fails — and stays silent on success: ```flash pub fn dupePair(alloc Allocator, a []u8, b []u8) ![2][]u8 { first := try alloc.dupe(u8, a) errdefer alloc.free(first) second := try alloc.dupe(u8, b) return .{ first, second } } ``` ## 3. Cast chains Pointer reinterpretation usually needs two casts: `#alignCast` to assert the alignment, `#ptrCast` to change the type. Spell `#ptrCast` outermost — the lowered Zig is then already in the order `zig fmt` canonicalizes to, so generated code passes format checks untouched: ```flash pub const Header = struct { magic u32, len u32, } pub fn asHeader(raw [*]mut u8) *mut Header { return #ptrCast(#alignCast(raw)) } ``` Both casts infer their result from where the value lands (here: the return type), so the chain stays free of repeated type names. ## 4. Bit manipulation The operators are Zig's, with Zig's precedence: bitwise `&`/`^`/`|` bind tighter than comparisons, so masks compose without parentheses. `#truncate` narrows by dropping high bits, and the wrapping operators (`+%`, `-%`, `*%`) say "overflow is intended" at the call site: ```flash pub const READY u32 = 1 << 5 pub fn isReady(reg u32) bool { return reg & READY != 0 } pub fn green(rgba u32) u8 { return #truncate((rgba >> 8) & 0xFF) } pub fn nextSeq(seq u8) u8 { return seq +% 1 } ``` Plain `+` on a `u8` panics at 255 in safe builds; `+%` rolls over to 0 by design. Pick per call site, not per project. ## 5. Struct literals and the formatter Give fields defaults and most call sites shrink to the fields they care about — `.{}` is a complete value when every field has one. Two formatting habits keep the diffs quiet: write initializers on one line (the formatter keeps them collapsed, so hand-wrapped literals are rewritten on the next format), and zero fixed buffers with `**` repetition: ```flash pub const Config = struct { name []u8 = "", retries u8 = 3, verbose bool = false, } pub fn defaultConfig() Config { return .{} } pub fn verboseConfig(name []u8) Config { return .{ .name = name, .retries = 5, .verbose = true } } pub var scratch [64]u8 = [_]u8{0} ** 64 ``` `flashc fmt` emits one canonical form, so the cheapest habit of all is to write that form from the start — wire your editor to format on save ([SETUP.md](SETUP.md) has the recipes) and the question never comes up. ## 6. JSON `core.json` parses an RFC 8259 document into a tree of `Value` nodes. Everything is allocated from the allocator you pass — combined with recipe 1, an arena makes the whole document one allocation lifetime. `get` looks up an object member, and a `switch` on the value's tag unpacks it: ```flash pub fn portOf(alloc Allocator, src []u8) !i64 { doc := try core.json.parse(alloc, src) value := doc.get("port") orelse return error.MissingPort return switch value { .int => |n| n, else => error.MissingPort, } } ``` Numbers that scan as integers and fit an `i64` arrive as `.int`; fractions, exponents, and oversized integers keep their raw lexeme in `.number`, which `stringify` writes back verbatim — a parse/stringify round trip never loses digits.