# XIRASM Language Guide

XIRASM is an assembler for writing machine code and binary files directly.
Its source language combines ordinary ISA text with a structured compile-time
language. Instructions remain readable as assembly, while values, functions,
loops, data layouts, and output formats use normal language constructs.

This guide teaches XIRASM in the order that a new user is likely to need it.
It begins with small flat binaries, develops the compile-time language, then
introduces labels, binary layouts, output regions, finalizers, and executable
formats. The separate API Reference provides complete signatures and lookup
tables. The [Format Tutorial](format-tutorial.md) provides focused PE, COFF,
and ELF tutorials.

## Guide Map

### Part I: Language Fundamentals

1. **Introducing XIRASM**
   - Assemble a first x86-64 program.
   - Understand ISA text and compile-time code.
   - Learn the basic source rules.
2. **Values and Bindings**
   - Declare constants and mutable local values.
   - Work with integers, booleans, strings, and bytes.
   - Understand scope and assignment.
3. **Expressions**
   - Use arithmetic, comparison, boolean, bitwise, and field expressions.
   - Call functions and compose compile-time calculations.
4. **Control Flow**
   - Select source with `if`.
   - Repeat work with `while` and `for`.
   - Generate data and instructions from compile-time conditions.
5. **Functions and Scope**
   - Write procedures and value-returning functions.
   - Use parameters, return types, local bindings, and nested scopes.
6. **Collections and Text**
   - Build and transform lists, maps, strings, and byte sequences.
   - Use collections to describe tables and generated output.
7. **Tokens and Pattern Matching**
   - Inspect token sequences.
   - Match source patterns and build higher-level source tools.

### Part II: The Assembler Model

8. **Targets, ISA Text, and Labels**
   - Select an ISA and instruction mode.
   - Define labels and use compile-time values in instructions.
   - Understand references and fixups.
9. **Data and Binary Layout**
   - Emit integers, strings, and bytes.
   - Reserve storage and control alignment.
   - Define structs and unions with exact binary layouts.
10. **Modules and Files**
    - Reuse source with includes and imports.
    - Read source data from files, JSON, and TOML.
11. **Output Regions and Virtual Data**
    - Separate logical addresses from physical file positions.
    - Build regions, reserved ranges, and virtual scratch data.
    - Read and patch generated bytes.
12. **Finalizers**
    - Defer checks and backfills until layout is stable.
    - Compute checksums and final field values.
    - Understand which operations may not change final layout.

### Part III: Building Programs

13. **Flat and Custom Binaries**
    - Build compact raw images and application-specific file formats.
14. **Executable and Object Formats**
    - Understand what a format facade provides.
    - Build one minimal executable without manual header arithmetic.
    - Continue with the separate Format Tutorial for PE, COFF, ELF,
      imports, exports, BSS, and relocations.
15. **Diagnostics and Practical Conventions**
    - Report invalid input and assert layout assumptions.
    - Organize reusable assembly projects.
    - Move from the guide to the API Reference and Format Tutorial.

## Part I: Language Fundamentals

## 1. Introducing XIRASM

### One Source Language, Two Forms

An XIRASM source file can contain two forms of code:

- **ISA text** describes instructions for the selected processor.
- **Compile-time code** calculates values, generates output, and controls how
  the source is assembled.

They belong to the same source language. Compile-time values can appear in
instructions, labels can be used by layout APIs, and functions can generate
both data and ISA text.

The following program combines both forms:

```asm
x86.use64();

const answer: u32 = 40 + 2

entry:
    mov eax, answer
    ret
```

Read it from top to bottom:

- `x86.use64();` selects 64-bit x86 instruction encoding.
- `const answer: u32 = 40 + 2` creates a compile-time constant.
- `entry:` defines a label at the current logical address.
- `mov eax, answer` is an ordinary x86 instruction. The compile-time value
  `answer` becomes its immediate operand.
- `ret` is another ordinary x86 instruction.

Calls such as `x86.use64();` use the structured language syntax. Instructions
such as `mov eax, answer` remain natural ISA text and do not use function-call
syntax.

### Assemble the First Program

Save the source as `hello.xir`, then run:

```text
xirasm hello.xir -o hello.bin --target x86-64
```

The output is six bytes:

```text
b8 2a 00 00 00 c3
```

The first five bytes encode `mov eax, 42`; the final byte encodes `ret`. The
label does not add bytes. It records an address that other instructions and
compile-time APIs can use.

Direct assembly writes a flat binary by default. A flat binary is exactly the
byte sequence produced by the source. It has no operating-system header. Later
chapters introduce the format facade with one small example. Complete PE,
COFF, and ELF programs belong in the
[Format Tutorial](format-tutorial.md).

### Compile-Time Code Generates Output

Compile-time code runs while XIRASM is constructing the output. It is not code
that executes when the resulting program runs.

For example, this loop emits four bytes:

```asm
const count: u8 = 4

for value in range(0, count) {
    db(value);
}
```

The generated output is:

```text
00 01 02 03
```

`range(0, count)` creates the values from zero up to, but not including,
`count`. The `for` loop runs during assembly, and `db(value);` emits one byte
for each value.

This distinction is fundamental:

| Task | Write it as |
| --- | --- |
| Handwritten processor instructions | ISA text |
| Constants and calculated values | Compile-time expressions |
| Repeated or conditional generation | Compile-time control flow |
| Binary data | Data calls such as `db`, `dw`, `dd`, and `dq` |
| Reusable generation logic | Compile-time functions |
| PE, COFF, or ELF output | Format facade calls |

### Basic Source Rules

The following rules are enough to read the examples in the first part of this
guide:

- `//` starts a line comment. It may appear after ISA text; quoted ISA operands
  may contain `//` as ordinary text.
- A label ends with `:`.
- ISA instruction lines do not end with semicolons.
- Function and API calls end with `;`.
- `return` statements end with `;`.
- `const` and `let` declarations do not end with semicolons.
- Blocks use `{` and `}`.
- Calls and aggregate values may span multiple lines while their parentheses
  or braces remain open.

For example:

```asm
// A compile-time helper.
fn emit_marker(value: u8) {
    db(0x7f, value);
}

const marker: u8 = 3
emit_marker(marker);

start:
    mov eax, marker
    ret
```

The function definition and call are compile-time code. The label and the two
indented lines are assembly code. Both forms contribute to one final output.

### A Practical Mental Model

When reading or writing XIRASM, ask what each line is meant to do:

1. If it is a processor instruction, write normal ISA text.
2. If it computes or selects something during assembly, use the compile-time
   language.
3. If it contributes bytes or reserves storage, use the data and output APIs.
4. If it describes a complete file format, use the corresponding format
   facade instead of calculating header rows by hand.

The next chapter develops the compile-time side of the language, beginning
with constants, mutable bindings, value types, assignment, and scope.

## 2. Values and Bindings

### Values Exist During Assembly

XIRASM values are compile-time values. They help construct the output, but a
binding does not create bytes or reserve memory by itself.

```asm
const magic: u16 = 0x5a4d
```

This declaration gives the name `magic` a value. It does not write `4d 5a` to
the output. To emit the value, pass it to a data API:

```asm
const magic: u16 = 0x5a4d
dw(magic);
```

The same rule applies to mutable values, strings, byte sequences, collections,
and values returned by functions. They become part of the output only when an
instruction, data call, layout operation, or format API uses them.

### Constants

A constant binds a name to a value that cannot be reassigned:

```asm
const page_size: u64 = 4096
const header_size = 64
const enabled: bool = true
```

The general forms are:

```text
const name = expression
const name: type = expression
```

The type annotation is optional when XIRASM can infer the type from the
initial value. Use an explicit type when the width or value category is part of
the binary contract.

Constants do not use a trailing semicolon. Reassigning one is an error:

```text
const value = 1
value = 2
```

Prefer `const` unless the source genuinely needs to update the binding while it
is being assembled.

### Mutable Bindings

Use `let` when a compile-time calculation needs state:

```asm
let offset: u32 = 0
offset = offset + 16
offset = offset + 4

dd(offset);
```

This emits:

```text
14 00 00 00
```

Both the declaration and the assignments happen during assembly. They do not
create a runtime variable named `offset`.

The forms are:

```text
let name = expression
let name: type = expression
name = expression
```

Like declarations, assignments do not end with semicolons. Calls made as
statements still do:

```asm
let value = 1
value = value + 1
db(value);
```

### Type Annotations and Inference

Bindings may state their type explicitly:

```asm
const signature: u16 = 0x5a4d
const title: string = "XIRASM"
const marker: bytes = b"OK"
const enabled: bool = true
```

They may also rely on inference:

```asm
const count = 4
const name = "payload"
const raw = b"DATA"
```

Common value types used in ordinary source include:

| Type | Purpose | Example |
| --- | --- | --- |
| `integer` | General compile-time integer | `42` |
| `u8` | Unsigned 8-bit value | `0xff` |
| `u16` | Unsigned 16-bit value | `0x5a4d` |
| `u32` | Unsigned 32-bit value | `0x401000` |
| `u64` | Unsigned 64-bit value | `0x140000000` |
| `i8` | Signed 8-bit value | `-1` |
| `i16` | Signed 16-bit value | `-200` |
| `i32` | Signed 32-bit value | `-4096` |
| `i64` | Signed 64-bit value | `-0x100000000` |
| `f32` | IEEE-754 binary32 value | `f32(1.5)` |
| `f64` | IEEE-754 binary64 value | `1.5` |
| `bool` | Compile-time condition | `true` |
| `string` | Compile-time text | `"kernel"` |
| `bytes` | Explicit byte sequence | `b"PE"` |

Lists, maps, structs, unions, and type values are introduced in later
chapters.

Width-specific integer types are useful for binary fields and function
contracts. The general `integer` type is useful when a helper accepts an
integer without requiring a particular encoded width. The API that eventually
emits the value still decides how many bytes are written.

Signed fixed-width values use two's-complement representation. Their declared
range is checked when a binding or aggregate field is created, and packing a
signed aggregate field writes the low 8, 16, 32, or 64 bits for that field.

Decimal literals with a fractional part or exponent have type `f64`. Use
`f32(value)` for an explicit finite narrowing conversion and `f64(value)` to
widen an `f32`. Integer and floating values are never mixed implicitly.

### Strings and Byte Sequences

Strings and byte sequences are different value categories:

```asm
const section_name: string = ".text"
const signature: bytes = b"MZ"
```

Use a string for textual names, paths, generated ISA text, and APIs that expect
text. Use `bytes` when the exact byte sequence is the value being modeled.

Some output APIs accept either category:

```asm
db("AB", b"CD");
```

This emits four bytes:

```text
41 42 43 44
```

Other APIs deliberately require one category. Keeping the distinction in the
binding makes those contracts visible in the source.

### Block Scope

Bindings belong to the scope in which they are declared. A block creates a
nested scope:

```asm
const value = 1

{
    let value = 2
    db(value);
}

db(value);
```

The output is:

```text
02 01
```

The inner `value` shadows the outer constant only inside the block. When the
block ends, the outer `value` is visible again.

Function parameters and local bindings also belong to their function scope.
Nested blocks can introduce short-lived names without changing an outer
binding.

Shadowing is useful when a nested operation has a natural local name, but avoid
it when two meanings would be difficult to distinguish while reading the
source.

### Choosing Between `const` and `let`

Use the narrowest binding that expresses the calculation:

| Situation | Prefer |
| --- | --- |
| A fixed option, size, name, or calculated result | `const` |
| A loop counter managed by the language | The loop binding |
| A running offset, checksum, or accumulator | `let` |
| A value that changes only inside a small block | Block-local `let` |
| A value whose width is part of a file format | Explicit type annotation |
| A temporary value whose type is obvious | Type inference |

Most bindings in format descriptions and generated assembly should remain
constants. Mutable state is clearest when it is local, short-lived, and tied to
one calculation.

The next chapter explains the expressions that produce these values, including
arithmetic, comparisons, boolean logic, bitwise operations, field access, and
function calls.

## 3. Expressions

### Expressions Produce Compile-Time Values

An expression calculates a value while XIRASM is assembling the source.
Expressions appear in declarations, assignments, function arguments, return
statements, conditions, assertions, instruction operands, and data calls.

```asm
const entry_count = 3
const bytes_per_entry = 8
const table_size = entry_count * bytes_per_entry

dd(table_size);
```

This emits the 32-bit value `24`. The multiplication is performed during
assembly; the output contains only the result.

### Arithmetic

Integer expressions support:

| Operator | Meaning |
| --- | --- |
| `+` | Addition |
| `-` | Subtraction |
| `*` | Multiplication |
| `/` | Integer division |
| `%` | Remainder |

```asm
const total = 2 + 3 * 4
const grouped = (2 + 3) * 4
const quotient = 17 / 5
const remainder = 17 % 5

dd(total, grouped, quotient, remainder);
```

The emitted values are `14`, `20`, `3`, and `2`.

Addition, subtraction, and multiplication are checked. A result outside the
supported integer range is rejected instead of silently changing the layout.
Division and remainder by zero are errors.

Unary `+` keeps an integer value unchanged. Unary `-` produces its 64-bit
two's-complement value:

```asm
const all_bits = -1
dq(all_bits);
```

This emits eight `ff` bytes.

### Bitwise Operations and Shifts

Bitwise expressions are useful for permissions, masks, instruction fields, and
file-format flags:

| Operator | Meaning |
| --- | --- |
| `&` | Bitwise AND |
| `\|` | Bitwise OR |
| `^` | Bitwise XOR |
| `~` | Bitwise NOT |
| `<<` | Shift left |
| `>>` | Shift right |

```asm
const readable = 1 << 0
const writeable = 1 << 1
const executable = 1 << 2

const permissions = readable | executable
const can_execute = (permissions & executable) != 0
const can_write = (permissions & writeable) != 0

assert(can_execute);
assert(!can_write);
db(permissions);
```

The emitted byte is `05`.

Use parentheses around mask tests. They make the intended operation clear and
avoid depending on the relative precedence of bitwise and comparison
operators.

### Comparison and Equality

Expressions can compare values:

| Operator | Meaning |
| --- | --- |
| `==` | Equal |
| `!=` | Not equal |
| `<` | Less than |
| `<=` | Less than or equal |
| `>` | Greater than |
| `>=` | Greater than or equal |

Ordering comparisons operate on integers:

```asm
const payload_size = 96
const maximum_size = 128
const fits = payload_size > 0 && payload_size <= maximum_size

assert(fits);
```

Equality and inequality also work with compatible non-integer values:

```asm
const format_name = "raw"
const signature = b"OK"

assert(format_name == "raw");
assert(signature == b"OK");
```

Values of unrelated categories are rejected instead of being converted
implicitly.

### Boolean Logic and Short-Circuiting

Boolean expressions use:

| Operator | Meaning |
| --- | --- |
| `!` | Logical NOT |
| `&&` | Logical AND |
| `\|\|` | Logical OR |

