# Chapter 14: Inline Assembly

Some instructions have no Loup equivalent: `cli` and `sti`, `rdmsr`/`wrmsr`, `lidt`, reading `cr2`, `mrs`/`msr` on Arm, `csrr`/`csrw` on RISC-V, the barrier instructions, the first few instructions after reset. For those, freestanding code uses `asm { }` blocks (Language Reference §3-3.7). This chapter covers the block's grammar, operands and clobbers, which modes accept it, what the 1.25 checker verifies about it and what it leaves to you, and examples for x86_64, AArch64 and RISC-V.

## The basic form

An `asm { }` block holds one or more string literals — the instructions, one per string — optionally followed by operand lists. With no operands:

```loup
@mode(freestanding)

func disableInterrupts(): void { asm { "cli" } }
func enableInterrupts(): void { asm { "sti" } }
func cpuRelax(): void { asm { "pause" } }

# Park the CPU until the next interrupt, forever.
func idle(): void {
    while (true) {
        asm { "hlt" }
    }
}
```

*Listing 14-1: One-instruction wrappers*

The instructions are passed to the target's assembler as written; `loup check` does not assemble them, so a typo in an instruction shows up only when you build for the target. Several strings form a sequence, and numeric local labels (`1:` … `jmp 1b`) work as in GNU assembler syntax. On x86_64 the syntax is Intel (`mov rax, cr2`, destination first).

## Operands — `out:`, `in:`, `clobbers:`

After the instruction strings come up to three lists, in this order:

- `out: name = "constraint", …` — binds a Loup variable to an output register;
- `in: "constraint" = expression, …` — loads a Loup value into a register before the instructions run;
- `clobbers: ["…", …]` — names registers and `"memory"` that the instructions change without listing them as outputs.

```loup
#~ @mode(freestanding)
#~
func readMsr(msr: uint32): uint64 {
    var lo: uint32 = 0
    var hi: uint32 = 0
    asm {
        "rdmsr"
        out: lo = "=a", hi = "=d"
        in: "ecx" = msr
        clobbers: []
    }
    return ((hi as uint64) << 32) | (lo as uint64)
}

func writeMsr(msr: uint32, value: uint64): void {
    let lo = (value & 0xFFFF_FFFFu64) as uint32
    let hi = (value >> 32) as uint32
    asm {
        "wrmsr"
        in: "ecx" = msr, "eax" = lo, "edx" = hi
        clobbers: ["memory"]
    }
}

# Port I/O: the 16550 UART on a PC is at I/O port 0x3F8.
func outb(port: uint16, value: uint8): void {
    asm { "out dx, al" in: "dx" = port, "al" = value clobbers: [] }
}

func inb(port: uint16): uint8 {
    var value: uint64 = 0
    asm { "in al, dx" out: value = "=a" in: "dx" = port clobbers: [] }
    return (value & 0xFFu64) as uint8
}
```

*Listing 14-2: Model-specific registers and port I/O*

Constraint strings follow the GCC/LLVM conventions: `"=a"` is "output in `rax`/`eax`/`al`", `"=d"` the `rdx` family, `"r"` any general register, and a constraint may simply name a register — `"ecx"`, `"dx"`, `"al"` on x86, `"x0"` on AArch64, `"a0"` on RISC-V. Output variables must be `var`s. Clobber `"memory"` whenever the instructions read or write memory the compiler cannot see (a TLB flush, a control-register write, a barrier), so it does not keep values cached in registers across the block.

The Language Reference also defines `"all"` (every caller-saved register) and per-architecture presets such as `"msr-read"` and `"cpuid"` for the clobber list; the checker accepts `clobbers: ["all"]`.

### Two syntax habits that do not carry over

The operand lists live **inside** the braces, after the instructions. GCC's parenthesized, colon-separated form does not parse:

```loup,does_not_compile
@mode(freestanding)

func readMsr(msr: uint32): uint64 {
    var lo: uint32 = 0
    var hi: uint32 = 0
    asm("rdmsr" : "=a"(lo), "=d"(hi) : "c"(msr))
    return ((hi as uint64) << 32) | (lo as uint64)
}
```

```text
error[E0001]: expected RParen, found Colon
 --> src/main.lpfree:6:17
  |
6 |     asm("rdmsr" : "=a"(lo), "=d"(hi) : "c"(msr))
  |                 ^
  |

error[E0003]: expected `;`
 --> src/main.lpfree:6:17
  |
6 |     asm("rdmsr" : "=a"(lo), "=d"(hi) : "c"(msr))
  |                 ^
  |
```

