Assembly Functions

An asm func is a function whose body is processor instructions instead of statements. Its signature is ordinary Rux, and callers call it like any function; its body is assembled by the compiler's own assembler and emitted exactly as written — no prologue, no epilogue, no checks. Use one for an instruction the language cannot express, a system-call stub, or exact control over a hot loop, and expect to manage registers, the stack and the calling convention yourself.

asm-func  = attributes [ "pub" ] "asm" "func" identifier "(" [ parameters ] ")" [ "->" type ] "{" { asm-item } "}"
asm-item  = label ":" | mnemonic [ operand { "," operand } ]

asm is not a keyword; it means this only directly before func. Instructions are separated by nothing but whitespace — one per line by convention — and // and /* … */ comments are allowed. Mnemonics and register names are case-insensitive.

The body works on registers

The parameters fix the signature and the convention callers use, but they are not names inside the body. The arguments are in whatever registers the convention puts them in, and the result goes where the convention says:

// Win64: the first two integers arrive in rcx and rdx; the result goes back in rax.
#Abi(.Win64)
asm func Add(a: int64, b: int64) -> int64 {
    mov rax, rcx
    add rax, rdx
    ret
}

Naming a parameter is an operand error (add rax, b fails with unsupported operands for 'add'). Nothing is added around the body, not even the ret: leave it out and execution runs on into whatever bytes follow. Preserving callee-saved registers and keeping the stack aligned across a call are the body's job.

Choosing the convention

Without #Abi an asm func uses the target's C convention, so a body that reads rcx is right on Windows and wrong on Linux. Pin every x86-64 body to the convention it was written for — #Abi(.Win64) or #Abi(.SysV) — and it works on every x86-64 system, because the compiler adapts each call to it:

ConventionInteger argumentsFloat argumentsResult
Win64rcx, rdx, r8, r9xmm0–xmm3rax / xmm0
System Vrdi, rsi, rdx, rcx, r8, r9xmm0–xmm7rax / xmm0
AAPCS64x0–x7d0–d7x0 / d0

AArch64 has the one convention on every system, so its bodies carry no #Abi.

Labels

A label is a name followed by :, and jumps and branches name it. Labels belong to their function, so two functions may both use next::

// The sum 1 + 2 + … + n, as a label and a conditional jump.
#Abi(.Win64)
asm func SumTo(n: int64) -> int64 {
    xor rax, rax
next:
    test rcx, rcx
    jle done
    add rax, rcx
    dec rcx
    jmp next
done:
    ret
}

A name that is neither a register nor a label is a symbol: another function, which call and jmp reach through a relocation the linker resolves.

Undefined symbols.
rux 0.4.0 does not yet report a symbol that names nothing — a misspelt label, or a const, which has no storage — and the executable it produces fails as soon as it starts. Check every symbol an asm body names.

One architecture per body

An asm body belongs to one architecture, and the compiler checks every mnemonic against the target's:

error: 'csel' is an AArch64 instruction, but asm func 'A' is compiled for x86-64

A package that supports both architectures writes one body per architecture and lets when keep the right one. The untaken body is parsed but never assembled:

import Core::{ #target, #Error };

when #target.arch {
    .X86_64 => {
        #Abi(.SysV)
        asm func Add(a: int64, b: int64) -> int64 {
            mov rax, rdi
            add rax, rsi
            ret
        }
    },
    .AArch64 => {
        asm func Add(a: int64, b: int64) -> int64 {
            add x0, x0, x1
            ret
        }
    },
    else => #Error("no assembly for this architecture")
}

Mnemonic and operand checks run when the body is assembled, which rux build does and rux check does not. Build for each architecture — rux build --target linux-aarch64 works on any host — to have every body assembled.

x86-64

Intel syntax: the destination is the first operand.

OperandWritten as
registerrax…r15, eax…r15d, ax…r15w, al…r15b (with spl, bpl, sil, dil), ah, bh, ch, dh, xmm0…xmm15
immediate42, -1, 0x10, 0o7, 0b1010, 1_000
memory[base + index*scale ± disp], any part optional, scale 1, 2, 4 or 8; a size prefix byte, word, dword or qword where the width is otherwise unclear
symbola function name, as in call Helper
#Abi(.Win64)
asm func Index(values: *int64, i: int64) -> int64 {
    mov rax, qword [rcx + rdx*8]
    ret
}

The assembler encodes a chosen subset of x86-64:

FamilyInstructions
data movementmov, movzx, movsx, movsxd, lea, push, pop
integer arithmeticadd, adc, sub, sbb, inc, dec, neg, mul, imul, div, idiv, cmp
logic, shifts, rotatesand, or, xor, not, test, shl, sal, shr, sar, rol, ror (count an immediate or cl)
sign extensioncdq, cqo, cdqe
control flowjmp, jcc (je, jne, jl, jge, jb, ja, …), setcc (sete, setl, …), call, ret, leave, nop
systemsyscall, int, int3
SSE movesmovd, movq, movss, movsd, movaps, movapd, movups, movupd
SSE arithmeticadd, sub, mul, div, min, max, sqrt in ss, sd, ps and pd forms (addsd, sqrtps, …)
SSE compare and convertcomiss, comisd, ucomiss, ucomisd, cvtsi2ss, cvtsi2sd, cvtss2si, cvtsd2si, cvttss2si, cvttsd2si, cvtss2sd, cvtsd2ss
SSE bitwise and integerandps, andpd, andnps, andnpd, orps, orpd, xorps, xorpd, pand, por, pxor, paddb/w/d/q, psubb/w/d/q, pmullw

Other x86-64 instructions — cmovcc, popcnt, bsf, cpuid, xchg, the string instructions, the x87 stack and more — are recognized but not encoded:

error: instruction 'popcnt' is recognized for target 'windows-x86_64' but is not implemented by its assembler
  note: this is an internal compiler limitation, not malformed inline assembly

A misspelling is answered with the nearest real name: unknown instruction 'movx'; did you mean 'mov'?.

AArch64

Most instructions take a destination and two sources (add x0, x0, x1), and an immediate is written with #.

OperandWritten as
registerx0–x30, w0–w30, xzr, wzr, sp, wsp, fp (x29), lr (x30); b, h, s, d, q 0–31 for the vector registers
immediate#1, #-1, #0xF; a shift or extend after a comma: #0x1234, lsl #16, x1, lsl #3
memory[x0], [x0, #8], [x0, x1], [x0, x1, lsl #3], [x0, w1, sxtw #2]; pre-index [sp, #-16]!; post-index [sp], #16
conditionas a branch suffix, b.lt or blt; or as an operand, csel x0, x0, x1, gt
symbola function name, as in bl Twice
asm func Max(a: int64, b: int64) -> int64 {
    cmp x0, x1
    csel x0, x0, x1, gt
    ret
}

asm func Element(values: *int32, i: int32) -> int64 {
    ldrsw x0, [x0, w1, sxtw #2]
    ret
}

asm func Twice(x: int64) -> int64 {
    lsl x0, x0, #1
    ret
}

asm func TwiceThenAddOne(x: int64) -> int64 {
    stp x29, x30, [sp, #-16]!
    mov x29, sp
    bl Twice
    add x0, x0, #1
    ldp x29, x30, [sp], #16
    ret
}

A body that calls another function must save and restore x30, the link register, as TwiceThenAddOne does; AAPCS64 also gives no red zone, so a body opens its own stack space before storing below sp.

FamilyInstructions
arithmeticadd, adds, sub, subs, neg, negs, cmp, cmn, mul, madd, msub, mneg, smull, umull, smulh, umulh, smaddl, umaddl, sdiv, udiv
logic and bitsand, ands, orr, orn, eor, eon, bic, bics, mvn, tst, lsl, lsr, asr, ror (and …v forms), clz, cls, rbit, rev, rev16, rev32, extr, bfi, bfxil, bfm, sbfm, ubfm, sbfx, ubfx, sxtb, sxth, sxtw, uxtb, uxth
movesmov, movz, movn, movk, adr, adrp, mrs, msr
conditionalcsel, csinc, csinv, csneg, cset, csetm, cinc, cinv, cneg
memoryldr, ldrb, ldrh, ldrsb, ldrsh, ldrsw, ldur…, str, strb, strh, stur…, ldp, stp
branchesb, b.cond, bl, br, blr, ret, cbz, cbnz, tbz, tbnz
systemsvc, brk, hlt, hint, nop, dmb, dsb, isb, udf
floating pointfadd, fsub, fmul, fdiv, fsqrt, fabs, fneg, fmin, fmax, fminnm, fmaxnm, fmadd, fmsub, fnmadd, fnmsub, fcmp, fcmpe, fccmp, fcsel, fmov, fcvt, fcvtzs, fcvtzu, scvtf, ucvtf, frinta, frintm, frintn, frintp, frintz

fmov takes registers only; a floating-point immediate such as #0.5 is not accepted. Instructions outside this set — atomics, ccmp, prfm, the cache and TLB maintenance instructions and others — are recognized but not encoded, as on x86-64. Operand mistakes name the form the instruction takes:

error: 'add' takes 3 operands, found 2; the form is 'ADD Rd, Rn, #imm | Rd, Rn, Rm{, shift #amount}'
add with a symbol.
An AArch64 add whose third operand is a name that is not a register is not yet rejected by rux 0.4.0 — add x0, x0, banana assembles. Write registers and # immediates only.

See also