`&&` and `||` short-circuit:

- `left && right` does not evaluate `right` when `left` is false.
- `left || right` does not evaluate `right` when `left` is true.

```asm
const enabled = true
const safe = enabled || (1 / 0 == 0)

assert(safe);
```

The division is never evaluated because `enabled` is true.

Short-circuiting is useful when a later expression is valid only after an
earlier condition has been established.

### Function Calls in Expressions

A value-returning function or builtin call can appear anywhere its result type
is accepted:

```asm
const name_length = lengthof("XIRASM")
const aligned_size = ((37 + 15) / 16) * 16

db(name_length);
dd(aligned_size);
```

Calls may be nested:

```asm
const normalized = upper(trim("  kernel  "))
assert(normalized == "KERNEL");
```

Procedure functions that emit output or perform assembler actions are called
as statements. They do not produce expression values. Value-returning
functions are covered in Chapter 5.

### Field Access

A value with named fields uses `.` to select one field:

```text
header.magic
header.entry_offset
```

Struct and union values are introduced in Chapter 9. Target queries such as
`target.bits` and `target.isa` belong to target conditions and are introduced
with control flow; they are not ordinary values that should be copied into a
binding.

### Operator Precedence

Operators on the same row have the same precedence. Rows nearer the top bind
more tightly:

| Precedence | Operators |
| --- | --- |
| Highest | Function calls, grouped expressions, field access |
| Unary | `+`, `-`, `~`, `!` |
| Multiplicative | `*`, `/`, `%` |
| Shifts | `<<`, `>>` |
| Additive | `+`, `-` |
| Bitwise AND | `&` |
| Bitwise XOR | `^` |
| Bitwise OR | `\|` |
| Ordering | `<`, `<=`, `>`, `>=` |
| Equality | `==`, `!=` |
| Logical AND | `&&` |
| Lowest | `\|\|` |

Operators at the same precedence are evaluated from left to right.

XIRASM deliberately follows this table, which may differ from the precedence
you remember from another language. In particular, shifts bind more tightly
than addition and subtraction:

```asm
const shift_first = 1 + 2 << 3
const grouped_shift = (1 + 2) << 3

dd(shift_first, grouped_shift);
```

The first value is `17` because it is evaluated as `1 + (2 << 3)`. The second
value is `24`.

Use parentheses whenever an expression mixes shifts, arithmetic, masks, or
comparisons. Parentheses document the binary layout calculation and make later
changes safer.

### Expression Errors

XIRASM rejects expressions when it cannot produce a well-defined compile-time
value. Common causes include:

- an undefined name;
- an operand of the wrong value category;
- division or remainder by zero;
- arithmetic overflow or subtraction underflow;
- an unknown field;
- a function called with invalid arguments.

These errors stop assembly. They are not converted to zero or ignored, because
doing so could silently corrupt instruction operands or binary layout.

The next chapter uses expressions inside `if`, `while`, and `for` to select and
repeat source during assembly.

## 4. Control Flow

### Control Flow Runs During Assembly

XIRASM control flow decides which source operations are performed while the
output is being constructed. It does not automatically create runtime branches
or loops.

```asm
const debug_build = false

if debug_build {
    db("DEBUG");
} else {
    db("RELEASE");
}
```

Only one branch contributes bytes. With `debug_build` set to false, the output
contains `RELEASE`; no runtime condition is present in the generated file.

Use compile-time control flow to:

- select target-specific source;
- generate repeated data or instructions;
- walk compile-time collections;
- calculate tables and offsets;
- include optional file-format records;
- validate source-controlled configuration.

Runtime control flow remains ordinary ISA text such as x86 `jmp`, `call`, and
conditional branch instructions.

### `if` and `else`

An `if` condition must produce a boolean value:

```asm
const payload_size = 32

if payload_size <= 64 {
    db(0x01);
} else {
    db(0xff);
}
```

The selected block executes in its own scope. The other block produces no
output and performs no compile-time actions.

The `else` block is optional:

```asm
const include_marker = true

if include_marker {
    db("MARK");
}
```

Use `else if` when a calculation needs more than two branches:

```asm
const payload_size = 32

if payload_size < 16 {
    db(1);
} else if payload_size < 64 {
    db(2);
} else {
    db(3);
}
```

This emits `02`.

### Selecting ISA Text

An `if` block may contain natural ISA text:

```asm
x86.use64();

const return_zero = true

entry:
    if return_zero {
        xor eax, eax
    } else {
        mov eax, 1
    }
    ret
```

Because `return_zero` is true, the generated instructions are:

```asm
xor eax, eax
ret
```

The unused `mov` instruction is never added to the output. This is compile-time
instruction selection, not a runtime conditional branch.

### Target Conditions

Target queries are available directly in `if` conditions:

```asm
if target.isa == .x86_64 {
    x86.use64();
}

if target.bits == 64 {
    db("wide");
} else {
    db("narrow");
}
```

`target.isa` identifies the selected ISA family, and `target.bits` reports the
active width. Mode calls such as `x86.use32();`, `x86.use64();`,
`riscv.use32();`, and `riscv.use64();` update the width observed by later
target conditions.

Target queries use dedicated condition syntax. Use them directly in an `if`
condition rather than copying `target.bits` or `target.isa` into an ordinary
binding.

### Iterating a Range

Use `for` with `range(start, end)` when the iteration count is known:

```asm
for index in range(0, 4) {
    db(index);
}
```

This emits:

```text
00 01 02 03
```

The start value is included and the end value is excluded. A range must move
forward or be empty. A descending range such as `range(4, 0)` is rejected.

The loop binding belongs to the loop body:

```asm
for index in range(0, 3) {
    const encoded = index + 0x10
    db(encoded);
}
```

This emits `10 11 12`. Both `index` and `encoded` are local to each loop
iteration.

### Iterating a List

A `for` loop can visit the values in a compile-time list:

```asm
const opcodes: list = list.of(0x90, 0x90, 0xc3)

for opcode in opcodes {
    db(opcode);
}
```

The values are visited in list order. This form is useful for byte tables,
import descriptions, generated names, and other data already represented as a
collection.

Maps are not iterated directly. Use `map.keys(...)` or `map.values(...)` to
obtain a list, then iterate that list. Collections are covered in Chapter 6.

### `while`

Use `while` when termination depends on a value updated by the loop:

```asm
let value = 1

while value <= 4 {
    db(value);
    value = value + 1
}
```

This emits:

```text
01 02 03 04
```

The condition is evaluated before each iteration. If it is false initially,
the body does not execute.

Keep the termination rule visible near the loop. A compile-time loop that
cannot terminate would otherwise prevent assembly from completing, so XIRASM
rejects control flow that exceeds 1,000,000 iterations.

### `break` and `continue`

`break` ends the innermost active Meta loop. `continue` skips the rest of the
current iteration and starts the next one. Both are valid in `for`, `while`,
and deferred `while` bodies, including inside nested `if` statements.

For example:

```asm
for value in range(0, 8) {
    if (value == 6) {
        break;
    }
    if (value & 1) != 0 {
        continue;
    }
    db(value);
}
```

This emits `00 02 04`. A loop-control statement does not cross a function call
boundary, and using one outside a loop is an error.

### Choosing a Control-Flow Form

| Need | Use |
| --- | --- |
| Include one of two source blocks | `if` / `else` |
| Include one of several source blocks | `if` / `else if` / `else` |
| Select source for an ISA or bit width | Target condition |
| Repeat a known number of times | `for` with `range` |
| Visit compile-time collection values | `for` with a list |
| Repeat until mutable state reaches a condition | `while` |
| Select generated instructions | `if` containing ISA text |
| End the innermost loop early | `break` |
| Skip the rest of one iteration | `continue` |

Prefer `for` over `while` when the iteration set is already known. It makes the
generated output easier to reason about and removes the need for a manually
updated loop variable.

The next chapter packages repeated calculations and output operations into
functions with parameters, return values, and local scopes.

## 5. Functions and Scope

### Functions Package Compile-Time Work

A function gives a name to compile-time work that should be reused. It may
calculate a value, emit output, or combine several lower-level operations into
one source-level action.

XIRASM has two function forms:

- a **procedure** performs actions and does not produce an expression value;
- a **value-returning function** calculates a value and declares its result
  type with `->`.

Both forms use `fn`, parameters, and a block body. They execute during
assembly, not when the generated program runs.

Function declarations are top-level declarations. Declaring a function does
not emit bytes. Output is produced only when a procedure is called or when a
calculated result is passed to an output API.

### Procedure Functions

A procedure groups output-producing operations:

```asm
fn emit_pair(value: u8) {
    db(value);
    db(value + 1);
}

emit_pair(2);
emit_pair(8);
```

This emits:

```text
02 03 08 09
```

The function has no `->` return type, so it is a procedure. Calls to a
procedure are statements and end with a semicolon.

A procedure may contain the same compile-time statements that could appear at
the call site: declarations, assignments, control flow, data emission, and
other procedure calls. This makes procedures suitable for repeated binary
records, instruction sequences, table entries, and format-building steps.

The procedure itself is not a value. It cannot be used to initialize a
binding:

```text
fn emit_marker() {
    db(0x90);
}

const marker = emit_marker()
```

This is rejected because `emit_marker()` performs work but does not return a
value.

### Parameters and Arguments

Parameters are written inside the declaration parentheses:

```asm
fn emit_run(value: u8, count: u64) {
    for index in range(0, count) {
        db(value);
    }
}

emit_run(0xcc, 4);
```

The output is four `cc` bytes. The `index` binding controls the loop even
though the body does not otherwise use it.

A parameter type is optional:

```asm
fn add(left, right) -> u64 {
    return left + right;
}

db(add(20, 22));
```

This emits `2a`. The argument values determine the parameter values for that
call. Type annotations are still useful for public helpers because they state
the expected contract and reject unsuitable arguments near the call site.

Arguments are positional. A call must provide exactly one argument for every
parameter, in declaration order. Functions do not have default arguments.

Parameter names must be unique within a function declaration. Each call gets
its own parameter bindings, so one invocation does not overwrite the values
of another invocation.

Procedure parameters are read-only by default. Prefix a parameter with `let`
when the procedure must update the caller's binding:

```asm
fn set_entry(let plan: map, address: u64) {
    map.set_mut(plan, "entry", address);
}

let image: map = map.new()
set_entry(image, 0x401000);
assert(map.get(image, "entry") == 0x401000);
```

The corresponding argument must be a direct `let` binding. A `const`, literal,
temporary expression, or the same binding passed to two `let` parameters is
rejected. Value-returning functions do not accept `let` parameters because an
expression call cannot write back caller state.

### Value-Returning Functions

Add `-> type` when a function should produce an expression value:

```asm
fn align_up(value: u64, alignment: u64) -> u64 {
    return ((value + alignment - 1) / alignment) * alignment;
}

const header_size = align_up(0x73, 0x20)
dw(header_size);
```

The function returns `0x80`, so the output is:

```text
80 00
```

The return statement ends with a semicolon. Its expression is converted to
the declared return type. Return types may describe ordinary compile-time
values such as integers, booleans, strings, and byte sequences:

```asm
fn is_page(value: u64) -> bool {
    return value == 0x1000;
}

fn signature() -> bytes {
    return b"XR";
}

assert(is_page(0x1000));
db(signature());
```

A value-returning function can be used anywhere an expression of its result
type is accepted: in a declaration, an argument, a condition, another
function call, or a larger expression.

### Return Rules and Side Effects

A value-returning function must reach a `return` statement when it is called.
Falling out of the function body without a value is rejected:

```text
fn incomplete(value: u64) -> u64 {
    const doubled = value * 2
}

const result = incomplete(4)
```

The returned value must be compatible with the declared type:

```text
fn enabled() -> bool {
    return 1;
}

const result = enabled()
```

This is rejected because an integer result does not satisfy a `bool` return
contract.

Value-returning functions are calculation helpers. They may use local
bindings, control flow, and other value-returning functions, but they may not
emit output or perform other assembler side effects:

```text
fn bad_counter() -> u64 {
    db(1);
    return 1;
}

const result = bad_counter()
```

Use a procedure when output or layout must change. Use a value-returning
function when the caller needs a calculated value.

A procedure does not declare a return type and cannot return a value:

```text
fn emit_one() {
    return 1;
}

emit_one();
```

The end of the procedure body completes the call naturally.

### Function-Local Scope

Parameters and bindings declared inside a function belong to that invocation:

```asm
fn adjusted_size(size: u64) -> u64 {
    const overhead = 4
    let result = size + overhead

    if result < 16 {
        result = 16
    }

    return result;
}

db(adjusted_size(3));
db(adjusted_size(20));
```

This emits `10 18`. Each call creates a fresh `size`, `overhead`, and `result`.
Those names do not remain available after the call.

Blocks inside a function create nested scopes and may shadow an outer name:

```asm
fn combine(value: u64) -> u64 {
    let result = value

    {
        const value = 5
        result = result + value
    }

    return result;
}

db(combine(3));
```

This emits `08`. Inside the nested block, `value` means the local constant
`5`. After that block ends, the parameter named `value` is visible again.
The mutable `result` belongs to the function scope, so the nested block may
update it.

Prefer local names for intermediate calculations. They keep temporary state
from leaking into the rest of the source and make repeated calls independent.

### Declaration Order

A function must be declared before the first source statement that calls it:

```asm
fn add(left: u64, right: u64) -> u64 {
    return left + right;
}

const answer = add(20, 22)
db(answer);
```

Moving the call before the declaration is rejected because the function name
is not known at that point in the source.

Function declarations themselves must appear at the top level. A function
cannot be declared inside another function, loop, conditional block, or
ordinary scoped block. Put related functions next to each other at the top
level, then call them from later source.

Chapter 10 explains how included files make reusable function declarations
available to another source file.

### Recursion and Call Depth

A value-returning function may call itself when the calculation has a clear
base case:

```asm
fn triangular(value: u64) -> u64 {
    if value == 0 {
        return 0;
    }

    return value + triangular(value - 1);
}

db(triangular(4));
```

This emits `0a`, the value of `4 + 3 + 2 + 1`.

Recursive functions still execute during assembly. Keep recursion shallow and
make the terminating condition obvious. XIRASM rejects function call chains
deeper than 128 calls, preventing uncontrolled recursion from exhausting the
assembler.

Use a loop when the work is naturally an iteration over a range or
collection. Use recursion when the calculation itself is recursive and the
base case remains easy to verify.

### Choosing a Function Form

| Need | Use |
| --- | --- |
| Emit bytes or instructions | Procedure |
| Perform several output API calls as one action | Procedure |
| Calculate a value for an expression | Value-returning function |
| Reuse a pure layout or encoding calculation | Value-returning function |
| Keep temporary names private to one operation | Either function form |
| Repeat over a simple range or collection | Usually a loop |
| Express a naturally recursive calculation | Recursive value function |

Keep each function focused on one job. A small calculation function is easier
to combine with other expressions, while a small procedure makes output
effects visible at the call site.

