Markdown 867 lines
<div align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/flash_logo_dark.png">
<img src="assets/flash_logo_light.png" alt="Flash" width="240">
</picture>
<h1>Language Reference</h1>
<p><i>The complete syntax and semantics of Flash, in one document.</i></p>
<p>
<a href="README.md"><b>README</b></a> ·
<a href="VISION.md"><b>Vision</b></a> ·
<a href="https://ajhahnde.github.io/Flash/"><b>Tutorial</b></a> ·
<b>Reference</b> ·
<a href="COOKBOOK.md"><b>Cookbook</b></a> ·
<a href="SETUP.md"><b>Setup</b></a> ·
<a href="VERSIONING.md"><b>Versioning</b></a> ·
<a href="CHANGELOG.md"><b>Changelog</b></a> ·
<a href="LICENSE.md"><b>License</b></a>
</p>
</div>
---
This is a reference, not a tutorial. It documents the surface the self-hosted
compiler (`selfhost/`) actually implements — the normative grammar is the EBNF
block at the top of `selfhost/parser.flash`, and where this document and the
compiler disagree, the compiler wins. The handwritten Zig compiler under
`src/` is the frozen bootstrap seed and parses an older surface; it is not a
language source.
## Contents
1. [Overview](#1-overview)
2. [Lexical structure](#2-lexical-structure)
3. [Type system](#3-type-system)
4. [Declarations](#4-declarations)
5. [Functions](#5-functions)
6. [Statements](#6-statements)
7. [Expressions](#7-expressions)
8. [Builtin functions](#8-builtin-functions)
9. [Comptime](#9-comptime)
10. [Error handling](#10-error-handling)
11. [Container types in detail](#11-container-types-in-detail)
12. [Modules and imports](#12-modules-and-imports)
13. [Inline assembly](#13-inline-assembly)
14. [Test blocks](#14-test-blocks)
15. [The formatter](#15-the-formatter)
16. [Differences from Zig](#16-differences-from-zig)
---
## 1. Overview
Flash is a statically typed systems language: a Go-flavored surface (type
after name, `:=`, no semicolons, no parentheses on control headers) over Zig
semantics (allocators, comptime, error unions, optionals — no GC, no async,
no runtime). The compiler, `flashc`, is written in Flash and lowers Flash
source to readable Zig source ("Tier 0"); the Zig toolchain performs type
checking, code generation, and linking downstream. The frontend performs its
own binding, scope, and mutability analysis plus compile-time evaluation
(see §9).
The smallest real program (`examples/hello.flash`):
```flash
use flibc
link "flibc_start"
link "flibc_mem"
export fn main(_ usize, _ argv) noreturn {
msg := "hello from flash\n"
_ = flibc.sys.write_fd(1, msg.ptr, msg.len)
flibc.exit()
}
```
Compiling and running (Zig 0.16.0 required, see [SETUP.md](SETUP.md)):
```sh
zig build # build flashc into zig-out/bin
zig-out/bin/flashc examples/hello.flash # lowered Zig source on stdout
zig-out/bin/flashc fmt file.flash # reformat in place (§15)
zig-out/bin/flashc --dump-tokens file.flash
zig-out/bin/flashc --version
```
A Flash file is a sequence of top-level items: `use` imports, `link`
directives, `comptime` blocks, `test` blocks, `const`/`var` declarations, and
`fn` declarations. Items are emitted in source order.
On the first malformed token the compiler stops with a single parse
diagnostic (line + expected-token message); semantic diagnostics (§6) are
collected across the whole file in one run.
## 2. Lexical structure
Source is a stream of UTF-8 bytes. Identifiers and keywords are ASCII;
string and character literals pass arbitrary UTF-8 (and `\u{…}` escapes)
through to the output verbatim. There are no semicolons — `;` is not even a
token; a newline ends a statement (rules below).
### Comments
```flash
// an ordinary line comment
/// a doc comment — exactly three slashes, content-bearing:
/// it attaches to the const/fn/field/variant that follows it
//// four or more slashes: an ordinary comment
//! also an ordinary comment (no special "module doc" form)
```
There are no block comments. A run of consecutive `///` lines forms one doc
comment; it may lead a `const`/`fn` declaration, a struct field, or an
enum/union variant — never a `use`, `link`, or `comptime` item.
### Identifiers
`[A-Za-z_][A-Za-z0-9_]*`. A lone `_` is its own token — the discard
placeholder (§6) — and cannot be bound or read. `mut` and `volatile` are
contextual words, not keywords: they only mean something inside a type
(§3) and are ordinary identifiers everywhere else.
### Keywords
The 43 reserved words, none of which can be shadowed by a binding:
| Group | Words |
| :--- | :--- |
| modules | `use` `as` `link` `pub` |
| declarations | `fn` `const` `var` `export` `extern` `inline` `comptime` `align` `linksection` `callconv` `test` |
| control flow | `if` `else` `while` `for` `in` `break` `continue` `return` `switch` `defer` `errdefer` |
| errors | `error` `try` `catch` `orelse` |
| containers | `struct` `enum` `union` `packed` |
| other | `asm` |
| value words | `true` `false` `null` `undefined` `unreachable` |
| type words | `noreturn` `anytype` `anyopaque` |
The value words parse only in value position and lower to the identical Zig
keyword; the type words parse only in type position.
### Number literals
```flash
n := 1_000_000 // decimal, '_' separators in any base
h := 0xFF // hex (0x or 0X)
o := 0o755 // octal
b := 0b1010_0001 // binary
pi := 3.14 // float: decimal only, fraction REQUIRED
small := 1.5e-3 // exponent only after a fraction
```
A letter or wrong-base digit glued to a literal is a lex error (`123abc`,
`0b102`). There are no hex floats and no bare-exponent integers (`3e10` is
invalid — write `3.0e10`).
### String, multiline, and character literals
```flash
s := "a line\n" // escapes: \n \r \t \0 \\ \" \xNN \u{N…}
raw := \\no escapes in here —
\\each \\ line is one physical line; consecutive lines fold
c := 'A' // char: 'c', '\n', '\x1b', '\u{41}'
```
Escape sequences are validated structurally and pass through to the emitted
Zig verbatim (Zig uses the same spellings).
### Enum literals
`.variant` — a leading dot followed by an identifier — is an inferred enum
literal; the enum type comes from context (§11).
### Statement boundaries
The semicolon-free rule, normative:
1. A newline at bracket-depth 0 ends a statement.
2. A line whose last token cannot end an expression — a binary operator, an
open delimiter, `=` — continues onto the next line (`x := a +` ⏎ `b` is
one expression). A *leading* operator on the next line is **not** a
continuation; it starts a new statement, which the unused-value checker
then rejects with a pointer at the fix.
3. `return` and `break :label` take a value on the same line only
(`return` ⏎ `x` is a void return plus a diagnostic on `x`).
4. A function's return type starts on the same line as the closing `)` (§5).
5. Go's composite-literal rule: after `if`/`while`/`for`/`switch`, a `{`
opens the body, never a typed literal `Type{…}`. Parenthesize to get the
literal: `if (P{}) …`. Delimited sub-contexts (call arguments, `(…)`,
`[…]`, initializer field lists) clear the suppression.
## 3. Type system
The load-bearing rule: **const-pointee is the default**. Every pointer and
slice type is read-only unless `mut` opts the pointee in; `const` is not
spellable in a type at all. This is the opposite reading from Zig, where
`[]u8` is mutable — in Flash `[]u8` lowers to Zig's `[]const u8`.
### Primitive types
`u8`–`u64`, `i8`–`i64` (any bit width, e.g. `u7`), `usize`, `isize`, `bool`,
`f16` `f32` `f64` `f80` `f128`, `void`, `type`, `comptime_int`,
`comptime_float` — plain identifiers, lowered verbatim, checked by Zig
downstream. The reserved type words `noreturn`, `anytype` (inferred
parameter type), and `anyopaque` (incomplete pointee) also lower verbatim.
### Pointers, slices, arrays
| Flash | Lowered Zig | Meaning |
| :--- | :--- | :--- |
| `*T` | `*const T` | single-item pointer, read-only pointee |
| `*mut T` | `*T` | mutable pointee |
| `*volatile T` / `*mut volatile T` | `*const volatile T` / `*volatile T` | volatile access |
| `[*]T` / `[*]mut T` | `[*]const T` / `[*]T` | many-item pointer |
| `[*:0]T` / `[*:0]mut T` | `[*:0]const T` / `[*:0]T` | sentinel-terminated many-pointer |
| `[]T` / `[]mut T` | `[]const T` / `[]T` | slice |
| `[:0]T` / `[:0]mut T` | `[:0]const T` / `[:0]T` | sentinel-terminated slice |
| `[N]T` / `[N:0]T` | verbatim | fixed-size array / with sentinel |
| `[_]T` / `[_:0]T` | inferred length | array-literal head only: `[_]u8{ 1, 2 }` |
| `[]align(16) u8` | `[]align(16) const u8` | alignment qualifier |
`align(N)` sits after the pointer/slice prefix and before `mut`/`volatile`;
it applies to pointers and slices, not to arrays or bare types. The sentinel
is an arbitrary expression (`[:NUL]u8` works).
```flash
fn fillPattern(msg *mut [64]u8) void {
for *b, i in msg {
b.* = #truncate(i *% 31 +% 7)
}
}
```
### Optionals and error unions
| Flash | Meaning |
| :--- | :--- |
| `?T` | optional — `null` or a `T`; unwrap with `.?`, `orelse`, or capture (§6) |
| `E!T` | error union with the named set `E` |
| `!T` | error union with an *inferred* set — valid only as a function **declaration** return type, rejected on function types |
| `error{ A, B }` | an error-set type (also a value-position expression, §10) |
### Function types and tuples
```flash
const Handler = fn(u32, []u8) anyerror!void // bare unnamed parameter types
var callback *fn(u8) bool = undefined // function pointer (const by default)
const CHook = fn(u32) callconv(.c) i32 // calling convention in the type
const Pair = (usize, []u8) // tuple type, arity >= 2
```
A function type's parameters are bare types (no names) and its return type
may not use the inferred `!T` form. A tuple type lowers to Zig's
`struct { A, B }`; values are written `.{ a, b }` and accessed `t[0]`,
`t[1]`. `(T)` is value grouping, never a one-tuple.
### Generic types
A generic is a `comptime` function returning `type` (§9); application is
ordinary call syntax in type or value position:
```flash
var names core.list.List([]u8) = .empty
```
### Builtin type aliases
Two spelling aliases exist for the C-ABI entry-point signature: `argv`
lowers to `[*]const ?[*:0]const u8` and `cstr` to `[*:0]const u8`. Both are
shadowable — declaring your own `argv` or `cstr` suppresses the alias for
the whole file.
## 4. Declarations
### Constants and variables
```flash
const LIMIT = 64 // file scope, type inferred
const BUF_LEN usize = 4096 // explicit type
pub var counter u32 = 0 // mutable global, exported with pub
var scratch [256]u8 align(16) = undefined // alignment on the binding
```
At file scope, `const`/`var` require an initializer. `pub` makes any
top-level declaration visible to importers; the default is file-private.
`undefined` is rejected as a `const` value.
### Export and extern variables
```flash
export var bss_probe i32 = 0 // defines a cross-object symbol
extern var kernel_ticks u64 // consumes one: typed, NO initializer
```
An `extern var` carries no initializer and no `linksection` — the defining
object owns the section.
### Link sections
```flash
var boot_stack [4096]u8 linksection(".boot.bss") = undefined
fn earlyInit() linksection(".boot.text") void {
// placed in the named section
}
```
`linksection("…")` sits between the type and the `=` on a file-scope
variable, and directly after the parameter list — before the return type —
on a function. File-scope
declarations only — not on locals, not on `extern var`.
### Short declarations (locals)
```flash
msg := "hello" // IMMUTABLE binding — the canonical local
var n usize = 0 // the only mutable binding form
const k u8 = 3 // typed immutable local
```
Unlike Go, `:=` declares an **immutable** binding. Mutation requires `var`.
See §6 for destructuring and `comptime` bindings.
## 5. Functions
```flash
fn nameLen(s cstr) usize { // type after name, no colon, no arrow
var n usize = 0
while s[n] != 0 {
n += 1
}
return n
}
pub fn append(self *mut Self, alloc base.Allocator, item T) !void { … }
export fn main(argc usize, argv argv) noreturn { … } // C ABI: callconv(.c)
extern fn exec_path(name cstr, argv argv) i32 // bodyless prototype
inline fn mask(x u32) u32 { return x & 0xFFF }
```
- Parameters: `name Type`, `_ Type` (unused), `comptime name Type` (§9). No
trailing comma in a declaration's parameter list.
- The return type follows the `)` directly on the same line — there is no
arrow. A missing return type means `void`.
- `export fn` gets `callconv(.c)` in the lowered Zig; an explicit
`callconv(expr)` clause may follow the parameter list (before the return
type): `fn handler(n u32) callconv(.c) i32 { … }`.
- `extern fn` is a bodyless prototype; everything else requires a body.
- Methods take an explicit receiver — there is no implicit `self` (§11).
- A statement-position `return a, b` returns a tuple; pair it with a
destructuring bind `x, y := f()` (§6). The value must be on the same line
as `return` (§2).
- A doc comment (`///`) before the declaration attaches to it.
## 6. Statements
A block is `{ stmt* }`. Braces are mandatory on all control flow; headers
take no parentheses.
### Bindings and assignment
```flash
x := compute() // immutable
var i usize = 0 // mutable
const j u8 align(4) = 7 // typed + aligned local
comptime var width = 8 // compile-time mutable (§9)
a, b := pair() // destructuring bind (tuple rhs); '_' skips
var lo, hi = bounds() // keyword form — no per-name type or align
a, b = swap(b, a) // destructuring ASSIGNMENT onto existing lvalues; '=' only
_ = mayFail() // discard — the only way to ignore a value
i += 1 // compound assignment (statement, not expression)
```
Destructuring requires at least two names with at least one real name.
Compound assignment operators: `=` `+=` `-=` `*=` `/=` `%=` `&=` `|=` `^=`
`<<=` `>>=` and the wrapping forms `+%=` `-%=` `*%=`.
The semantic checker rejects, across the whole file in one run:
- shadowing or redeclaration of **any** visible name (params, globals, and
imports included);
- unused locals, parameters, and captures (discard with `_` or `_ = name`);
- a bare value-producing expression statement (`x == y` alone) —
effectful and control-flow expressions are exempt;
- stores to immutable targets (`:=`/`const` locals, parameters, captures);
- member-access roots that resolve to nothing (`X.f` where no `X` is in
scope).
### If
```flash
if cond {
…
} else if other {
…
} else {
…
}
if maybe |x| { use(x) } // optional capture: runs when non-null
if maybe |*x| { x.* = v } // pointer capture — mutate in place
if loadFile() |data| {
use(data)
} else |err| { // error-union capture on the else
report(err)
}
```
For the *value* form of `if`, see §7 — it requires parentheses.
### While
```flash
while i < n { i += 1 }
while it.next() |item| { use(item) } // payload capture per iteration
while readByte() |b| { … } else |err| { … } // error-union while
inline while cond { … } // comptime-unrolled (§9)
```
### For
```flash
for x in xs { … } // element capture
for x, i in xs { … } // element + index (index always by value)
for i in 0..n { … } // range — hi exclusive
for *p, i in &table { p.* = #intCast(i) } // pointer element capture
for b in row { … } else { … } // else runs when the loop did not break
inline for f in fields { … } // comptime-unrolled
```
The `*` pointer capture is valid on the element capture only, never the
index. A `for` loop's `else` takes no capture.
### Labeled loops
```flash
outer: while true {
if pick(task) |i| {
idx = i
break :outer
}
refill(task)
}
rows: for row, r in grid {
for b in row {
if b == 0 {
continue :rows // restart the OUTER loop's next iteration
}
}
break :rows
}
```
A label is written `name:` before `while`/`for` (or before a block, §7) and
is the target of `break :name` / `continue :name`. A label nothing targets
is an error. (From `examples/register/labeled_loop.flash`.)
### Defer and errdefer
```flash
defer base.testAlloc.free(s) // statement form
defer { a(); b() } // block form — runs on every exit
errdefer list.deinit(alloc) // runs only when unwinding with an error
errdefer |err| last_failure = classify(err) // the unwinding error, by value
errdefer |err| {
last_failure = classify(err)
unwound += 1
}
```
The `errdefer` capture binds by value — `|*err|` is rejected, as in Zig.
(From `examples/register/errdefer_capture.flash`.)
## 7. Expressions
### Operator precedence
Higher binds tighter; all binary operators are left-associative. The tiers
mirror Zig's.
| Prec | Operators | Notes |
| :--- | :--- | :--- |
| 1 | `orelse` `catch` | loosest; `catch` may carry a `\|err\|` capture |
| 2 | `\|\|` | logical or — lowers to Zig `or`; **not** error-set merge (§10) |
| 3 | `&&` | logical and — lowers to Zig `and` |
| 4 | `==` `!=` `<` `<=` `>` `>=` | comparison |
| 5 | `&` `^` `\|` | bitwise — one tier, as in Zig |
| 6 | `<<` `>>` | shifts |
| 7 | `+` `-` `++` `+%` `-%` | additive; `++` array/slice concat; `+%` `-%` wrapping |
| 8 | `*` `/` `%` `*%` `**` | multiplicative; `**` array repetition |
Unary prefix: `!` `-` `&` `~` and `try` — `try` binds tighter than any
binary operator, so `try a + b` is `(try a) + b`. Maximal munch applies:
`<<` is one token, `* *` is two stars (not `**`).
Postfix, tightest of all:
```flash
v := s.field // member access
p.* = 1 // pointer dereference (a valid lvalue)
x := opt.? // optional unwrap — asserts non-null
r := f(a, b) // call (no trailing comma in args)
e := xs[i] // index
w := xs[lo..hi] // slice; also xs[lo..] and xs[lo..hi :0] (sentinel)
q := Point{ .x = 1 } // typed literal (§11)
```
### Literals in expression position
```flash
p := .{ .x = 1, .y = 2 } // anonymous struct literal
t := .{ a, b } // tuple / positional literal
zeros := [_]u8{0} ** 64 // array literal head + repetition
state := .ground // inferred enum literal
e := error.NotFound // error-value origination
set := error{ A, B } // error-set definition (a value)
```
### Value `if`
```flash
x := if (cond) a else b
```
The parentheses are **required** on a value `if` and the `else` is
mandatory. (Only the statement form, delimited by `{`, may omit them.)
### Switch
`switch` is an expression. Prongs are comma-separated, never fall through,
and must be exhaustive (Zig's rule, enforced downstream).
```flash
event := switch b {
0x1b => blk: {
self.state = .esc
break :blk Event{ .key = .none }
},
'\r', '\n' => .{ .key = .enter }, // multiple patterns per prong
0x20...0x7e => .{ .key = .char, .ch = b }, // '...' inclusive range
else => .{ .key = .none },
}
switch msg {
.ping => |seq| reply(seq), // payload capture
.data => |*d| { d.acked = true }, // pointer capture — mutate the payload
else => {},
}
```
`...` (inclusive) is the switch-range spelling; `..` is slicing/`for`
ranges (exclusive).
### Labeled blocks
```flash
limit := blk: {
if fast { break :blk 16 }
break :blk 64
}
```
A labeled block is an expression; `break :label value` yields its value.
### Catch and orelse
```flash
n := parse(s) catch 0 // expression handler
data := load() catch |err| return err // capture + control flow
buf := alloc.alloc(u8, n) catch { // block handler (void recovery)
return error.NoMemory
}
first := argv[i] orelse break // unwrap optional or do the rhs
```
`break`, `continue`, and `return` are themselves expressions, which is what
lets them sit on the right of `orelse`/`catch`.
### Grouping
`(expr)` groups and is preserved verbatim in the lowered Zig. After a value
`if`'s `(cond)`, a binary operator continues the condition
(`if (a || b) && c …`), while a postfix `.`/`(`/`[` starts the then-arm.
## 8. Builtin functions
`#name(args)` is Flash's spelling of Zig's `@name(args)` — the call lowers
1:1 (`#intCast(x)` → `@intCast(x)`), the semantics are exactly Zig's, and
any Zig builtin passes through. The frontend walks builtin arguments like
any expression but does not restrict the name set; an unknown name is
rejected by the Zig compiler downstream.
The set proven in this repository's own sources (the compiler, `std/`, and
the examples):
| Builtin | Use |
| :--- | :--- |
| `#intCast(x)` | integer cast, result-location-inferred: `n := #as(usize, #intCast(x))` |
| `#truncate(x)` | integer cast that drops high bits: `var b u8 = #truncate(i)` |
| `#as(T, x)` | explicit result type for an inferred-cast expression |
| `#bitCast(x)` | same-size bit reinterpretation |
| `#ptrCast(p)` | pointer type cast |
| `#alignCast(p)` | pointer alignment cast |
| `#intFromPtr(p)` / `#ptrFromInt(a)` | pointer ↔ integer address |
| `#sizeOf(T)` / `#bitOffsetOf(T, "f")` | layout queries |
| `#This()` | the enclosing container type — `const Self = #This()` (§11) |
| `#typeInfo(T)` / `#tagName(v)` | reflection |
| `#min(a, b)` | minimum |
| `#divTrunc(a, b)` / `#rem(a, b)` | explicit signed division/remainder |
| `#addWithOverflow(a, b)` / `#subWithOverflow` / `#mulWithOverflow` / `#shlWithOverflow` | overflow-reporting arithmetic (result, bit) |
| `#memcpy(dst, src)` | bulk copy |
| `#compileError("msg")` | comptime diagnostic (§9) |
| `#export(sym, opts)` | export a symbol from a `comptime` block |
| `#import("…")` | escape hatch to a Zig import (prefer `use`, §12) |
| `#setEvalBranchQuota(n)` | raise the comptime branch quota |
```flash
pub fn resetTable() void {
for *p, i in &table {
p.* = #intCast(i)
}
}
```
## 9. Comptime
Flash rents Zig's comptime: the keyword marks compile-time evaluation, and
the frontend's evaluator folds constant initializers and rejects definite
errors (division by a known zero, wrong generic arity, `#compileError` on a
taken path) once per generic, regardless of instantiation count.
```flash
comptime { // top-level comptime block (see examples/start.flash)
#export(&_start_shim, .{ .name = "_start", .linkage = .strong })
}
fn tables() usize {
comptime var width = 8 // compile-time bindings are statements,
width += 1 // not top-level items
comptime const mask = (1 << width) - 1
return mask
}
pub fn List(comptime T type) type { // comptime parameter = generics
return struct { … }
}
inline while i < 4 { … } // unrolled at compile time
inline for f in fields { … }
```
Integer literals are `comptime_int` until context gives them a concrete
type; `core.math.minInt(T)`/`maxInt(T)` return `comptime_int` exactly.
## 10. Error handling
```flash
const InitError = error{ NoMemory, BadDevice } // a named error set
fn claim(slot u16) InitError!u16 { // error union return
if slot == 0 {
return error.BadDevice // originate
}
return slot
}
pub fn bringup(slot u16) InitError!u16 {
id := try claim(slot) // propagate
errdefer |err| last_failure = classify(err) // cleanup on unwind (§6)
…
}
n := parse(s) catch 0 // recover (§7)
```
- `!T` (inferred set) is valid only on function declarations; function
types must name the set (`E!T`).
- Error-set merge `E1 || E2` is **rejected at the Flash line** — the
diagnostic says to declare the combined set explicitly. `||` with a type
operand is a definite error.
- All error captures (`catch |err|`, `else |err|`, `errdefer |err|`) bind
by value; `|*err|` is rejected.
## 11. Container types in detail
### Struct
Fields come first (comma-separated, optional defaults, optional `///`
docs), then the associated declarations — methods, constants, and `use`
imports, each optionally `pub`:
```flash
pub const Decoder = struct {
state State = .ground, // field with a default value
const State = enum { ground, esc, csi }
fn atGround(self *mut Decoder, b u8) Event { … }
}
```
The method receiver is explicit — `self Decoder` (by value), `self *Decoder`
(read-only), or `self *mut Decoder` (mutating); `const Self = #This()` is
the idiom inside generics. Calls use method syntax: `dec.atGround(b)`.
Layout modifiers: `packed struct { … }` and `extern struct { … }`. The
field widths define a packed struct's layout — there is no
`packed struct(uN)` backing-integer form.
Initialization: anonymous `.{ .x = 1 }` where the type is known from
context, or typed `Decoder{ .state = .esc }`.
### Enum
```flash
const Mode = enum { read, write, append }
const Errno = enum(u8) { // explicit tag type
ok = 0, // explicit discriminant
perm = 1,
noent,
pub fn fatal(self Errno) bool { return self != .ok }
}
```
Variants first, declarations after. Reference variants as `Mode.read` or,
where the type is known, the inferred `.read`.
### Tagged union
```flash
const Packet = union(enum) {
none, // bare variant = void payload
byte u8, // variant with a payload type
span []u8,
}
fn handle(p Packet) void {
switch p {
.none => {},
.byte => |b| put(b),
.span => |s| write(s),
}
}
```
`union(enum)` infers the tag enum; `union(MyTag)` names one; a bare `union`
is untagged. Pattern-match payloads via `switch` captures (§7).
## 12. Modules and imports
| Flash | Lowered Zig | Meaning |
| :--- | :--- | :--- |
| `use flibc` | `const flibc = @import("flibc");` | bare name = a named module, resolved by the build |
| `use core` | `const core = @import("core");` | the standard library root |
| `use console_ui as ui` | `const ui = @import("console_ui");` | alias |
| `use "syscalls" as sys` | `const sys = @import("syscalls.zig");` | quoted stem = sibling **file**; no extension in source — the backend owns the suffix |
| `pub use "mem" as mem` | `pub const mem = @import("mem.zig");` | re-export (how `std/core.flash` builds its facade) |
| `link "flibc_start"` | `comptime { _ = @import("flibc_start"); }` | force-link a module; consecutive `link`s fold into one block |
A quoted import carrying a file extension is rejected with a migration
hint. `use` is also valid inside a struct/enum/union body — an associated
import. Import names share the no-shadowing namespace rule with every
other name (§6).
The standard library ships as the `core` module: `core.mem` (slice and
memory primitives), `core.list` (`List(T)`), `core.fmt` (comptime-checked
`allocPrint`/`bufPrint` with `{s}`/`{d}` verbs, `parseInt`), `core.math`
(`minInt`/`maxInt`), `core.arena` (`ArenaAllocator`), `core.json` (an
RFC 8259 value tree — `parse`/`stringify`). See
[std/README.md](std/README.md).
## 13. Inline assembly
```flash
fn currentEl() u64 {
return asm ("mrs %[ret], CurrentEL"
: [ret] "=r" (-> u64),
)
}
fn outByte(port u16, b u8) void {
asm volatile ("outb %[b], %[port]"
:
: [b] "{al}" (b),
[port] "N{dx}" (port),
: "memory"
)
}
```
`asm [volatile] (template : outputs : inputs : clobbers)` — the template is
a string (or multiline string), the operand sections are positional and each
may be empty (an empty earlier section keeps its `:`), and the clobbers
section is a single expression with no trailing comma. Operands are
`[name] "constraint" (expr)` for inputs and `[name] "constraint" (-> T)`
for a typed output return. The whole form lowers to Zig's `asm` verbatim.
## 14. Test blocks
```flash
test "allocPrint formats decimals" {
s := try fmt.allocPrint(base.testAlloc, "budget {d}, width {d}", .{ count, bits })
defer base.testAlloc.free(s)
try base.expectEqualStrings("budget 3, width 8", s)
}
```
`test "name" { … }` is a top-level declaration; the string names the test.
Tests live beside the code they cover and run through the build:
`zig build test-flash` (the `.flash` suite), `zig build test-selfhost` (the
compiler's own sources), `zig build test-std` (the standard library), and
`zig build test` for the whole host suite.
## 15. The formatter
```sh
flashc fmt file.flash # reformat in place
flashc fmt --check file.flash # report whether the file is canonical; write nothing
```
The formatter parses and re-renders the file in the one canonical style —
four-space indents, the brace and spacing rules the compiler itself is
written in — and preserves comments (ordinary line comments are reattached;
`///` docs travel with their declaration). Formatting is idempotent: a
formatted file reformats to itself.
## 16. Differences from Zig
Flash users usually arrive from Zig; this is the delta in one place.
| Topic | Zig | Flash |
| :--- | :--- | :--- |
| local binding | `const x: T = v;` / `var x: T = v;` | `x := v` (immutable), `var x T = v`; no colon, no semicolon |
| pointee mutability | `[]u8` mutable, `[]const u8` const | **opposite default**: `[]T` is const; `[]mut T` opts in; `const` unspellable in types (§3) |
| logical ops | `and` / `or` | `&&` / `\|\|` |
| control headers | `if (c)`, `while (c)`, `for (xs) \|x\|` | no parens: `if c`, `while c`, `for x in xs` — but a *value* `if` requires them: `if (c) a else b` |
| builtins | `@intCast` | `#intCast` — same names, `#` sigil |
| imports | `@import("f.zig")` | `use` / `link` (§12); extension never written |
| fields | `x: i32,` | `x i32,` — no colon |
| return type | `fn f() T` | same position, and `void` when absent |
| statements | `;` terminated | newline terminated; `;` is not a token |
| error-set merge | `E1 \|\| E2` | rejected — declare the combined set |
| shadowing | disallowed | disallowed, and extended to params/globals/imports |
| multi-return | — | `return a, b` tuple sugar + `x, y := f()` |
| wrapping ops | `+%` `-%` `*%` (+ assigns) | identical |
What Flash deliberately does not have: GC, async/await, interfaces/traits,
closures (function pointers only), exceptions, operator overloading,
variadics, string interpolation, ternary `?:`, `goto`, inheritance,
block comments, `packed struct(uN)`, one-element tuples, hex floats, and
implicit `self`. The backend today is Tier 0 (Zig emission) only; deeper
type checking beyond binding/scope/mutability and comptime evaluation is
deferred to the emitted Zig.
---
[← Prev: Vision](VISION.md) · [Next: Setup →](SETUP.md)