Inline Assembly

Part of: Reef Language Reference Last reviewed on version: 0.8.0


Reef supports inline assembly for low-level hardware access, performance-critical code, and OS/embedded development. Assembly functions are defined at the module level and generate native machine code for the target architecture.

Syntax

Assembly Procedure (No Return Value)

asm proc name(param1: type1, param2: type2) for architecture
    // Assembly instructions
end name

Assembly Function (With Return Value)

asm fn name(param1: type1, param2: type2): return_type for architecture
    // Assembly instructions
    MOV result, value  // Use 'result' for return value
end name

Supported Architectures

Architecture Syntax Style Target Flag
amd64 Intel syntax --target amd64 or --target amd64-baremetal
arm64 ARM syntax --target arm64 or --target arm64-baremetal
riscv64 RISC-V syntax --target riscv64 or --target riscv64-baremetal

Basic Examples

x86-64 (AMD64)

// Halt CPU - wait for interrupt
asm proc hlt() for amd64
    HLT
end hlt

// Read from I/O port
asm fn inb(port: int): int for amd64
    MOV EDX, port
    XOR EAX, EAX
    IN AL, DX
    MOV result, EAX
end inb

// Write to I/O port
asm proc outb(port: int, value: int) for amd64
    MOV EDX, port
    MOV EAX, value
    OUT DX, AL
end outb

// Atomic compare-and-swap
asm fn cas(ptr: pointer, old_val: int, new_val: int): int for amd64
    MOV RAX, old_val
    MOV RCX, new_val
    MOV RDX, ptr
    LOCK CMPXCHG [RDX], ECX
    MOV result, EAX
end cas

ARM64 (AArch64)

// Wait for interrupt
asm proc wfi() for arm64
    WFI
end wfi

// Data memory barrier
asm proc dmb() for arm64
    DMB SY
end dmb

// Read system register (CNTPCT_EL0 - timer)
asm fn read_timer(): int for arm64
    MRS X0, CNTPCT_EL0
    MOV result, X0
end read_timer

// Load-exclusive for atomics
asm fn ldxr(ptr: pointer): int for arm64
    LDXR W0, [X0]
    MOV result, W0
end ldxr

RISC-V 64-bit

// Full memory fence
asm proc memory_barrier() for riscv64
    fence iorw, iorw
end memory_barrier

// No-op for spin-wait loops
asm proc cpu_pause() for riscv64
    nop
end cpu_pause

// Add two values
asm fn add_native(a: int, b: int): int for riscv64
    add a0, a0, a1
    mv result, a0
end add_native

// Read cycle counter (CSR)
asm fn read_cycle(): int for riscv64
    rdcycle a0
    mv result, a0
end read_cycle

Parameter Access

Parameters are accessed by name within assembly code:

asm fn add_values(a: int, b: int): int for amd64
    MOV EAX, a      // Load parameter 'a'
    ADD EAX, b      // Add parameter 'b'
    MOV result, EAX // Store in 'result'
end add_values

The compiler substitutes parameter names with appropriate register/memory operands.

Return Values

For asm fn (functions with return values):

  • Use result as the destination for the return value
  • The compiler handles moving result to the appropriate return register
asm fn get_flags(): int for amd64
    PUSHFQ
    POP RAX
    MOV result, RAX
end get_flags

Register Usage

AMD64 Conventions

  • Parameters: Passed in registers (RDI, RSI, RDX, RCX, R8, R9) or stack
  • Return: RAX (integer), XMM0 (floating point)
  • Callee-saved: RBX, RBP, R12-R15
  • Caller-saved: RAX, RCX, RDX, RSI, RDI, R8-R11

ARM64 Conventions

  • Parameters: X0-X7 (integer), D0-D7 (floating point)
  • Return: X0 (integer), D0 (floating point)
  • Callee-saved: X19-X28, X29 (FP), X30 (LR)
  • Caller-saved: X0-X18

Labels

Use local labels with . prefix to avoid conflicts:

asm proc delay_loop(count: int) for amd64
    MOV RCX, count
.loop:
    DEC RCX
    JNZ .loop
end delay_loop

Multi-Architecture Support

Define the same function for multiple architectures:

// AMD64 version
asm proc memory_barrier() for amd64
    MFENCE
end memory_barrier

// ARM64 version
asm proc memory_barrier() for arm64
    DMB SY
end memory_barrier

The compiler selects the appropriate version based on --target.

Baremetal Compilation

For OS kernels and embedded systems, use baremetal targets:

reefc kernel.reef --target amd64-baremetal --entry none --emit-c

Baremetal Flags

Flag Description
--target amd64-baremetal x86-64 freestanding mode
--target arm64-baremetal ARM64 freestanding mode
--no-stdlib Skip libc linkage
--entry <name> Custom entry point name
--entry none Don't generate entry point
--linker-script <path> Custom linker script