The next chapter introduces lists, maps, strings, and byte sequences for
functions that need to work with collections and structured text.

## 6. Collections and Text

### Collections Are Compile-Time Values

Assembly programs often describe more than individual integers. A source file
may need an ordered list of fields, a table of named properties, a generated
symbol name, or an exact sequence of bytes.

XIRASM provides four common value types for this work:

| Type | Represents | Typical use |
| --- | --- | --- |
| `string` | Source-level text | Names, paths, diagnostics, parsed fields |
| `bytes` | Exact binary data | Signatures, encoded integers, binary records |
| `list` | Ordered values | Tables, repeated items, ordered descriptors |
| `map` | String-keyed values | Named options, records, lookup tables |

These are compile-time values. Creating a string, byte sequence, list, or map
does not emit output. Pass the value to an output API when its contents should
become part of the generated file.

Strings, byte sequences, lists, and maps have value-producing helpers. An
operation such as `list.push` or `map.set` returns a new value rather than
modifying the original value in place. Lists and maps also provide explicit
statement APIs for updating a direct `let` binding when building every
intermediate collection would obscure the algorithm. Values inserted through
those APIs are still cloned, so separately bound collections do not alias.

### Strings for Source-Level Text

A string stores text used by the assembly process:

```asm
const raw_name: string = "  Kernel64  "
const name: string = lower(trim(raw_name))

assert(name == "kernel64");
assert(starts_with(name, "kernel"));
assert(ends_with(name, "64"));
assert(contains(name, "nel"));

emit.bytes(name);
```

This emits the eight text bytes for `kernel64`.

The most common string helpers are:

- `trim(text)` removes surrounding whitespace;
- `lower(text)` and `upper(text)` normalize ASCII letter case;
- `starts_with`, `ends_with`, and `contains` test text;
- `replace(text, needle, replacement)` replaces matching text;
- `to_string(value)` produces a textual representation of a value.

String case conversion is ASCII-oriented. Use strings for source names,
configuration text, paths, diagnostics, and other human-readable values. Use
`bytes` when every binary byte must be preserved exactly.

### Splitting and Joining Text

`split` turns delimited text into a list of strings. `join` combines a list of
strings:

```asm
const sections: list = split("text,data,bss", ",")
const path: string = join(sections, "/")

assert(len(sections) == 3);
assert(list.get(sections, 0) == "text");
assert(list.get(sections, 2) == "bss");
assert(path == "text/data/bss");

emit.bytes(path);
```

Indexes are zero-based. `list.get(sections, 0)` returns the first element.

Splitting is useful for small source-level formats, command-style text, and
generated names. More structured external data belongs in Chapter 10, which
covers files, JSON, and TOML.

### Bytes for Binary Values

A `bytes` value is an exact sequence of bytes:

```asm
const magic: bytes = bytes.from_hex("58495200")
const version: bytes = bytes.le(3, 2)
const header: bytes = bytes.concat(magic, version)

assert(bytes.eq(magic, bytes.from_hex("58495200")));
assert(bytes.hex(version) == "0300");

emit.bytes(header);
```

The output is:

```text
58 49 52 00 03 00
```

`bytes.from_hex` converts hexadecimal text into binary data.
`bytes.le(value, width)` encodes an integer in little-endian order using the
requested number of bytes. `bytes.concat` combines two byte sequences.

Use `bytes.eq` for explicit byte-sequence equality and `bytes.hex` when a
binary value must be displayed as lowercase hexadecimal text.

### Building Byte Sequences

Byte helpers return new values, so a binary record can be developed in clear
steps:

```asm
const base: bytes = bytes.from_hex("414243")
const marked: bytes = bytes.insert(base, 1, b"-")
const patched: bytes = bytes.replace(marked, 2, 1, b"Z")
const trailer: bytes = bytes.repeat(2, 0xff)
const result: bytes = bytes.concat(patched, trailer)

emit.bytes(result);
```

This emits:

```text
41 2d 5a 43 ff ff
```

The operations are:

- `bytes.new()` creates an empty sequence;
- `bytes.push(value, byte)` appends one byte;
- `bytes.repeat(count, byte)` creates repeated bytes;
- `bytes.insert(value, index, addition)` inserts bytes at a zero-based index;
- `bytes.replace(value, index, count, replacement)` replaces a byte range;
- `bytes.concat(left, right)` joins two sequences.

The original `base` value remains `41 42 43`. Each operation produces a new
value that may be kept, reused, or passed into the next operation.

### Lists Preserve Order

A list stores an ordered sequence of compile-time values:

```asm
const base: list = list.of(1, 2, 3)
const extended: list = list.push(base, 4)
const patched: list = list.set(extended, 1, 0xaa)
const middle: list = list.slice(patched, 1, 2)

assert(list.eq(base, list.of(1, 2, 3)));
assert(list.eq(patched, list.of(1, 0xaa, 3, 4)));
assert(list.eq(middle, list.of(0xaa, 3)));

for value in patched {
    db(value);
}
```

This emits:

```text
01 aa 03 04
```

`list.set` returns a list with one element replaced. `list.slice` takes a
starting index and a count. Neither operation changes its input list.

Other common list operations are:

- `list.new()` creates an empty list;
- `list.get(value, index)` reads one element;
- `list.concat(left, right)` joins two lists;
- `list.eq(left, right)` compares list contents;
- `len(value)` returns the number of elements.

Lists work naturally with `for` because their order is part of the value.
They are a good choice for section descriptions, table rows, byte chunks, and
any other data that must be processed in a defined sequence.

### Updating `let`-Bound Collections

Use the mutation statement APIs when one local algorithm incrementally builds
a list or map:

```asm
let items: list = list.of(1, 2)
list.push_mut(items, 3);
list.set_mut(items, 0, 4);

let options: map = map.new()
map.set_mut(options, "arch", "x64");
map.set_mut(options, "items", items);
```

The first argument must be a direct identifier bound with `let`. A `const`
binding, temporary expression, function result, field access, missing name, or
value of the wrong collection type is rejected. Lexical lookup uses the
nearest binding, so a block-local `let` shadows an outer binding. Top-level
`let` values may be updated during ordinary lowering; a value function may
update only its own local `let` bindings.

`list.push_mut` appends one cloned item. `list.set_mut` replaces the item at an
existing zero-based index. `map.set_mut` inserts or replaces a string-keyed
entry while preserving the position of an existing key. Mutation statements
do not produce expression values and are unavailable in `defer` and
`late_layout` blocks.

Cloning remains the isolation boundary:

```asm
let items: list = list.of(1)
const snapshot: list = items
list.push_mut(items, 2);

assert(list.eq(snapshot, list.of(1)));
assert(list.eq(items, list.of(1, 2)));
```

### Lists Can Hold Larger Values

List elements are not limited to individual integers. A list can organize
strings, byte sequences, maps, or other compile-time values:

```asm
const chunks: list = list.concat(
    list.of(b"XR"),
    list.of(bytes.le(0x1234, 2))
)

for chunk in chunks {
    emit.bytes(chunk);
}
```

This emits:

```text
58 52 34 12
```

This pattern is useful when a record is easiest to describe as ordered pieces.
The list controls order; each byte sequence controls its exact binary
representation.

### Maps Store Named Values

A map associates string keys with compile-time values:

```asm
const base: map = map.set(map.new(), "arch", "x64")
const configured: map = map.set(base, "mode", "release")
const changed: map = map.set(configured, "arch", "rv64")
const complete: map = map.set(changed, "tags", list.of("asm", "dsl"))

assert(len(complete) == 3);
assert(map.has(complete, "arch"));
assert(!map.has(complete, "missing"));
assert(map.get(base, "arch") == "x64");
assert(map.get(changed, "arch") == "rv64");
assert(map.get_or(complete, "missing", "default") == "default");
assert(list.eq(map.get(complete, "tags"), list.of("asm", "dsl")));
```

`map.set` adds a key or returns a new map with that key replaced. Earlier map
values remain unchanged, so `base` still contains `"x64"` after `changed`
stores `"rv64"`.

Use:

- `map.has(value, key)` to test whether a key exists;
- `map.get(value, key)` when the key is required;
- `map.get_or(value, key, fallback)` when the key is optional;
- `map.eq(left, right)` to compare map contents.

`map.eq` compares key/value contents rather than insertion order.

### Iterating Map Contents

Maps are lookup-oriented values. Convert their keys or values to a list before
iteration:

```asm
const fields: map = map.set(
    map.set(map.new(), "magic", b"XR"),
    "version",
    bytes.le(3, 2)
)

const keys: list = map.keys(fields)
const values: list = map.values(fields)

assert(len(keys) == 2);
assert(len(values) == 2);

for value in values {
    emit.bytes(value);
}
```

Use `map.keys` when processing names and `map.values` when processing stored
values. If a particular processing order is part of the file format, keep that
order in a list and use a map only for lookup.

### Composing Collections in Functions

Collections become especially useful when a function transforms a description
into binary data:

```asm
fn encode_u16(values: list) -> bytes {
    let result: bytes = bytes.new()

    for value in values {
        result = bytes.concat(result, bytes.le(value, 2))
    }

    return result;
}

const words: list = list.of(0x1234, 0xabcd)
emit.bytes(encode_u16(words));
```

This emits:

```text
34 12 cd ab
```

The function receives an ordered description, builds an immutable byte value
one step at a time, then returns the completed encoding. The caller decides
when and where to emit it.

This separation scales well:

- strings describe names and source-level text;
- maps describe named properties;
- lists preserve output order;
- byte sequences hold the final binary representation;
- procedures decide where that representation is emitted.

### Choosing a Collection Type

| Need | Use |
| --- | --- |
| Human-readable source text | `string` |
| Exact binary representation | `bytes` |
| Ordered sequence of values | `list` |
| Lookup by a string key | `map` |
| Parse small delimited text | `split` into a `list` |
| Reconstruct delimited text | `join` |
| Build a binary record in memory | `bytes` helpers |
| Preserve order and also support lookup | A `list` plus a `map` |

Prefer the type that matches the meaning of the data. Do not use a string as a
binary buffer, and do not use a map when sequence is part of the format.

The next chapter introduces tokens and pattern matching for source text that
must be understood as language syntax rather than plain strings.

## 7. Tokens and Pattern Matching

### Tokens Represent Source Structure

String helpers treat text as characters. That is enough for names, paths,
delimited fields, and simple replacements. Source-like text often needs a
different model.

Consider:

```text
load rax, [rbx+(rcx*4)]
```

The commas, brackets, parentheses, names, operators, and integer literals have
structural meaning. Splitting on spaces would lose that structure and would
handle equivalent spacing inconsistently.

XIRASM can convert source-like text into a list of tokens:

```asm
const source: string = "count + 0x2a"
const source_tokens: list = tokens.of(source)

assert(len(source_tokens) == 3);
assert(list.get(source_tokens, 0) == "count");
assert(list.get(source_tokens, 1) == "+");
assert(list.get(source_tokens, 2) == "0x2a");

emit.bytes(tokens.join(source_tokens));
```

This emits the canonical text:

```text
count+0x2a
```

`tokens.of` discards insignificant whitespace and preserves the token
sequence. `tokens.join` renders that sequence with canonical spacing. It is not
a byte-for-byte restoration of the original string.

Use strings when character layout matters. Use tokens when names, punctuation,
operators, literals, and balanced groups matter.

### Matching a Token Shape

`match.tokens(pattern, input)` compares source-like input with a token pattern:

```asm
const line: string = "load rax, [rbx+(rcx*4)]"
const result: map = match.tokens(
    "=load destination:name =, source:tokens",
    line
)

assert(map.get(result, "ok"));

const captures: map = map.get(result, "captures")
assert(map.get(captures, "destination") == "rax");
assert(tokens.join(map.get(captures, "source")) == "[rbx+(rcx*4)]");
```

The result is a map with two fields:

- `"ok"` is a boolean that reports whether the complete input matched;
- `"captures"` is a map containing values extracted by named captures.

Read captures only after checking `"ok"`.

### Literal Pattern Tokens

A pattern piece beginning with `=` matches one exact token:

```asm
const result: map = match.tokens(
    "left:name =&& right:name",
    "ready && enabled"
)

assert(map.get(result, "ok"));
```

`=&&` requires the logical-and token. The same rule applies to names and
punctuation:

```text
=load      exact name token
=,         exact comma token
=[         exact opening bracket token
===        exact equality token
```

The first `=` is the pattern marker. Therefore `===` means “match the `==`
token,” not a three-character equality operator.

Pattern pieces are separated by whitespace. Whitespace in the pattern makes
the pattern readable; matching is based on tokens rather than the original
spacing between them.

### Capture Kinds

A capture has the form `name:kind`. The capture name becomes a key in the
result's `"captures"` map.

| Kind | Matches | Captured value |
| --- | --- | --- |
| `token` | One token of any kind | `string` |
| `name` | One identifier-like name | `string` |
| `int` | One integer literal | Integer value |
| `quoted` | One quoted token | Unquoted `string` |
| `tokens` | A balanced token range | `list` of token strings |

Capture names must be unique within a pattern.

The single-token capture kinds make small command grammars precise:

```asm
const assignment: map = match.tokens(
    "=set target:name =, value:int",
    "set count, 0x2a"
)
const assignment_captures: map = map.get(assignment, "captures")

const operation: map = match.tokens(
    "left:name operator:token right:name",
    "count + step"
)
const operation_captures: map = map.get(operation, "captures")

const message: map = match.tokens("=db text:quoted", "db 'READY'")
const message_captures: map = map.get(message, "captures")

assert(map.get(assignment, "ok"));
assert(map.get(assignment_captures, "target") == "count");
assert(map.get(assignment_captures, "value") == 42);
assert(map.get(operation_captures, "operator") == "+");
assert(map.get(message_captures, "text") == "READY");
```

An `int` capture converts the literal to an integer value. A `quoted` capture
removes its surrounding quotes and decodes supported escapes. A `token`
capture returns the token text without imposing a more specific type.

### Balanced Token Ranges

A `tokens` capture can consume multiple tokens while keeping parentheses,
brackets, and braces balanced:

```asm
const result: map = match.tokens(
    "=load destination:name =, address:tokens",
    "load rax, [rbx+(rcx*4)]"
)
const captures: map = map.get(result, "captures")
const address: list = map.get(captures, "address")

assert(map.get(result, "ok"));
assert(tokens.join(address) == "[rbx+(rcx*4)]");
```

The captured value is a token list, not a string. Keeping it as tokens allows
another matcher to inspect it without tokenizing the text again.

Balanced capture applies to `()`, `[]`, and `{}`. Comparison operators such as
`<` and `>` remain ordinary operator tokens:

```asm
const result: map = match.tokens("expression:tokens", "left < right")
const captures: map = map.get(result, "captures")

assert(map.get(result, "ok"));
assert(tokens.join(map.get(captures, "expression")) == "left<right");
```