And the instructions come first. Putting the operands before them — as in Rust's `asm!`, where operands follow the template but are separated by commas — fails at the first operand:

```loup,does_not_compile
@mode(freestanding)

func readCr2(): uint64 {
    var faultAddr: uint64 = 0
    asm {
        out: faultAddr = "=a"
        "mov rax, cr2"
    }
    return faultAddr
}
```

```text
error[E0001]: asm block requires at least one template string literal
 --> src/main.lpfree:6:9
  |
6 |         out: faultAddr = "=a"
  |         ^^^
  |

error[E0001]: expected RBrace, found StringLiteral { quote: 34, is_raw: false }
 --> src/main.lpfree:7:9
  |
7 |         "mov rax, cr2"
  |         ^^^^^^^^^^^^^^
  |

error[E0001]: expected expression
  --> src/main.lpfree:10:1
   |
10 | }
   | ^
   |
```

The fixed version:

```loup
#~ @mode(freestanding)
#~
# The faulting address of the last page fault.
func readCr2(): uint64 {
    var faultAddr: uint64 = 0
    asm {
        "mov rax, cr2"
        out: faultAddr = "=a"
        clobbers: []
    }
    return faultAddr
}
```

*Listing 14-3: Instructions first, then operands*

## Which modes accept `asm`

`asm { }` is compiled to machine code, so it exists only in the modes that produce machine code: `freestanding`, `host.native` (where the Language Reference requires an `@unsafe("asm: …")` acknowledgment on the enclosing function), and `web.compiled` (a WebAssembly form). In the interpreted and dual modes it is rejected. A helper written for a hosted tool fails like this:

```loup,does_not_compile
@mode(host.dual)

func disableInterrupts(): void {
    asm { "cli" }
}

func main(): void {
    disableInterrupts()
}
```

```text
main.loup:4:5: error[E_ASM_NOT_COMPILED]: E_ASM_NOT_COMPILED: `asm { }` is only valid in a compiled mode (`web.compiled`, `host.native`, `freestanding`); the module mode is `host.dual` (§3-3.7)
```

(This listing is a `host.dual` program, so the message comes from `loup run`.) Keep hardware access in `.lpfree` files; a hosted tool that needs the same logic needs its own copy without the `asm`.

In freestanding, an `asm` block does **not** need `@unsafe`: inline assembly is too common in kernel code for every block to be an audit site. The Language Reference describes an `asm-unchecked` advisory per function instead; the 1.25 toolchain does not print it.

## What the checker does not verify

`loup check` parses the block, resolves the operand variables and expressions, and stops there. In 1.25 it does not check:

- **Operand widths.** The Language Reference makes a mismatch between an operand's type and its constraint `E_ASM_OPERAND_TYPE`. It is not implemented: this compiles, although `rdtsc` writes 32 bits to `eax` and the variable is a `uint8`.

  ```loup,not_desired_behavior
  #~ @mode(freestanding)
  #~
  func cycleCounterLow(): uint8 {
      var low: uint8 = 0
      asm { "rdtsc" out: low = "=a" clobbers: ["rdx"] }
      return low
  }
  ```

  *Listing 14-4: A width mismatch the checker accepts*

  Give each output the width of the register it receives (`uint32` for `eax`, `uint64` for `rax`), as Listing 14-2 does, and narrow afterwards with `as`.
- **Mutability of outputs.** An output bound to a `let` is accepted. Use `var`.
- **The instructions themselves**, and whether the constraints exist on the target. Those errors come from the assembler at build time.
- **Expired borrows used as operands** (`E_ASM_EXPIRED_BORROW` is specified, not raised).

## Borrowed values as operands

A struct passed by `borrow` can be an input operand; the register receives its address. That is the natural way to hand a descriptor-table pointer to `lidt`:

```loup
#~ @mode(freestanding)
#~
# The 10-byte operand of lidt/lgdt: a 16-bit limit and a 64-bit base.
@repr(packed)
struct DescriptorTablePointer {
    var limit: uint16;
    var base: uint64;
}

func loadIdt(borrow idtr: DescriptorTablePointer): void {
    asm {
        "lidt [rdi]"
        in: "rdi" = idtr
        clobbers: ["memory"]
    }
}
```

*Listing 14-5: Passing a borrowed struct's address to `lidt`*

## When to use `asm` — and when not to

Inline assembly is hard to review and invisible to the checker, so use the Loup primitive where one exists:

- **Memory-mapped registers:** `Volatile[T]` or a `volatile borrow` parameter (chapter 15).
- **Atomic operations and memory ordering:** the atomic intrinsics `atomicFetchAddU64`, `atomicCompareExchangeU64`, `atomicFence` and friends (chapter 19), not `lock xadd`.
- **Disabling interrupts around a critical section:** `InterruptGuard` or `irqSaveDisable`/`irqRestore`, which restore the previous state correctly on every architecture (chapter 16).
- **Layout arithmetic:** `size_of`, `align_of`, `addressOf`.

Use `asm` for privileged and system instructions (`lgdt`, `lidt`, `invlpg`, `wrmsr`, `cpuid`), control and system registers (`cr0`–`cr4`, `msr`/`mrs`, `csrr`/`csrw`), I/O ports on x86, explicit hardware barriers (`mfence`, `dsb`, `isb`, `fence`), and the bodies of `@naked` functions — boot entries, vector tables and context switches (chapter 16).

## Architecture examples

The constraint and register spellings below are the ones the QEMU-booted test kernels in the toolchain repository use.

### x86_64

```loup
#~ @mode(freestanding)
#~
# The timestamp counter.
func rdtsc(): uint64 {
    var lo: uint32 = 0
    var hi: uint32 = 0
    asm { "rdtsc" out: lo = "=a", hi = "=d" clobbers: [] }
    return ((hi as uint64) << 32) | (lo as uint64)
}

# Load a new page-table root; this also flushes non-global TLB entries.
func loadCr3(root: uint64): void {
    asm { "mov cr3, rax" in: "rax" = root clobbers: ["memory"] }
}

# Invalidate one page's TLB entry.
func invlpg(virt: uint64): void {
    asm { "invlpg [rax]" in: "rax" = virt clobbers: ["memory"] }
}
```

*Listing 14-6: x86_64 helpers*

### AArch64

```loup
#~ @mode(freestanding)
#~
# The counter frequency and the virtual counter.
func counterFrequency(): uint64 {
    var f: uint64 = 0
    asm { "mrs x0, cntfrq_el0" out: f = "=x0" clobbers: [] }
    return f
}

func virtualCount(): uint64 {
    var v: uint64 = 0
    asm { "mrs x0, cntvct_el0" out: v = "=x0" clobbers: [] }
    return v
}

# A 32-bit device-register store and the barriers around it.
func write32(addr: uint64, value: uint64): void {
    asm { "str w1, [x0]" in: "x0" = addr, "x1" = value clobbers: ["memory"] }
}

func dataBarrier(): void { asm { "dsb sy" clobbers: ["memory"] } }
func instructionBarrier(): void { asm { "isb" clobbers: [] } }

# Mask IRQs (DAIF.I).
func maskIrq(): void { asm { "msr daifset, #2" clobbers: ["memory"] } }
```

*Listing 14-7: AArch64 helpers*

### RISC-V (riscv64)

```loup
#~ @mode(freestanding)
#~
# A 64-bit load from a device or memory address.
func load64(addr: uint64): uint64 {
    var v: uint64 = 0
    asm { "ld a0, 0(a0)" out: v = "=a0" in: "a0" = addr clobbers: ["memory"] }
    return v
}

# Machine-mode interrupt enable/disable (mstatus.MIE, bit 3).
func enableInterrupts(): void { asm { "csrsi mstatus, 8" clobbers: ["memory"] } }
func disableInterrupts(): void { asm { "csrci mstatus, 8" clobbers: ["memory"] } }

# Wait for an interrupt.
func waitForInterrupt(): void { asm { "wfi" clobbers: [] } }
```

*Listing 14-8: RISC-V helpers*

## Summary

- `asm { "instr" … out: … in: … clobbers: [...] }` — instructions first, then outputs, inputs and clobbers, all inside the braces. GCC's `asm("…" : …)` form does not parse.
- Constraints follow GCC/LLVM conventions and may name registers directly; clobber `"memory"` around anything with memory effects.
- `asm` is valid in `freestanding`, `host.native` and `web.compiled`; elsewhere it is `E_ASM_NOT_COMPILED`. In freestanding it needs no `@unsafe`.
- The 1.25 checker does not verify operand widths, output mutability or the instructions; match register widths yourself.
- A `borrow` struct operand passes its address.
- Prefer `Volatile`, the atomic intrinsics, `InterruptGuard` and the layout queries where they fit; keep `asm` for system instructions, ports, barriers and naked functions.

Next: MMIO with `Volatile`.
