Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

IR and Lowering

Flint uses two intermediate representations (IRs) between the AST and machine code:

  • HIR (High-Level IR): typed, desugared, ownership-annotated
  • FIR (Flint IR): target-independent control-flow graph

HIR: High-Level IR

HIR is produced from the type-checked AST. It retains the high-level structure of the source but with:

  • All types resolved and inferred, with no ambiguity remaining.
  • ? propagation expanded into explicit match/return sequences.
  • defer statements expanded into explicit cleanup scheduling at scope exits.
  • Ownership transfers made explicit: every value move, borrow, and drop is annotated.
  • Pattern match exhaustiveness verified.
  • Collection method calls type-checked against official generic definitions.
  • Implicit drop sites computed: the compiler inserts drop operations at scope boundaries for every owned value.

HIR is still structured as functions with statements and expressions. It does not have basic blocks or explicit control flow edges yet.

Key HIR Transformations

? desugaring:

// Source
let data = file.read()?

// HIR (conceptual)
let data = match file.read() {
    Ok(v)  => v
    Err(e) => { drop(everything_live); return Err(e) }
}

defer scheduling:

// Source
let conn = db.connect()?
defer conn.close()
do_work(conn)

// HIR (conceptual)
let conn = db.connect()?
// ... defer registered at this scope level ...
do_work(conn)
// Scope exit: conn.close() runs before conn is dropped

FIR: Flint IR

FIR is a typed, SSA-like control-flow graph IR. It is the last shared representation before target-specific lowering begins.

Basic Blocks

Each function in FIR is a set of basic blocks. A basic block is a linear sequence of instructions with exactly one entry and one exit (a terminator).

Terminators:

  • Branch(cond, true_block, false_block): conditional jump
  • Jump(target_block): unconditional jump
  • Return(value): return from function
  • Trap: unreachable / fatal

Control flow between blocks is explicit as edges in a directed graph.

FIR Values

FIR uses named values (not physical registers). Each value has a type and is assigned exactly once (SSA property where it applies). Ownership is explicit:

  • A move of value v to a new owner invalidates v.
  • A borrow of v creates a view valid for a lexically bounded region.
  • A drop of v inserts the generated drop glue for its type.

FIR Instructions

Example FIR instruction kinds:

  • Let(result, type, init): bind a value
  • Call(result, callee, args, calling_conv): function call
  • FieldGet(result, base, field_index): struct field read
  • FieldSet(base, field_index, value): struct field write
  • Index(result, base, index): array/slice indexing
  • Move(result, source): ownership transfer
  • Borrow(result, source, mode): create read or write borrow
  • Drop(value): drop an owned value
  • Cast(result, value, target_type): numeric conversion

Why Two IRs?

HIR is better for semantic analysis: it is still structured like source code, making it easier to check ownership, defer, and match exhaustiveness.

FIR is better for code generation: the control-flow graph makes reachability analysis, dead code elimination, and instruction selection straightforward.

Ownership in FIR

The ownership model is encoded directly in FIR:

  • Every owned value has a single live definition at any program point.
  • Moves are explicit edges that transfer ownership.
  • Borrows carry a mode (read or write) and a scope.
  • Drop insertions are explicit instructions; nothing drops implicitly at the FIR level.

This makes FIR verifiable: a simple analysis can confirm that no value is used after a move or drop.

Dead Code Elimination

FIR supports dead code elimination (DCE) before machine lowering:

  • Unreachable basic blocks are removed.
  • Unused bindings are eliminated.
  • Capability-query branches that are always-false for the selected target are removed (static constant folding).

DCE keeps MCU binaries small by removing code paths for unsupported features on the selected target.