### Minimal Matching and Backtracking

When a `tokens` capture is followed by more pattern pieces, it initially
consumes as little as possible. If the remaining pattern does not match, the
capture expands and matching tries again:

```asm
const result: map = match.tokens(
    "prefix:tokens value:int",
    "name 42"
)
const captures: map = map.get(result, "captures")

assert(map.get(result, "ok"));
assert(tokens.join(map.get(captures, "prefix")) == "name");
assert(map.get(captures, "value") == 42);
```

The first attempt gives `prefix` an empty range, but `value:int` cannot match
`name`. The matcher then expands `prefix` to contain `name`, allowing the
integer capture to match `42`.

A typed capture that encounters the wrong token is an ordinary non-match. It
does not make a valid pattern an assembly error:

```asm
const result: map = match.tokens("value:int", "name")
assert(!map.get(result, "ok"));
```

This behavior makes it practical to try several valid patterns in sequence.

### Empty Token Ranges

A `tokens` capture may be empty:

```asm
const result: map = match.tokens("=call arguments:tokens", "call")
const captures: map = map.get(result, "captures")

assert(map.get(result, "ok"));
assert(len(map.get(captures, "arguments")) == 0);
```

When a token range is required to be non-empty, check its length after a
successful match or include another required capture in the pattern.

### Matching Existing Token Lists

The input to `match.tokens` may already be a list of token strings:

```asm
const input: list = tokens.of("load r1, 42")
const result: map = match.tokens(
    "=load destination:name =, value:int",
    input
)
const captures: map = map.get(result, "captures")

assert(map.get(result, "ok"));
assert(map.get(captures, "destination") == "r1");
assert(map.get(captures, "value") == 42);
```

Pass token lists between helpers when several processing steps inspect the
same source fragment. Convert back to text with `tokens.join` only when a text
value is required.

### Trying Alternative Shapes

Patterns do not contain an alternative operator. Try each complete shape in
compile-time control flow:

```asm
const line: string = "store r1, r2"
const load: map = match.tokens(
    "=load destination:name =, source:name",
    line
)

if map.get(load, "ok") {
    const captures: map = map.get(load, "captures")
    emit.bytes(map.get(captures, "destination"));
} else {
    const store: map = match.tokens(
        "=store destination:name =, source:name",
        line
    )

    assert(map.get(store, "ok"));
    const captures: map = map.get(store, "captures")
    emit.bytes(map.get(captures, "destination"));
    emit.bytes(map.get(captures, "source"));
}
```

This emits `r1r2`. Each pattern describes one valid form, while normal
`if`/`else` logic decides which form was present.

For a larger grammar, put each shape in a small value-returning function and
keep the dispatch order visible at the call site.

### Misses and Invalid Patterns

An ordinary shape or type mismatch returns a result with `"ok" == false`.
Examples include:

- an exact literal differs;
- a `name` capture receives punctuation;
- an `int` capture receives a name;
- brackets inside a `tokens` capture are not balanced;
- input remains after the pattern is complete.

A malformed pattern is different. It is rejected as an assembly error rather
than reported as a miss. Pattern errors include an unknown capture kind,
an empty literal, or duplicate capture names:

```text
const invalid = match.tokens(
    "value:name value:int",
    "left 42"
)
```

Both captures use the key `"value"`, so the pattern is invalid.

Keep this distinction in mind:

- **valid pattern, unsuitable input** means `"ok" == false`;
- **invalid pattern definition** is a source error that should be fixed.

### Choosing Between Strings and Tokens

| Need | Use |
| --- | --- |
| Search for a substring | `contains` |
| Check a text prefix or suffix | `starts_with` / `ends_with` |
| Parse simple delimiter-separated text | `split` |
| Preserve names, punctuation, and grouping | `tokens.of` |
| Match one exact source shape | `match.tokens` |
| Extract typed fields | `name`, `int`, or `quoted` captures |
| Capture a nested expression or operand | `tokens` capture |
| Try several command forms | Multiple patterns with `if` / `else` |

Do not tokenize ordinary ISA lines merely to assemble them. Natural ISA text
already belongs directly in the source language. Token matching is most useful
for compact user-defined DSLs, generated source fragments, command-style
records, and reusable compile-time helpers.

This completes the language-fundamentals part of the guide. The next chapter
begins the assembler model with targets, natural ISA text, labels, and
references.

## Part II: The Assembler Model

## 8. Targets, ISA Text, and Labels

The compile-time language decides what to generate. The assembler model
decides where generated instructions and data belong, which instruction set
encodes them, and how symbolic references become concrete values.

This chapter begins that model with three closely related ideas:

- the active target selects an ISA and instruction width;
- natural ISA text creates instruction fragments for that target;
- labels give those fragments stable symbolic names.

For handwritten assembly, these features should feel direct. Select a target,
write instructions, and use labels normally. Dynamic instruction text and
computed label names are available when compile-time generation actually needs
them.

### The Active Target

Every instruction is assembled for an active target. A target contains the ISA
family and the width information required by that ISA.

The command line selects the initial target:

```text
xirasm program.xir -o program.bin --target x86-64
xirasm program.xir -o program.bin --target x86
xirasm program.xir -o program.bin --target rv64
xirasm program.xir -o program.bin --target rv32
xirasm module.spvasm -o module.spv --target spv
```

XIRASM defaults to 64-bit x86 when no other target is selected. Source code may
still select an explicit instruction mode. Doing so makes the source
self-describing and is recommended for examples, reusable includes, and code
that depends on a particular width:

```asm
x86.use64();

entry:
    xor eax, eax
    ret
```

The mode call affects instructions that appear after it. Each instruction
remembers the target that was active when the instruction was created.

### Selecting x86, RISC-V, and SPIR-V

The source-level mode APIs are:

| API | Active instruction mode |
| --- | --- |
| `x86.use16()` | 16-bit x86 |
| `x86.use32()` | 32-bit x86 |
| `x86.use64()` | 64-bit x86 |
| `riscv.use32()` | 32-bit RISC-V |
| `riscv.use64()` | 64-bit RISC-V |
| `spv.use()` | SPIR-V 1.6 module |

A source file may switch modes:

```asm
x86.use16();
mov ax, 1

x86.use32();
mov eax, 2

x86.use64();
mov rax, 3
```

The three instructions retain 16-bit, 32-bit, and 64-bit encoding contexts
respectively. A mode call does not rewrite earlier instructions.

Selecting an encoding mode does not make the generated program switch the
processor's runtime mode. A boot image, kernel, firmware component, or mixed
mode program must still arrange any required runtime transition itself.

RISC-V width selection follows the same source-order rule:

```asm
riscv.use64();

addi x1, x0, 1
addi x0, x0, 0
```

Do not apply x86 mode concepts to RISC-V or SPIR-V. XIRASM keeps their target
settings separate rather than treating every ISA as a generic `mode_bits`
value.

SPIR-V is encoded as one complete logical module rather than as independent
machine instructions. Select it with `spv.use()` and write standard `Op*`
instruction spelling with numeric result IDs:

```asm
spv.use();

OpCapability Shader
OpMemoryModel Logical GLSL450
%1 = OpTypeVoid
```

The command-line aliases are `--target spv` and `--target spirv`; both select
SPIR-V 1.6. A SPIR-V output must contain only SPIR-V ISA lines in one section
and at one module version. It cannot be mixed with x86 or RISC-V instructions,
data emission, reservation, or alignment fragments. Use numeric `%id` spelling
such as `%1`; symbolic SPIR-V IDs are not currently accepted.

### Querying the Target

Compile-time control flow can inspect the active target:

```asm
x86.use64();

if target.isa == .x86_64 {
    assert(target.bits == 64);
    mov eax, 1
}
```

The available target-family values are:

| Value | Target family |
| --- | --- |
| `.x86_64` | x86 in 16-bit, 32-bit, or 64-bit mode |
| `.riscv64` | RISC-V with a 32-bit or 64-bit XLEN |
| `.spirv` | SPIR-V |

The public family names identify the backend family. They do not report the
current instruction width. After `x86.use32()`, for example,
`target.isa == .x86_64` remains true while `target.bits == 32`.

Use `target.bits` when width matters. `target.xlen` is also accepted for
RISC-V width conditions:

```asm
riscv.use32();

if target.isa == .riscv64 {
    assert(target.xlen == 32);
    addi x1, x0, 1
}
```

Target conditions run during assembly. They select which source is generated;
they do not emit a runtime branch.

### Natural ISA Text

Instructions use the normal textual syntax of the selected ISA:

```asm
x86.use64();

entry:
    mov rax, 1
    add rax, 2
    ret
```

An ISA line consists of a mnemonic followed by its operands. Whitespace may be
used for readability, and commas inside brackets, parentheses, or braces
remain part of the nested operand rather than splitting the instruction at the
wrong place.

ISA lines do not end with semicolons. Structured compile-time calls do:

```asm
x86.use64();
emit.u8(0x90);
nop
```

Here `x86.use64();` and `emit.u8(0x90);` are compile-time API calls. `nop` is
natural ISA text.

XIRASM stores the instruction text together with the active target, then asks
the corresponding ISA backend to encode it. The frontend continues to own
labels, layout, output regions, and fixups.

SPIR-V is the exception to per-instruction encoding: its ISA lines are gathered
in source order and encoded together so the module header, ID bound, type
context, and extended-instruction sets are resolved consistently.

### Compile-Time Values in Instructions

Compile-time constants may appear directly in instruction operands:

```asm
x86.use64();

const initial_value: u32 = 40 + 2

entry:
    mov eax, initial_value
    ret
```

There is no need to convert the instruction into a function call. The operand
expression is evaluated as part of assembling the instruction.

Label arithmetic may also remain in natural ISA text:

```asm
x86.use64();
origin(0x1000);

target:
    mov rax, target + 4
    ret
```

The expression resolves to the address of `target` plus four. The reference is
kept symbolic until the instruction and label layout are known.

Prefer readable constants and labels over constructing instruction strings.
Use generated ISA text only when the mnemonic, operand shape, or symbol name is
itself computed.

### Static Labels

A label definition is a name followed by a colon:

```asm
x86.use64();

entry:
    mov eax, 1
    jmp done

done:
    ret
```

Defining a label does not emit bytes. It attaches the name to the current
logical output address.

Labels can be referenced before or after their definitions:

```asm
x86.use64();

entry:
    jmp short finished
    mov eax, 1

finished:
    ret
```

No separate forward declaration is required. The explicit `short` keyword is
part of the x86 instruction text and is preserved as an encoding constraint.

Use ordinary static labels for handwritten control flow, data names, entry
points, and other names known when the source is written. They are the clearest
form and produce the best diagnostics.

### References and Fixups

A symbolic instruction operand cannot always be encoded immediately. The
instruction size, the target address, or both may depend on layout that is
still being constructed.

XIRASM handles this in stages:

1. The source defines labels and creates ISA instruction fragments.
2. The selected backend encodes each instruction and reports symbolic fields
   that still need values.
3. Layout assigns final addresses to labels and fragments.
4. Fixup resolution evaluates each symbolic expression and patches the encoded
   field.

This is why a forward branch works without a manual address calculation:

```asm
x86.use64();

entry:
    jmp target
    nop

target:
    ret
```

The source expresses intent with a label. The assembler owns the displacement
calculation.

Keep symbolic expressions in instruction operands when possible. Manually
subtracting instruction addresses or hardcoding a displacement makes source
fragile when an earlier instruction changes size.

### Dynamic ISA Text and Labels

Compile-time code sometimes creates names or entire instruction lines. Use
`isa(text)` to assemble a computed instruction and `label.define(name)` to
define a computed label:

```asm
const done: string = sym.join("generated_", "done")

isa(sym.join("jmp ", done));
label.define(done);
isa("ret");
```

This creates the same logical structure as:

```asm
jmp generated_done
generated_done:
ret
```

The generated instruction still uses the active target and participates in the
normal label and fixup pipeline.

`label.define` accepts a string and defines that name at the current logical
address. `sym.join` is useful when a stable generated name is composed from
known parts. `sym.unique` is useful inside reusable generators that need a
fresh private name for every expansion.

Use dynamic labels only when the name must be computed. Do not replace clear
static source such as `loop:` or `done:` with generated strings.

### Reading a Label Address

`label_addr(label_or_name)` returns the logical address associated with a
label. It accepts either a normal label reference or a string containing a
dynamic label name:

```asm
x86.use64();

entry:
    ret

dq(label_addr(entry));
```

The example emits a `ret` instruction followed by the logical address of
`entry`.

There is an important difference between an instruction reference and an
ordinary compile-time address query:

- an ISA operand can remain symbolic and be resolved through a fixup;
- `label_addr(...)` produces a compile-time integer at the point where the
  expression is evaluated.

If earlier instruction sizes, alignment, regions, or reserved ranges may still
change the final address, perform the binary backfill in a finalizer rather
than freezing an early value. Chapter 12 explains stable-layout queries and
`defer`.

Executable and object formats may also require a relocation record rather than
a raw absolute integer. Use the format facade's relocation and import/export
APIs for those cases. The Format Tutorial covers that distinction.

### Practical Rules

Use these defaults when writing ordinary XIRASM source:

- select the intended target mode explicitly near the start of the source;
- write handwritten instructions as natural ISA text;
- omit semicolons from ISA lines and retain them on compile-time calls;
- use static labels for names known in the source;
- keep forward references symbolic and let fixup resolution calculate them;
- use `isa` and `label.define` only for genuinely computed source;
- use `target.bits` for width and `target.isa` for the backend family;
- defer final binary address backfills when layout is not yet stable;
- use format relocation APIs when a loader or linker must adjust an address.

The next chapter moves from instruction references to explicit data and binary
layout: integer emission, byte sequences, reserved storage, alignment, structs,
and unions.

## 9. Data and Binary Layout

Instructions are only one source of output bytes. File headers, lookup tables,
messages, constants, reserved memory, and application-specific records all
require explicit data layout.

XIRASM provides two complementary levels:

- data emission APIs write exact integer, string, and byte values;
- binary aggregate types describe reusable structs and unions.

Use direct emission for short layouts and isolated fields. Use aggregates when
several fields form a named record that is created, inspected, or emitted as a
unit.

### Output Is an Ordered Byte Sequence

Data calls append bytes at the current output position:

```asm
emit.u8(0x11);
emit.u16(0x2233);
emit.u32(0x44556677);
emit.u64(0x0102030405060708);
```

The integer APIs emit little-endian values. The generated bytes are:

```text
11
33 22
77 66 55 44
08 07 06 05 04 03 02 01
```

Each API checks that its value fits the requested width. Use a wider API when
the value does not fit rather than silently truncating it.

The compact aliases are:

| Alias | Width | Equivalent use |
| --- | --- | --- |
| `db` | 1 byte | byte values, strings, and `bytes` values |
| `dw` | 2 bytes | little-endian integers |
| `dd` | 4 bytes | little-endian integers |
| `dp` | 6 bytes | little-endian integers |
| `dq` | 8 bytes | little-endian integers |
| `dt` | 10 bytes | zero-extended little-endian integers |
| `ddq` | 16 bytes | zero-extended little-endian integers |
| `dqq` | 32 bytes | zero-extended little-endian integers |
| `ddqq` | 64 bytes | zero-extended little-endian integers |

For example:

```asm
db(0x41);
dw(0x1122);
dd(0x33445566);
dq(0x0102030405060708);
```

The aliases accept multiple arguments. Every alias except `db` requires
integers; `db` additionally accepts strings and byte sequences. Values for
1/2/4/6/8-byte elements must fit exactly. The current Meta integer is `u64`, so
10/16/32/64-byte elements zero-extend it. Use `emit.bytes` for wider exact bit
patterns. `dt` is raw 10-byte data and does not introduce an `f80` type.

### Floating-Point Values

Floating output is explicit and separate from integer data aliases:

```asm
const scale: f64 = 1.5
const compact: f32 = f32(scale)

emit.f32(compact);
emit.f64(scale * 2.0);
```

`emit.f32` and `emit.f64` write the exact little-endian IEEE-754 binary32 and
binary64 encodings. Their arguments must already have the matching type.
Floating arithmetic and comparisons require matching types. Literals,
conversions, and arithmetic results must remain finite; overflow, NaN, and
Infinity are rejected. Finite underflow and signed zero are preserved.

### Strings and Byte Sequences

`db` can combine byte values, strings, and `bytes` values in one call:

```asm
const suffix: bytes = b"CD"

db(0x41, "B", suffix, 0);
```

This emits:

```text
41 42 43 44 00
```

Strings and byte sequences are copied exactly. XIRASM does not add a trailing
zero automatically. Add the terminator explicitly when a file format, ABI, or
runtime interface requires one.

`emit.bytes(value)` writes one string or `bytes` value:

```asm
const signature: bytes = bytes.from_hex("7f454c46")

emit.bytes(signature);
emit.bytes("DATA");
```

Use `bytes` when the value represents exact binary content. Use a string when
the value represents source-level text that happens to be emitted directly.

### Reserving Storage

`reserve(count)` advances the output by a number of bytes:

```asm
db(0xeb);
reserve(2);
db(0xfe);
```

In an ordinary flat binary, this emits:

```text
eb 00 00 fe
```

The reserve aliases multiply a count by an element width:

| Alias | Reserved bytes |
| --- | --- |
| `rb(count)` | `count` |
| `rw(count)` | `count * 2` |
| `rd(count)` | `count * 4` |
| `rp(count)` | `count * 6` |
| `rq(count)` | `count * 8` |
| `rt(count)` | `count * 10` |
| `rdq(count)` | `count * 16` |
| `rqq(count)` | `count * 32` |
| `rdqq(count)` | `count * 64` |

```asm
rb(2);
db(0xaa);
rw(1);
db(0xbb);
```

This emits two zero bytes, `aa`, two more zero bytes, then `bb`.

Reserve expresses unused storage rather than meaningful initialized content.
When initialized output follows it, the reserved range becomes a real zero
filled gap in the flat file. A continuous reserved tail may instead advance
logical size without occupying file bytes. Chapter 11 explains this distinction
through output regions and their real and potential file cursors.

### Padding and Alignment

`pad(count, fill)` writes an exact number of repeated bytes:

```asm
db(1);
pad(3, 0xaa);
db(2);
```

The result is:

```text
01 aa aa aa 02
```

The fill argument is optional and defaults to zero:

```asm
pad(4);
```

`pad_to(position, fill)` writes until the current output byte position reaches
the requested position:

```asm
db(0x11, 0x22);
pad_to(6, 0x90);
db(0x33);
```

This emits:

```text
11 22 90 90 90 90 33
```

The requested position cannot be behind the current output position.

`align(boundary, fill)` advances to the next multiple of a boundary:

```asm
db(0x11, 0x22, 0x33);
align(8, 0xcc);
db(0x44);
```

This emits five `cc` bytes before `44`. The fill argument defaults to zero.

Alignment boundaries must be nonzero powers of two. Values such as 1, 2, 4,
16, and 4096 are valid. Values such as 3 or 24 are rejected.

Choose the operation that expresses the intent:

| Need | Use |
| --- | --- |
| Uninitialized storage of a known size | `reserve` or `rb`/`rw`/`rd`/`rq` |
| An exact number of fill bytes | `pad` |
| Reach a known output position | `pad_to` |
| Reach the next aligned position | `align` |

### Declaring Binary Structs

A struct gives names and types to a sequence of fields:

```asm
struct NaturalHeader {
    tag: u8
    size: u32
}
```

This is a naturally aligned struct. `tag` begins at offset 0. The `u32` field
requires four-byte alignment, so `size` begins at offset 4. The complete struct
occupies eight bytes:

```text
offset 0: tag
offset 1: padding
offset 2: padding
offset 3: padding
offset 4: size
offset 5: size
offset 6: size
offset 7: size
```

Natural layout aligns every field and rounds the final size to the struct's
alignment. This is useful for in-memory records whose layout should follow the
declared field alignments.

For an exact file layout, declare a packed struct:

```asm
packed struct FileHeader {
    tag: u8
    size: u32
}
```

`FileHeader` occupies five bytes. Its `size` field begins immediately at offset
1, and there is no trailing padding.

Packing removes internal and trailing padding, but the type retains the largest
field alignment. When a packed aggregate is nested inside a naturally aligned
aggregate, the outer aggregate aligns that field to the retained alignment.

Packed layout is usually the correct choice for file headers, protocol
records, instruction metadata, and other externally specified byte layouts.
Natural layout is usually the correct choice for native in-memory records.

### Field Defaults and Struct Literals

Integer fields in structs may provide compile-time defaults:

```asm
packed struct Header {
    magic: u16 = 0x5a4d
    flags: u16 = 1
    size: u32
}

const header: Header = Header {
    size: 0x40
}

emit.u16(header.magic);
emit.u32(header.size);
```

The literal supplies `size` and uses the defaults for `magic` and `flags`.
Literal fields are matched by name rather than by source order.

Every omitted field must have a default. Unknown fields, duplicate fields, and
values of the wrong type are source errors.

Union fields cannot declare defaults. A union value must always select exactly
one active field explicitly.

Fields can be read with normal field access, as shown by the two emission calls
in the example.

Aggregate values exist during assembly. They are not automatically written to
the output merely because they were declared.

### Measuring Layout

`sizeof(Type)` returns the complete size of a binary type:

```asm
struct NaturalHeader {
    tag: u8
    size: u32
}

packed struct PackedHeader {
    tag: u8
    size: u32
}

assert(sizeof(NaturalHeader) == 8);
assert(sizeof(PackedHeader) == 5);
assert(offset_of(NaturalHeader, tag) == 0);
assert(offset_of(NaturalHeader, size) == 4);
assert(offset_of(PackedHeader, size) == 1);
```

`offset_of(Type, field)` returns a field offset. Both operations are
compile-time expressions. They may be used in
instructions, emitted fields, assertions, and other layout calculations:

```asm
x86.use64();

packed struct SaveArea {
    rax: u64
    rcx: u64
}

sub rsp, sizeof(SaveArea)
```

Keeping layout calculations symbolic prevents a field addition or type change
from leaving stale hardcoded sizes elsewhere in the source.

### Using Aggregate Layouts on the x86-64 Stack

An aggregate type is an assembly-time layout description. x86 does not provide
an instruction that pushes a complete struct value. Allocate the record on the
stack, then use `offset_of` in ordinary memory operands:

```asm
x86.use64();

struct StackFrame {
    tag: u8,
    value: u32,
    tail: u16,
}

assert(sizeof(StackFrame) == 12);
assert(offset_of(StackFrame, value) == 4);
assert(offset_of(StackFrame, tail) == 8);

sub rsp, sizeof(StackFrame)
mov dword [rsp + offset_of(StackFrame, value)], 0x44332211
mov eax, [rsp + offset_of(StackFrame, value)]
mov word [rsp + offset_of(StackFrame, tail)], 0x6655
add rsp, sizeof(StackFrame)
```

The same form works with packed and nested layouts. A nested field can be used
directly, for example `[rsp + offset_of(NestedFrame, point.y)]`. When this code
calls a Windows x64 function, satisfy the ABI's stack-alignment and shadow-space
requirements separately; `sizeof(StackFrame)` describes only the record.

When this pattern is used in a PE64 program, the source must still satisfy the
Windows x64 calling convention. The structure size describes the local record
layout only; it does not automatically provide call-boundary stack space.

### Packing and Emitting Struct Values

`pack(value)` converts an aggregate value to a `bytes` value:

```asm
packed struct Header {
    magic: u16 = 0x4241
    tail: u16
}

const header: Header = Header {
    tail: 0x4443
}

const encoded: bytes = pack(header)
assert(encoded == b"ABCD");
emit.bytes(encoded);
```

`emit.struct(value)` performs the packing and emission directly:

```asm
struct NaturalHeader {
    tag: u8 = 0x41
    size: u32 = 0x11223344
}

const header: NaturalHeader = NaturalHeader { }
emit.struct(header);
```

This emits eight bytes. The three natural-layout padding bytes are zero:

```text
41 00 00 00 44 33 22 11
```

Use `pack` when the byte sequence must be compared, transformed, stored in a
collection, or passed to another function. Use `emit.struct` when the value
should be written immediately.

### Unions and Active Fields

A union overlays several field types at offset zero:

```asm
packed struct Point {
    x: u16
    y: u16
}

union ValueBits {
    raw: u32
    point: Point
}

const coordinates: ValueBits = ValueBits {
    point: Point {
        x: 0x1122,
        y: 0x3344
    }
}
```

Every union field begins at offset zero. A natural union uses the largest field
size rounded to the largest field alignment. A packed union uses the exact
largest field size.

The `coordinates` literal selects exactly one active field. A union literal
cannot omit every field or initialize several fields at once. Union field
declarations cannot provide defaults because a default would not identify an
explicitly selected active field.

Unions may be nested inside structs:

```asm
packed struct Point {
    x: u16
    y: u16
}

union ValueBits {
    raw: u32
    point: Point
}

packed struct Record {
    kind: u8
    value: ValueBits
}

const record: Record = Record {
    kind: 1,
    value: ValueBits {
        raw: 0xaabbccdd
    }
}

assert(offset_of(Record, value) == 1);
assert(offset_of(Record, value.point.y) == 3);
emit.struct(record);
```

Nested field paths work with `offset_of`, as shown by the two assertions in the
example.

The second result combines the offset of `value` inside `Record` with the
offset of `y` inside `Point`.

### Choosing a Layout Technique

Use direct integer and byte emission when:

- the layout has only a few fields;
- fields are written once and do not need names afterward;
- the source follows an external table closely enough that explicit calls are
  clearer than a type declaration.

Use packed structs and unions when:

- an exact binary record is reused;
- field names improve readability;
- `sizeof` and `offset_of` should drive other calculations;
- values need defaults, nesting, comparison, or conversion to `bytes`.

Use natural structs when:

- the record represents aligned memory rather than a serialized file layout;
- padding is intentional and should follow field alignment.

Always verify externally defined layouts with `sizeof` and `offset_of`
assertions. These assertions turn format assumptions into executable checks
and make later edits safer.

The next chapter introduces modules and files: reusing source with `include`
and `import`, and reading external text, bytes, JSON, and TOML during assembly.

## 10. Modules and Files

Assembly projects quickly grow beyond one source file. Instruction helpers,
binary record definitions, generated tables, configuration, and embedded data
often have different owners and different reasons to change.

XIRASM provides two separate file-loading models:

- `include` and `import` load XIRASM source;
- `fs`, `json`, and `toml` functions load data used by Meta code.

Keeping these roles separate makes a project easier to reason about. Source
files contribute declarations or output operations. Data files produce values
that the source can inspect, transform, and emit.

> **Current I/O status:** `include/io/` is still under development. It is not a
> supported standard-library surface and must not be imported or used by user
> programs yet. The compile-time `fs.*` APIs described in this chapter remain
> supported for reading assembly inputs. Runtime I/O currently requires
> user-owned ISA/ABI routines until `include/io/` is declared stable.

### Importing a Source Module

`import(path)` evaluates a source file at most once:

```asm
import("support.inc");
import("support.inc");

emit_word(0x1234);
```

Suppose `support.inc` contains:

```text
fn emit_word(value: u64) {
    emit.u16(value);
}
```

The two imports refer to the same resolved file, so the function is declared
once. The example emits:

```text
34 12
```

Use `import` for files that define reusable functions, constants, structs, or
other names. Importing the same module through several dependency paths does
not repeat its declarations or output.

An `import` statement must appear at source scope. It cannot be placed inside
a block whose local execution would make module loading conditional:

```text
if target.bits == 64 {
    import("x64-support.inc");
}
```

Select modules at source scope and put target-dependent behavior inside the
module instead.

### Including Source Inline

`include(path)` evaluates the requested source every time it is reached:

```asm
include("inline.inc");
include("inline.inc");
```

If `inline.inc` contains:

```text
emit.u8(0xaa);
```

the output is:

```text
aa aa
```

Use `include` when repeated evaluation is intentional. Typical uses include:

- inserting generated data at the current output position;
- sharing a short sequence of emission statements;
- selecting a source fragment from a computed path;
- applying the same source template more than once.

Because inclusion is repeated, including a file that declares the same
function, constant, or type twice may produce a duplicate-name error. Such a
file is normally a module and should be loaded with `import`.

### How Source Paths Are Resolved

A relative source path is resolved from the directory of the file that
contains the `include` or `import` statement.

For this project:

```text
project/
    main.asm
    tables/
        records.inc
        shared/
            constants.inc
```

`tables/records.inc` may load its neighboring file with:

```text
import("shared/constants.inc");
```

The path is relative to `records.inc`, not necessarily to the process working
directory or the original entry source.

If a relative file is not found beside the current source, XIRASM next checks
the project's `include` directory and then its installed include directory.
This lets project modules override or complement installed libraries without
placing machine-specific absolute paths in source.

Absolute paths are accepted, but portable projects should normally use
source-relative paths or configured include roots.

Source-loading cycles are rejected. A file cannot directly or indirectly
include or import itself while it is already being evaluated.

### Choosing Between `import` and `include`

Choose based on execution semantics, not the filename extension:

| Need | Use |
|---|---|
| Define reusable names once | `import` |
| Share a library of Meta functions or types | `import` |
| Emit a source fragment at the current position | `include` |
| Evaluate the same source more than once | `include` |
| Avoid duplicate declarations through dependency chains | `import` |

A useful project convention is:

- modules describe reusable vocabulary;
- included fragments perform position-dependent work;
- the entry source selects targets and assembles the final output.

### Checking and Reading Data Files

`fs.exists(path)` checks whether a data file can be resolved:

```asm
assert(fs.exists("payload.bin"));
emit.bytes(fs.read_bytes("payload.bin"));
```

Data paths use the same source-relative resolution model as source loading.
When this code is moved into a module, `payload.bin` is resolved relative to
that module.

`fs.read_text(path)` reads an entire file as a `string`:

```asm
const banner: string = fs.read_text("banner.txt");
assert(contains(banner, "XIRASM"));
emit.bytes(banner);
```

If `banner.txt` contains:

```text
XIRASM
```

the exact file bytes are emitted. No terminating zero byte is added
automatically.

`fs.read_bytes(path)` reads an entire file as `bytes`. It is the natural choice
for images, encoded tables, prebuilt records, and other binary payloads.

When the bytes are needed only as output, `emit.file(path)` avoids creating an
intermediate binding. `emit.file(path, offset, count)` emits an exact range.
Both forms use the same source-relative resolver and bounds checks as
`fs.read_bytes`, and are unavailable in `late_layout` and `defer`.

### Reading a Byte Range

The three-argument form of `fs.read_bytes` reads a bounded range:

```asm
const middle: bytes = fs.read_bytes("payload.bin", 1, 2);
assert(len(middle) == 2);
emit.bytes(middle);
```

The second argument is a zero-based file offset. The third is the number of
bytes to return.

If `payload.bin` contains:

```text
10 20 30 40
```

the example emits:

```text
20 30
```

The complete requested range must exist. XIRASM reports an error instead of
silently shortening an out-of-bounds read.

### Loading JSON

`json.file(path)` reads and parses a JSON file in one operation:

```asm
const config: map = json.file("config.json");
const values: list = map.get(config, "values");

assert(map.get(config, "enabled"));
emit.bytes(map.get(config, "name"));
emit.u8(map.get(config, "bits"));
emit.u8(list.get(values, 0));
emit.u8(list.get(values, 1));
```

For this input:

```json
{
  "name": "XR",
  "bits": 64,
  "enabled": true,
  "values": [1, 2]
}
```

the output is:

```text
58 52 40 01 02
```

`json.parse(value)` parses JSON already held in a `string` or `bytes` value:

```asm
const raw: string = fs.read_text("config.json");
const config: map = json.parse(raw);
emit.u8(map.get(config, "bits"));
```

JSON objects become maps, arrays become lists, strings and booleans keep their
corresponding Meta types, and non-negative integers become integer values.
JSON `null` becomes `void`.

Floating-point numbers and negative integers are not part of the current Meta
data model. Duplicate object keys and malformed JSON are rejected.

### Loading TOML

`toml.file(path)` provides the same direct workflow for TOML:

```asm
const config: map = toml.file("project.toml");
const target: map = map.get(config, "target");

emit.bytes(map.get(config, "name"));
emit.u8(map.get(target, "bits"));
```

For this input:

```toml
name = "XR"

[target]
bits = 64
```

the output is:

```text
58 52 40
```

`toml.parse(value)` parses TOML from a `string` or `bytes` value:

```asm
const raw: string = fs.read_text("project.toml");
const config: map = toml.parse(raw);
const target: map = map.get(config, "target");

assert(map.get(target, "bits") == 64);
emit.u8(0x40);
```

TOML tables become maps, arrays become lists, and strings, booleans, and
non-negative integers become their corresponding Meta values. Floating-point
and timestamp values are not currently accepted by the Meta data conversion.

### Separating Configuration from Binary Payloads

Structured configuration and raw binary data solve different problems.

Use JSON or TOML when:

- fields should be addressed by name;
- humans are expected to edit the input;
- one input drives several generated values;
- validation and conditional generation are useful.

Use `fs.read_bytes` when:

- the file contents are already the desired binary representation;
- byte-for-byte preservation matters;
- only a bounded range is needed;
- parsing would add no useful structure.

Use `fs.read_text` when text is itself the payload or when a parser should be
applied explicitly.

### Practical Module Organization

A medium-sized project might use this structure:

```text
project/
    main.asm
    include/
        records.inc
        encoding.inc
    data/
        config.toml
        payload.bin
```

`main.asm` imports reusable definitions:

```text
import("records.inc");
import("encoding.inc");
```

Those modules may read data relative to themselves or through explicit paths
such as `../data/config.toml`.

Keep module behavior visible:

- prefer `import` for declaration libraries;
- reserve repeated `include` calls for deliberately repeated work;
- keep file paths relative to the source that owns them;
- validate required data with `assert` and `fs.exists`;
- parse structured files once and reuse the resulting map or list;
- use bounded binary reads when only part of a large file is needed.

The next chapter explains output regions and virtual data: how XIRASM separates
logical addresses, physical file bytes, reserved space, and temporary layout
areas.

## 11. Output Regions and Virtual Data

An assembler often needs to answer two different questions:

1. At what address will this value exist?
2. At what position, if any, will its bytes appear in the output file?

For a simple flat binary, both answers often begin at zero and advance
together. More structured binaries need them to diverge. A block may load at a
high address while remaining near the beginning of a file. Reserved memory may
increase the address range without adding file bytes. Temporary generated data
may never enter the final file at all.

XIRASM models these cases with output regions.

### Logical and Physical Coordinates

Every ordinary output region has:

- an **origin**, used as the address basis for labels;
- a **file offset**, used as the physical placement of its bytes;
- a **logical size**, including reserved storage;
- a **file size**, containing the bytes that are materialized.

These properties are deliberately independent. Changing an origin does not
insert file padding, and changing a file offset does not change label values.

The most common location queries are:

| Query | Meaning |
|---|---|
| `region_base()` | origin of the active region |
| `here()` | current logical address |
| `file_offset()` | current real file position |
| `file_cursor_real()` | current materialized file cursor |
| `file_cursor_potential()` | file position if all current reserved space became physical |
| `tail_reserve_size()` | unmaterialized reserved bytes at the active tail |

The cursor queries are most useful when building custom layouts or reusable
format helpers. Ordinary code can often work with labels and `here()`.

### Setting an Origin

`origin(address)` sets the address basis of the active region:

```asm
origin(0x4000);

start:
emit.u8(0xaa);

assert(region_base() == 0x4000);
assert(label_addr("start") == 0x4000);
assert(here() == 0x4001);
assert(file_offset() == 1);
```

The output contains one byte:

```text
aa
```

The origin changes label addresses, not physical placement. Set it before
defining labels or emitting content in a simple flat region. A later origin
change updates the basis of the whole active region rather than creating a new
independent area.

### Creating an Explicit Region

`region.begin(name, origin, file_offset)` starts an ordinary output region with
both coordinates specified:

```asm
region.begin("header", 0x1000, 0);

header:
emit.bytes(b"HD");

region.begin("payload", 0x2000, 0x10);

payload:
emit.bytes(b"DATA");

assert(label_addr("header") == 0x1000);
assert(label_addr("payload") == 0x2000);
```

The header occupies file offsets `0` and `1`. The payload begins at file offset
`0x10`. The unwritten space between them is zero-filled in flat output:

```text
48 44 00 00 00 00 00 00 00 00 00 00 00 00 00 00
44 41 54 41
```

Starting another region switches the active output area. Regions are not
lexically nested and do not use an `end` statement.

The region name describes the source layout. It does not by itself create a
section record in any executable or object file format. Those records belong
to the format facade described in the separate executable formats guide.

Use explicit regions when both coordinates are already known. For sequential
construction, `output.section` and `output.org` are usually safer because they
derive the next file offset from the current region.

### Real and Potential File Cursors

Initialized bytes advance both file cursors:

```asm
emit.u8(0xaa);

assert(file_cursor_real() == 1);
assert(file_cursor_potential() == 1);
assert(tail_reserve_size() == 0);
```

A reserve advances the potential cursor and the logical address, but it does
not immediately commit physical bytes:

```asm
emit.u8(0xaa);
reserve(3);

assert(here() == 4);
assert(file_cursor_real() == 1);
assert(file_cursor_potential() == 4);
assert(tail_reserve_size() == 3);
```

If initialized output follows, the reserved range becomes a middle gap:

```asm
emit.u8(0xaa);
reserve(3);
emit.u8(0xbb);

assert(file_cursor_real() == 5);
assert(file_cursor_potential() == 5);
assert(tail_reserve_size() == 0);
```

The file then contains:

```text
aa 00 00 00 bb
```

This delayed commitment is what allows one reserve operation to describe both
ordinary zero-filled gaps and file-free trailing storage.

### Trimming a Reserved Tail

`output.section(name, origin)` starts the next output area at the current
**real** file cursor:

```asm
emit.u8(0x41);
reserve(3);

output.section("next", 0x2000);
emit.u8(0x42);
```

The reserved tail advances the first area's logical size, but the next area
starts immediately after its last initialized byte. The output is:

```text
41 42
```

This is useful when a memory range should exist at runtime without consuming
space at the end of its file-backed area.

`output.section` accepts a name and a new logical origin. It derives the file
offset from the real cursor and therefore discards only the unmaterialized
reserved tail. Earlier middle gaps remain physical.

### Preserving a Potential Gap

`output.org(name, origin)` starts the next output area at the current
**potential** file cursor:

```asm
emit.u8(0x41);
reserve(3);

output.org("next", 0x2000);
emit.u8(0x42);
```

Because initialized output now appears after the reserved range, that range is
part of the file:

```text
41 00 00 00 42
```

The difference between the two operations is therefore precise:

| Operation | Next file offset |
|---|---|
| `output.section` | real cursor, trimming the active reserved tail |
| `output.org` | potential cursor, preserving the active reserved range |

Both operations choose a new logical origin independently of that file
position.

### Closing a Region with File Alignment

`region.file_align(alignment)` aligns the active region's final physical size:

```asm
region.begin("first", 0x1000, 0);

emit.bytes(b"ABC");
reserve(13);

assert(here() == 0x1010);
assert(file_cursor_real() == 3);
assert(file_cursor_potential() == 16);

region.file_align(8);

region.begin("second", 0x2000, 8);
emit.u8(0x5a);
```

The reserved tail is removed before the three physical bytes are rounded up to
an eight-byte file boundary. The second region begins at file offset `8`:

```text
41 42 43 00 00 00 00 00 5a
```

File alignment does not advance the logical size. In this example the first
region still describes sixteen logical bytes.

The alignment must be a nonzero power of two. Calling `region.file_align`
closes physical output for the active region; start another region before
emitting more bytes.

This operation is different from `align`:

- `align` advances the logical position and emits fill bytes when later output
  makes them physical;
- `region.file_align` rounds the final physical size without changing logical
  addresses.

### Virtual Output

A virtual region is an isolated output area used during assembly. Its bytes,
labels, and instructions can be inspected or transformed, but the region is
not copied into the final flat output automatically.

`virtual.begin(origin)` starts a virtual region at an explicit logical address:

```asm
virtual.begin(0x3000);

table:
emit.u32(0x11223344);
store.u32(table, load.u32(table) ^ 0x01010101);
const encoded: bytes = load.bytes(table, 4)

virtual.end();

emit.bytes(encoded);
```

The temporary region initially contains:

```text
44 33 22 11
```

After the transformation, the bytes copied into the main output are:

```text
45 32 23 10
```

The original virtual region itself contributes no file bytes.

Virtual regions can contain normal emission, reserve, alignment, labels, and
ISA instructions. `load.*` and `store.*` operate on their generated bytes just
as they do in an ordinary region. Every `virtual.begin` must have a matching
`virtual.end`.

### Using the Current Address as a Virtual Origin

When the origin argument is omitted, a virtual region begins at the current
logical address of the surrounding region:

```asm
origin(0x4000);
emit.u8(0xaa);

virtual.begin();

scratch:
emit.u16(0x1234);
const copied: bytes = load.bytes(scratch, 2)

virtual.end();

emit.bytes(copied);
```

`scratch` has logical address `0x4001`, but its virtual bytes do not replace or
advance the surrounding region. After `virtual.end`, main output resumes where
it stopped. The final file contains:

```text
aa 34 12
```

This form is useful for temporary encodings whose labels should use the same
address basis as the destination where their bytes may later be copied.

### Stable Region Facts

During ordinary emission, the real and potential cursors describe the current
state. Final region sizes are available only after layout has stabilized.

The finalizer-only queries are:

| Query | Result for the region containing an address |
|---|---|
| `region_file_offset(address)` | base file offset |
| `region_file_size(address)` | final physical size |
| `region_logical_size(address)` | final logical size |

They are intentionally separate from the live cursor queries. A finalizer can
use them to backfill a header or assert that a custom format's declared sizes
match its final layout. Chapter 12 introduces that phase and its restrictions.

### Choosing the Right Tool

Use a simple origin when:

- one flat stream needs a nonzero address basis;
- file and logical positions otherwise advance together.

Use `output.section` when:

- the next sequential area should follow real file bytes;
- a reserved tail should increase memory size without occupying file space.

Use `output.org` when:

- the next sequential area should follow the potential position;
- reserved space before later initialized data must remain in the file.

Use `region.begin` when:

- logical origin and physical file offset are both known explicitly;
- constructing a custom binary layout or an advanced format helper.

Use a virtual region when:

- data must be assembled, measured, read, or transformed temporarily;
- the temporary bytes must not enter the main output automatically.

Keep the coordinate model explicit:

- labels and `here()` describe logical addresses;
- real file cursors describe materialized bytes;
- potential cursors include the current reserved tail;
- final region facts belong to the stable finalization phase.

The next chapter explains finalizers: how to read stable layout facts, patch
existing bytes, compute checksums, and validate the completed image without
changing its layout.

## 12. Finalizers

Many binary fields describe information that is not final when the field is
first emitted:

- the size of a payload declared later;
- the file offset of another region;
- the final logical size of a reserved area;
- a checksum over encoded instructions and patched fixups;
- a count accumulated by source that follows the header.

XIRASM handles these cases with explicit finalization phases. It does not
re-run the complete source until values happen to converge.

There are two different tools:

- `late_layout` performs a restricted layout-changing step before the image is
  sealed;
- `defer` reads and patches the stable final image without changing layout.

Most user-written backfills belong in `defer`.

### Reserving a Field for Backfill

The usual pattern is:

1. emit a fixed-width placeholder;
2. write the payload normally;
3. patch the placeholder in `defer`.

```asm
size_field:
emit.u16(0);

payload:
emit.bytes(b"ABC");
payload_end:

defer {
    store.u16(size_field, payload_end - payload);
}
```

The final output is:

```text
03 00 41 42 43
```

The placeholder already occupies two bytes, so the finalizer changes only
their value. It does not insert bytes or move the payload.

Top-level finalizers may be declared before the labels they reference:

```asm
defer {
    store.u16(size_field, payload_end - payload);
}

size_field:
emit.u16(0);

payload:
emit.bytes(b"ABC");
payload_end:
```

The labels are resolved when the stable final image is available.

### Reading the Final Image

`load.u8`, `load.u16`, `load.u32`, and `load.u64` read little-endian integers
from materialized output bytes. `load.bytes(address, count)` returns a byte
range.

