ajhahn.de
← Flash
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)