Example: Minimal Kernel

// kernel.reef - Minimal x86-64 kernel

asm proc hlt() for amd64
    HLT
end hlt

asm proc cli() for amd64
    CLI
end cli

extern "C" proc reef_putchar(c: char)

proc print_string(s: string)
    mut i = 0
    // Reading s[i] up to and including the NUL terminator requires
    // unsafe -- the checked form panics on the index == length read this
    // idiom needs (same idiom as core.str.length, which wraps it the same
    // way).
    unsafe
        mut ch = s[i]
        while ch != '\0'
            reef_putchar(ch)
            i = i + 1
            ch = s[i]
        end while
    end unsafe
end print_string

proc main()
    cli()
    print_string("Hello from Reef OS!")
    loop
        hlt()
    end loop
end main

Compile:

reefc kernel.reef --target amd64-baremetal --entry none --emit-c -o kernel.c
gcc -masm=intel -ffreestanding -nostdlib -c kernel.c -o kernel.o

Use Cases

1. OS Development

  • Interrupt handlers
  • Context switching
  • Page table manipulation
  • I/O port access

2. Embedded Systems

  • Hardware register access
  • Timing-critical loops
  • Power management (WFI, HLT)

3. Performance Critical Code

  • SIMD operations
  • Atomic primitives
  • Custom memory operations

4. Hardware Abstraction

  • CPU feature detection
  • System register access
  • Cache control

Restrictions

  1. Module Level Only: Assembly functions must be declared at module level, not inside other functions or Active Objects

  2. No Active Objects: Cannot define asm fn inside Active Object definitions

  3. Simple Types: Parameters must be simple types (int, pointer, char, etc.)

  4. Single Architecture: Each asm fn/proc targets one architecture

  5. No Closures: Assembly functions cannot capture variables

Generated Code

Reef generates GCC-compatible inline assembly. For AMD64, all inline asm uses Intel syntax (destination-first operand order, bare register names). The compiler automatically passes -masm=intel to the C compiler (clang or gcc) so that the assembler interprets instructions correctly.

asm fn add(a: int, b: int): int for amd64
    MOV EAX, a
    ADD EAX, b
    MOV result, EAX
end add

Generates:

int reef_add(int a, int b) {
    int __result;
    __asm__ __volatile__ (
        "MOV EAX, %[a]\n"
        "ADD EAX, %[b]\n"
        "MOV %[result], EAX\n"
        : [result] "=r" (__result)
        : [a] "r" (a), [b] "r" (b)
        : "memory", "cc", "eax"
    );
    return __result;
}

The compiler automatically detects hard-coded register names in the asm body and adds them to the GCC clobber list (e.g., "eax" above). This prevents the C compiler from assuming those registers are preserved across the asm block.

AMD64 Intel Syntax and -masm=intel

AMD64 inline asm in Reef uses Intel syntax: bare register names (rax, not %rax), destination-first operand order (MOV dest, src), and no prefix on memory operands. This matches RISC-V and ARM64 conventions, keeping all three architectures consistent.

When you compile normally (reefc file.reef), the compiler handles everything automatically — it detects AMD64 asm blocks and passes -masm=intel to clang/gcc.

Using --emit-c (manual C compilation)

When using --emit-c to generate C code for manual compilation, you must pass -masm=intel yourself:

# Generate C code
reefc kernel.reef --target amd64-baremetal --entry none --emit-c

# Compile the C — note the -masm=intel flag!
gcc -masm=intel -ffreestanding -nostdlib -c kernel.c -o kernel.o
# or
clang -masm=intel -ffreestanding -nostdlib -c kernel.c -o kernel.o

Without -masm=intel, the assembler defaults to AT&T syntax and will reject Intel-syntax instructions with errors like:

error: unknown use of instruction mnemonic without a size suffix

The -masm=intel flag works with both GCC and Clang. It tells the assembler to interpret inline asm as Intel syntax and to expand GCC asm operand references (like %[result]) without the AT&T % prefix on registers.

Note: ARM64 and RISC-V are unaffected — those architectures have a single assembly syntax defined by the ISA spec, with bare register names that work naturally with GCC extended asm.

Best Practices

  1. Keep It Simple: Use inline assembly only when necessary
  2. Document Intent: Add comments explaining what the assembly does
  3. Test Thoroughly: Assembly bypasses type checking
  4. Use Labels: Prefix with . to avoid symbol conflicts
  5. Consider Portability: Provide versions for each target architecture
  6. Use Parameter Names: Write MOV EAX, port not MOV EAX, EDI — the compiler maps parameters to operands and detects hard-coded registers for the clobber list
  7. Remember -masm=intel: When using --emit-c for AMD64, pass -masm=intel to your C compiler

See Also