They can be combined with stores and assertions:

```asm
origin(0x4000);

header:
emit.u32(0);
emit.u32(0);

body:
emit.bytes(b"ABCD");
tail:
emit.bytes(b"????");
image_end:

defer {
    store.u32(header, image_end - body);
    store.u32(header + 4, body - region_base());
    store.bytes(tail, b"OK!!");

    assert(load.u32(header) == 8);
    assert(load.u32(header + 4) == 8);
    assert(load.bytes(tail, 4) == b"OK!!");
}
```

The completed bytes are:

```text
08 00 00 00 08 00 00 00 41 42 43 44 4f 4b 21 21
```

`store.bytes` accepts either a string or a `bytes` value. Integer stores reject
values that do not fit their destination width.

All loads and stores are range checked. A finalizer may access only bytes that
exist in the physical image. A logical address inside a trimmed reserved tail
is not a writable placeholder and is rejected.

### Computing a Checksum

A finalizer may declare local values, update mutable values, and use `while`:

```asm
origin(0x5000);

checksum:
emit.u16(0);

payload:
emit.bytes(b"ABCD");
payload_end:

defer {
    let cursor = payload
    let sum = 0

    while cursor < payload_end {
        sum = sum + load.u8(cursor)
        cursor = cursor + 1
    }

    store.u16(checksum, sum);
    assert(load.u16(checksum) == 266);
}
```

The checksum is `266`, or `0x010a`, so the output is:

```text
0a 01 41 42 43 44
```

Finalizer loops use the same compile-time iteration limit as ordinary Meta
loops. `for` is not currently accepted inside a finalizer; use a bounded
`while` loop for byte scans.

### Using Stable Region Facts

The final region queries introduced in Chapter 11 are available inside
`defer`:

```asm
region.begin("payload", 0x5000, 0);

file_size_field:
emit.u32(0);
logical_size_field:
emit.u32(0);

body:
emit.u8(0xaa);
reserve(3);

defer {
    store.u32(file_size_field, region_file_size(body));
    store.u32(logical_size_field, region_logical_size(body));

    assert(region_file_offset(body) == 0);
    assert(region_file_size(body) == 9);
    assert(region_logical_size(body) == 12);
}
```

The reserved tail contributes to logical size but not file size:

```text
09 00 00 00 0c 00 00 00 aa
```

These queries report facts for the region containing the supplied logical
address:

- `region_file_offset(address)` returns the region's physical base offset;
- `region_file_size(address)` returns its final materialized size;
- `region_logical_size(address)` returns its complete address-space size.

They require stable layout and therefore do not act as ordinary live cursor
queries during initial emission.

### Calling Value Functions

Value-returning Meta functions remain useful for pure final calculations:

```asm
fn align_up(value: u64, alignment: u64) -> u64 {
    return ((value + alignment - 1) / alignment) * alignment;
}

size_field:
emit.u32(0);

payload:
emit.bytes(b"ABC");
payload_end:

defer {
    store.u32(size_field, align_up(payload_end - payload, 8));
    assert(load.u32(size_field) == 8);
}
```

The final output is:

```text
08 00 00 00 41 42 43
```

The function performs only expression work. It does not emit or patch bytes
itself.

### Reusable Backfill Procedures

A procedure may register a finalizer and capture its arguments:

```asm
fn patch_u16(address: u64, value: u64) {
    defer {
        store.u16(address, value);
    }
}

field:
emit.u16(0);

patch_u16(field, 0x1234);
```

The call runs during normal source processing. Its argument values are frozen
for the deferred block, which later emits:

```text
34 12
```

This pattern is useful for small, typed backfill helpers. Scoped procedure
arguments are frozen by value. Direct top-level finalizer expressions may
remain symbolic until final evaluation.

A procedure is not called from inside `defer`; the procedure call registers
the finalizer earlier.

### Finalizer Control Flow

A `defer` body may contain:

- `const` and `let` declarations;
- assignment to local `let` values;
- `if` and `else`;
- `while`;
- `store.u8`, `store.u16`, `store.u32`, `store.u64`, and `store.bytes`;
- `assert`, `print`, `warn`, and `err`.

Expressions inside those statements may use ordinary pure operators and value
functions, labels, `load.*`, and stable region facts.

Each deferred block has its own local scope. A local declared in one block is
not visible in another.

The following operations are rejected because they would change layout:

```text
defer {
    emit.u8(0x22);
}
```

The same restriction applies to ISA instructions, labels, region changes,
alignment, reserve, nested finalizers, function declarations, and source
loading.

### Finalizer Execution Order

Deferred blocks execute in the order they were registered:

```asm
emit.u8(0);

defer {
    store.u8(0, 1);
}

defer {
    store.u8(0, load.u8(0) + 1);
}
```

The second block observes the first block's patch, producing:

```text
02
```

Use this ordering deliberately. When two finalizers modify the same bytes, a
later block sees the earlier result.

Finalizers run after:

1. normal source processing;
2. instruction encoding;
3. late-layout work;
4. fixup resolution;
5. final layout and byte materialization;
6. fixup patching.

They therefore see the exact image that will be written, including encoded
instructions and resolved references.

### Layout Work That Must Run Late

Sometimes the source must append real bytes only after the main source has
finished registering its content. That is the role of `late_layout`:

```asm
emit.u8(0x10);

late_layout {
    emit.u8(0x20);
}

late_layout {
    emit.u8(0x30);
}
```

Late-layout blocks execute once, in registration order, beginning at the end of
the default output region. The example produces:

```text
10 20 30
```

Late layout completes before fixup resolution and final layout. Its emitted
bytes participate in final layout and materialization.

### Late Layout Can Materialize a Reserved Tail

Because `late_layout` still changes layout, appended initialized bytes can turn
an earlier reserved tail into a middle gap:

```asm
emit.u8(0xaa);
reserve(3);

late_layout {
    emit.u8(0xbb);
}
```

The final output is:

```text
aa 00 00 00 bb
```

Use this behavior only when the reserved range is intended to become physical.
If the tail should remain file-free, switch to another area before registering
later initialized output.

### Combining Late Layout and Final Backfill

A deferred finalizer sees bytes appended during late layout:

```asm
size_field:
emit.u32(0);

emit.bytes(b"AB");

late_layout {
    emit.bytes(b"CD");
}

defer {
    store.u32(size_field, region_logical_size(size_field));
    assert(region_file_size(size_field) == 8);
}
```

The stable region contains the four-byte field followed by four data bytes:

```text
08 00 00 00 41 42 43 44
```

This is the intended division of responsibility:

- `late_layout` creates bytes that must participate in layout;
- `defer` observes the resulting layout and patches existing fields.

### Late-Layout Restrictions

`late_layout` is deliberately narrower than ordinary source. It accepts
layout and output API calls, diagnostics, and `if` selection. It can:

- emit integers, bytes, and structs;
- reserve, pad, and align;
- switch or align output regions;
- open and close virtual regions;
- store into existing module bytes.

It cannot declare labels, emit ISA instruction text, declare local values,
loop, define functions, load source modules, or register nested late/final
blocks.

Late layout runs once. It is not an implicit multi-pass mechanism and should
not be used to make unstable values repeatedly converge.

### Choosing the Correct Phase

Use ordinary source when the bytes can be emitted in normal source order.

Use `late_layout` when:

- real bytes or regions must be appended after the main source;
- those bytes must affect final offsets and sizes;
- a restricted, one-time late layout step is sufficient.

Use `defer` when:

- a fixed-width placeholder needs a final value;
- a checksum or validation needs the completed byte image;
- final region sizes or offsets are required;
- existing bytes must be patched without changing layout.

Never use `defer` to create missing space. Reserve or emit the required storage
before finalization, then patch only that existing range.

The next chapter begins Part III with flat and custom binaries: combining
labels, structs, regions, late layout, and finalizers into complete file
formats.

## Part III: Building Programs

## 13. Flat and Custom Binaries

XIRASM writes a flat binary by default. The output file contains exactly the
materialized bytes produced by instructions, data declarations, padding, and
output regions. It does not receive an operating-system header unless the
source explicitly creates one.

This direct model is useful for:

- boot sectors and firmware images;
- ROM tables and embedded resources;
- protocol messages and test vectors;
- compact asset containers;
- project-specific binary formats;
- small executable code fragments loaded by another program.

A flat binary can still have a rich internal structure. Labels describe
logical positions, structs describe records, regions separate logical and
physical coordinates, and finalizers derive fields that depend on the completed
image.

### Writing Raw Machine Code

Natural ISA text can be the complete contents of a flat file:

```asm
x86.use64();

entry:
    mov eax, 42
    ret
```

The output is:

```text
b8 2a 00 00 00 c3
```

There is no executable header, entry-point field, or loader metadata. `entry`
is an assembler label, not an operating-system entry point. A program that
loads these bytes must already know the target ISA, instruction mode, load
address, and calling convention.

### Combining a Header and Payload

Data APIs can build a small file directly:

```asm
emit.bytes(b"RAW1");
emit.u16(1);
emit.u16(0);

payload:
emit.bytes(b"ABC");
```

The output is:

```text
52 41 57 31 01 00 00 00 41 42 43
```

The four-byte signature identifies the file, the first `u16` stores a version,
the second `u16` is currently reserved, and the remaining bytes form the
payload.

No special file-format declaration is required. The source order is the file
order.

### Describing Records with Structs

Use a packed struct when a binary record has named fields but no implicit
alignment gaps:

```asm
packed struct ChunkHeader {
    kind: u16
    size: u16
}

emit.struct(ChunkHeader {
    kind: 1,
    size: 3,
});
emit.bytes(b"ABC");
```

The output is:

```text
01 00 03 00 41 42 43
```

The struct keeps field names and widths visible while `emit.struct` writes its
packed representation. Use a natural struct only when the file format itself
requires the same alignment and trailing padding.

For repeated records, place the emission logic in a procedure:

```asm
fn emit_record(tag: u8, value: u32) {
    emit.u8(tag);
    emit.u32(value);
}

emit_record(1, 0x11223344);
emit_record(2, 0xaabbccdd);
```

The records are emitted consecutively:

```text
01 44 33 22 11 02 dd cc bb aa
```

Procedures help keep a custom writer consistent, but the resulting file is
still determined by ordinary source order.

### Deriving Header Fields

Avoid manually calculating offsets, sizes, and checksums. Emit fixed-width
fields, label the relevant data, and derive the final values in `defer`:

```asm
origin(0);

magic:
emit.bytes(b"XIF1");
size_field:
emit.u32(0);
payload_offset_field:
emit.u32(0);
checksum_field:
emit.u32(0);

payload:
emit.bytes(b"OK!!");
payload_end:

defer {
    store.u32(size_field, payload_end - magic);
    store.u32(payload_offset_field, payload - magic);
    store.u32(checksum_field, load.u8(payload) + load.u8(payload + 1));

    assert(load.bytes(magic, 4) == b"XIF1");
    assert(load.u32(size_field) == 20);
    assert(load.u32(payload_offset_field) == 16);
    assert(load.u32(checksum_field) == 0x9a);
}
```

The completed file is:

```text
58 49 46 31 14 00 00 00 10 00 00 00 9a 00 00 00 4f 4b 21 21
```

The source does not hardcode the total size or payload offset. If the header or
payload changes, the finalizer writes the new values into the existing fields.
This example uses one compact region beginning at origin zero, so the logical
distance from `magic` is also the file-relative payload offset.

Keep the coordinate being stored explicit:

- subtract labels when the format wants a logical distance;
- use `file_offset()` while emitting when the current physical cursor is
  required;
- use `region_file_offset(address)` in `defer` when the final physical base of
  a region is required;
- use `region_file_size(address)` and `region_logical_size(address)` for stable
  final sizes.

### Separating Logical Addresses from File Offsets

A file may store bytes consecutively while assigning them unrelated logical
addresses:

```asm
region.begin("header", 0x1000, 0);
emit.bytes(b"HDR0");

output.section("payload", 0x2000);
payload:
emit.bytes(b"DATA");

defer {
    assert(payload == 0x2000);
    assert(region_file_offset(payload) == 4);
}
```

The physical file remains compact:

```text
48 44 52 30 44 41 54 41
```

The payload begins at file offset `4`, but its logical address is `0x2000`.
This distinction is common in firmware, ROM, memory images, and formats that
describe where data will be loaded.

Do not derive a file offset from a logical address unless the format explicitly
defines that relationship. Query or record each coordinate independently.

### Representing Initialized Gaps and File-Free Tails

Reserve behaves differently depending on whether initialized data follows it.
An internal gap becomes physical zero bytes, while a continuous reserved tail
can remain absent from the file:

```asm
region.begin("image", 0x5000, 0);

emit.bytes(b"HDR0");
file_size_field:
emit.u32(0);
logical_size_field:
emit.u32(0);

emit.u8(0xaa);
reserve(3);
emit.u8(0xee);
reserve(8);

defer {
    store.u32(file_size_field, region_file_size(file_size_field));
    store.u32(logical_size_field, region_logical_size(logical_size_field));

    assert(load.u32(file_size_field) == 17);
    assert(load.u32(logical_size_field) == 25);
}
```

The physical file is only 17 bytes:

```text
48 44 52 30 11 00 00 00 19 00 00 00 aa 00 00 00 ee
```

The three-byte internal reserve is materialized because initialized data
follows it. The final eight-byte reserve increases logical size to 25 without
adding bytes to the file.

This is useful for zero-initialized storage. The file records the initialized
prefix, while the loader or consuming program provides the remaining logical
space.

### Appending a Trailer During Late Layout

Use `late_layout` when a real trailer or table must be appended after ordinary
source processing:

```asm
total_size:
emit.u32(0);
emit.bytes(b"DATA");

late_layout {
    emit.bytes(b"END!");
}

defer {
    store.u32(total_size, region_file_size(total_size));
    assert(load.u32(total_size) == 12);
}
```

The output is:

```text
0c 00 00 00 44 41 54 41 45 4e 44 21
```

The trailer participates in final layout. The deferred size field observes the
completed image and patches the already allocated header field.

Use late layout only when the appended bytes genuinely must be created after
the main source. Ordinary source order is clearer when it can express the same
layout.

### Validating a Completed File

A custom writer should assert the invariants that make its output readable:

- magic bytes and version fields have the expected values;
- offsets point inside the intended region;
- counts match the number of emitted records;
- stored sizes match final region facts;
- checksums are computed over the intended byte range;
- logical and physical sizes follow the format's rules.

Assertions inside `defer` validate the exact bytes that will be written,
including encoded instructions and resolved fixups. A failed invariant stops
assembly instead of producing a file that only appears valid.

Keep validation close to the field it protects. A size backfill and its
corresponding assertion usually belong in the same finalizer.

### Organizing a Custom Writer

For a small format, a clear source order is usually:

1. declare record types and constants;
2. emit the fixed header and placeholders;
3. emit ordinary payload regions;
4. append genuinely late tables or trailers;
5. backfill stable fields and assert invariants.

Reusable includes can package record declarations and emission procedures.
Keep format state explicit in arguments and values rather than scattering
unrelated hardcoded offsets through the source.

As a custom format grows, continue to preserve the same separation:

- source and procedures decide what records exist;
- labels and regions describe layout;
- late layout creates bytes that must participate in final layout;
- finalizers derive and validate stable fields.

### When to Use a Format Facade

Do not hand-build a standardized executable or object format merely because a
flat writer can express its bytes. Such formats also require coordinated
headers, tables, permissions, imports, exports, relocations, and loader rules.

XIRASM provides user-facing format facades for those tasks. The Language Guide
introduces only their role; the separate
[Format Tutorial](format-tutorial.md) explains the ordinary facade. Direct
construction belongs in the [Advanced Format Construction Guide](advanced-formats.md).

The next chapter gives a short language-level introduction to selecting and
using a format facade without duplicating the complete executable-format
guide.

## 14. Executable and Object Formats

Standard executable and object formats contain more than a sequence of
instructions and data. They coordinate headers, section or segment tables,
permissions, entry points, imports, exports, symbols, and relocations according
to rules expected by linkers and operating-system loaders.

XIRASM provides a user-facing format facade for these files:

```asm
import("format/format.inc");
```

The facade lets source describe the intended image instead of manually writing
table counts, row positions, file offsets, virtual addresses, and header
fields.

This chapter explains the common language pattern. Complete format options,
imports, exports, relocations, shared libraries, object files, and advanced
helpers belong in the [Format Tutorial](format-tutorial.md).

### Declaring the Image Before Writing It

A facade program begins by declaring the sections or segments that the image
will contain. Each descriptor gives a name, purpose, and permission set.

The descriptor list is significant:

- its length determines the required table count;
- its order determines the format table order;
- its names identify the content blocks written later;
- its purposes select format-specific behavior;
- its permissions become section or segment attributes.

The source does not pass a separate count or row number. Adding or removing a
descriptor updates the generated format structure automatically.

### A Minimal ELF64 Executable

The following program creates an x86-64 ELF executable with one loadable,
readable, executable segment:

```asm
import("format/format.inc");
x86.use64();

let image: map = format_elf64(
    format_elf_exec,
    list.of(
        format_segment(
            ".text",
            format_load | format_readable | format_executable
        )
    )
)
format_begin(image);

format_segment_begin(image, ".text");
start:
    mov eax, 60
    xor edi, edi
    syscall
format_segment_end(image, ".text");

format_entry_mut(image, start)
format_finish(image);
```

On x86-64 Linux, the generated program exits with status zero.

Read the source in five steps:

1. `format_elf64` creates a declarative ELF64 plan.
2. `format_segment` declares one named segment and its permissions.
3. `format_begin` emits the structures determined by the plan.
4. `format_segment_begin` and `format_segment_end` surround the segment's
   actual instructions and data.
5. `format_entry_mut` binds the entry label, and `format_finish` completes the
   image.

The source never supplies a program-header count, table row, file offset, load
address, or entry-point field offset. The facade derives them from the plan and
the completed segment layout.

### Sections and Segments

The ordinary lifecycle uses the vocabulary of the selected format:

- PE images and object formats primarily expose named sections;
- ELF executables and shared objects expose loadable segments;
- ELF object files expose sections rather than runtime load mappings.

The corresponding lifecycle calls use those names:

```text
format_section_begin(image, name)
format_section_end(image, name)

format_segment_begin(image, name)
format_segment_end(image, name)
```

Open only descriptors declared in the plan, and use each name for the purpose
assigned to it. The facade can then associate emitted bytes, logical sizes,
permissions, and generated tables with the correct format entry.

### Format Families

The ordinary facade includes constructor families for the supported image
classes:

| Image class | Ordinary constructor family |
| --- | --- |
| PE32 and PE64 images | `format_pe32`, `format_pe64` |
| COFF32 and COFF64 objects | `format_coff32`, `format_coff64` |
| ELF32 and ELF64 executables | `format_elf32`, `format_elf64` |
| ELF32 and ELF64 objects | `format_elfobj32`, `format_elfobj64` |
| ELF64 shared objects | `format_elf64_so` |

Constructor options distinguish image properties such as executable or DLL
mode, console or GUI subsystem, position-independent execution, NX policy, and
address randomization. Section and segment descriptors carry content purposes
and read, write, execute, or discardable permissions.

The complete option and descriptor lists are reference material and are not
duplicated in this Language Guide.

### Binding an Entry Point

Executables bind an entry label after its source has defined the intended
starting code:

```text
format_entry_mut(image, start)
format_finish(image);
```

`format_entry_mut` updates the direct `let` binding passed as its first
argument. `format_finish` then validates that the executable has the required
entry information.

Object files and some library forms do not use an executable entry point.
Follow the lifecycle of the selected format rather than adding an artificial
entry label.

### Generated Format Content

Some format content is generated from higher-level declarations rather than
written as ordinary payload bytes. Examples include:

- import and export tables;
- symbol and string tables;
- relocation records;
- base-relocation sections;
- dynamic metadata;
- resource directories.

The facade APIs for those features accept names, labels, permissions, and
relocation kinds. They derive section numbers, symbol indexes, table offsets,
rows, and counts internally.

Do not replace those declarations with manually calculated table positions.
The purpose of the facade is to keep format bookkeeping tied to the actual
layout.

### Ordinary and Advanced Format APIs

Use `format/format.inc` for normal programs. It is the stable user layer that
coordinates format attributes, descriptors, named content blocks, entries,
and generated tables.

Format-specific includes provide lower-level building blocks. Some expose
width-specific convenience wrappers, while others expose direct header,
section, segment, or table helpers. These APIs are useful when implementing a
new facade or a specialized layout that the ordinary layer cannot express.

They are not required merely to produce a multi-section executable, DLL,
object file, shared object, import table, export table, or relocation table.
Prefer the ordinary facade whenever it supports the intended image.

Avoid mixing abstraction levels casually. A low-level helper may require the
caller to manage counts, rows, offsets, or format invariants that the ordinary
facade normally owns.

### Where to Continue

Use the [Format Tutorial](format-tutorial.md) for complete, runnable examples
covering:

- PE32 and PE64 executables and DLLs;
- COFF32 and COFF64 objects;
- ELF32 and ELF64 executables and position-independent executables;
- ELF32 and ELF64 objects;
- ELF64 shared objects;
- multiple sections and segments;
- initialized and uninitialized data;
- imports, exports, symbols, and relocations;
- the boundary between the ordinary facade and advanced low-level helpers.

Use the API Reference when the required format family and workflow are already
known and only an exact signature, flag, or constant is needed.

The final chapter covers diagnostics and practical source conventions for
maintainable XIRASM programs.

## 15. Diagnostics and Practical Conventions

Assembler errors are easiest to fix when the source states its assumptions
explicitly. XIRASM provides diagnostics for informational output, recoverable
warnings, deliberate failures, and executable invariants.

The same principle applies to source organization. Clear names, explicit
coordinates, small helpers, and the correct abstraction layer make binary
layout easier to review and safer to change.

### Reading a Diagnostic

Diagnostics identify the source location, severity, and message:

```text
source.asm:12:5: error: header size must be four
```

The location points to the statement that reported or triggered the problem.
Errors from parsing, Meta evaluation, instruction encoding, layout, fixup
resolution, and finalization use the same source-oriented reporting model.

Begin with the first error. A later failure may be a consequence of an earlier
invalid declaration, missing label, or rejected instruction.

### Notes and Warnings

`print` emits an informational note. `warn` emits a non-fatal warning:

```asm
print("image origin", here());
warn("using default alignment", 16);
assert(true, "configuration must be valid");
emit.u8(0x7d);
```

The source assembles successfully and emits one byte:

```text
7d
```

The diagnostics include the values passed after the message:

```text
note: image origin 0
warning: using default alignment 16
```

Use `print` for intentional assembly-time information, not permanent tracing
noise. Use `warn` when the output remains valid but the selected configuration
deserves attention.

Warnings do not change the output or make assembly fail. If the condition would
produce an invalid file or unsupported program, report an error instead.

### Deliberate Errors

`err` emits a fatal diagnostic and stops assembly:

```text
if target.bits != 64 {
    err("this source requires a 64-bit target", target.bits);
}
```

Additional arguments are formatted after the message. Include the value that
made the configuration invalid when it helps the user correct the source.

An error message should explain the violated requirement:

```text
err("section alignment must be a nonzero power of two", alignment);
```

Avoid messages such as `invalid value` when the source can name the expected
property.

### Assertions

`assert` is the shortest way to encode an invariant:

```asm
packed struct Header {
    kind: u16
    size: u16
}

assert(sizeof(Header) == 4, "Header must remain four bytes");

emit.struct(Header {
    kind: 1,
    size: 3,
});
emit.bytes(b"ABC");
```

The assertion produces no output when it succeeds. The completed bytes are:

```text
01 00 03 00 41 42 43
```

If the condition is false, assembly stops and the supplied message becomes the
error text.

Use ordinary assertions for facts known during source processing:

- type sizes and field offsets;
- option combinations;
- list and map contents;
- target requirements;
- constant ranges;
- relationships between declared values.

Use assertions inside `defer` for facts that require the stable final image:

- final file and logical sizes;
- resolved offsets and addresses;
- patched header fields;
- checksums;
- generated table contents;
- bytes produced by instruction fixups.

Keeping an assertion in the phase where its inputs become stable prevents
accidental dependence on temporary layout values.

### Guarding Target-Specific Source

Make target requirements visible near the code that depends on them:

```asm
x86.use64();

if target.bits != 64 {
    err("this routine requires x86-64");
}

entry:
    xor eax, eax
    ret
```

The generated bytes are:

```text
31 c0 c3
```

Target conditions run during assembly. They select or reject source; they do
not emit a runtime check.

Explicit mode selection also makes reusable source easier to review. A reader
should not need to infer whether an instruction was intended for x86-16,
x86-32, x86-64, RISC-V 32, or RISC-V 64.

### Prefer Names Over Magic Numbers

Name values that carry format, protocol, or layout meaning:

```text
const header_size: u32 = 16
const page_alignment: u64 = 0x1000
const record_kind_code: u16 = 3
```

Numeric instruction operands that are part of an algorithm may remain local.
Offsets, flags, structure sizes, format values, and alignment policies usually
deserve names.

Use named constants when a value represents one choice from a closed set. Use
structs when several fields form one record. Use maps or lists when the source
is building a dynamic plan.

### Name Coordinates Explicitly

Binary writers commonly work with several coordinate systems at once. Make the
coordinate visible in names:

```text
header_foa
text_rva
entry_address
payload_file_size
payload_logical_size
```

Do not use a generic name such as `offset` when it may mean a file offset,
region-relative offset, virtual address, or structure field offset.

The same rule applies to helper parameters. A function that accepts
`logical_address` and `file_offset` is easier to use correctly than one that
accepts `start` and `position`.

### Derive Layout Facts

Prefer labels and layout queries over manually maintained constants:

- subtract labels for logical distances;
- use `sizeof` and `offset_of` for aggregate layout;
- use `file_offset()` for the live physical cursor;
- use stable region facts in `defer`;
- let format descriptor lists determine table counts and order;
- let symbol and relocation declarations determine generated indexes.

Hardcoded offsets are appropriate only when the external format specification
requires that exact constant. Even then, give the constant a descriptive name
and assert the surrounding layout when possible.

### Keep Finalization Focused

Ordinary source should emit bytes in ordinary order whenever possible.

Use `late_layout` only for real bytes or regions that must be appended after
the main source. Use `defer` only to inspect and patch the stable image.

Do not move normal emission into a late phase merely to avoid organizing the
source. A small explicit header, payload, trailer, and finalizer sequence is
easier to understand than a network of deferred side effects.

Keep each finalizer focused on related fields. A block that patches one header
and verifies its values is easier to review than one block that mutates
unrelated regions throughout the file.

### Organize Modules by Responsibility

A practical project layout separates:

- shared constants and data types;
- reusable emission procedures;
- target-specific instruction routines;
- format or container construction;
- generated data inputs;
- the top-level source that selects options and assembles the final image.

Use `import` for modules that should initialize once. Use `include` only when
repeated textual evaluation is intentional.

Keep public helpers small and explicit about their inputs. Avoid relying on
hidden mutable state when a plan, list, map, label, or option value can be
passed directly.

### Choose the Correct Format Layer

For standardized executable and object formats, begin with:

```asm
import("format/format.inc");
```

The ordinary facade owns descriptor counts, table order, generated indexes,
offsets, and common format invariants.

Use lower-level format includes only when implementing a new facade or a layout
that the ordinary facade cannot represent. Do not switch layers merely because
the advanced API exposes more numbers. Those numbers often represent
bookkeeping that the ordinary layer should continue to own.

For project-specific files, use the flat and custom binary techniques from
Chapter 13 rather than forcing the data into an operating-system format.

### Validate at the Boundary

Validate facts where XIRASM hands output to another system:

- assert the final structure before writing a custom file;
- declare executable permissions and relocation policy explicitly;
- verify imported data before emitting it;
- reject unsupported target combinations early;
- keep entry points and exported symbols named;
- retain a small runnable example for every reusable public include.

Validation should describe the contract, not duplicate implementation details.
An assertion that a table count matches a declaration list is useful. An
assertion that repeats every internal arithmetic step is harder to maintain.

### Finding the Right Documentation

Use this Language Guide to understand:

- how compile-time source and ISA text work together;
- how values, control flow, functions, collections, and tokens behave;
- how labels, data, regions, virtual output, and finalizers shape a file;
- how to choose between flat output and a format facade.

Use the API Reference for:

- exact function signatures;
- accepted value types;
- constants and option flags;
- concise behavior and error constraints.

Use the Format Tutorial for:

- complete PE, COFF, and ELF programs;
- executable, DLL, object, PIE, and shared-object workflows;
- imports, exports, symbols, resources, and relocations;
- multi-section and multi-segment layouts;
- the ordinary facade and advanced format-helper boundary.

Together, these documents separate concepts, lookup material, and
format-specific workflows so each can remain readable.

### Final Checklist

Before treating a source file as complete, check that:

- the target and instruction mode are explicit;
- constants and coordinates have descriptive names;
- labels replace manually calculated addresses;
- struct layout is asserted when external consumers depend on it;
- imports and includes use the intended evaluation model;
- reserved tails and physical gaps are intentional;
- late layout creates only genuinely late bytes;
- finalizers patch existing storage and validate stable facts;
- standard formats use the ordinary facade unless a real advanced requirement
  exists;
- fatal conditions produce actionable messages;
- the final output has a small reproducible assembly or execution check.

These practices keep XIRASM source close to its purpose: readable assembly,
explicit compile-time logic, and verifiable binary layout.
