Flash

Language Reference

The complete syntax and semantics of Flash, in one document.

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

--- 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)