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:
| Convention | Integer arguments | Float arguments | Result |
|---|---|---|---|
| Win64 | rcx, rdx, r8, r9 | xmm0–xmm3 | rax / xmm0 |
| System V | rdi, rsi, rdx, rcx, r8, r9 | xmm0–xmm7 | rax / xmm0 |
| AAPCS64 | x0–x7 | d0–d7 | x0 / 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.
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.
| Operand | Written as |
|---|---|
| register | rax…r15, eax…r15d, ax…r15w, al…r15b (with spl, bpl, sil, dil), ah, bh, ch, dh, xmm0…xmm15 |
| immediate | 42, -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 |
| symbol | a 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:
| Family | Instructions |
|---|---|
| data movement | mov, movzx, movsx, movsxd, lea, push, pop |
| integer arithmetic | add, adc, sub, sbb, inc, dec, neg, mul, imul, div, idiv, cmp |
| logic, shifts, rotates | and, or, xor, not, test, shl, sal, shr, sar, rol, ror (count an immediate or cl) |
| sign extension | cdq, cqo, cdqe |
| control flow | jmp, jcc (je, jne, jl, jge, jb, ja, …), setcc (sete, setl, …), call, ret, leave, nop |
| system | syscall, int, int3 |
| SSE moves | movd, movq, movss, movsd, movaps, movapd, movups, movupd |
| SSE arithmetic | add, sub, mul, div, min, max, sqrt in ss, sd, ps and pd forms (addsd, sqrtps, …) |
| SSE compare and convert | comiss, comisd, ucomiss, ucomisd, cvtsi2ss, cvtsi2sd, cvtss2si, cvtsd2si, cvttss2si, cvttsd2si, cvtss2sd, cvtsd2ss |
| SSE bitwise and integer | andps, 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 #.
| Operand | Written as |
|---|---|
| register | x0–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 |
| condition | as a branch suffix, b.lt or blt; or as an operand, csel x0, x0, x1, gt |
| symbol | a 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.
| Family | Instructions |
|---|---|
| arithmetic | add, adds, sub, subs, neg, negs, cmp, cmn, mul, madd, msub, mneg, smull, umull, smulh, umulh, smaddl, umaddl, sdiv, udiv |
| logic and bits | and, 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 |
| moves | mov, movz, movn, movk, adr, adrp, mrs, msr |
| conditional | csel, csinc, csinv, csneg, cset, csetm, cinc, cinv, cneg |
| memory | ldr, ldrb, ldrh, ldrsb, ldrsh, ldrsw, ldur…, str, strb, strh, stur…, ldp, stp |
| branches | b, b.cond, bl, br, blr, ret, cbz, cbnz, tbz, tbnz |
| system | svc, brk, hlt, hint, nop, dmb, dsb, isb, udf |
| floating point | fadd, 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
#Abi— the conventions a body is written against- Conditional compilation — per-architecture bodies
- Compile-time context —
#target.arch - RCU — how an assembled body and its relocations are stored
- Learn: Assembly, ARM assembly