Note: Currently amd64 targets only (e.g. windows_amd64, linux_amd64, darwin_amd64).
An asm template is a callable entity, instantiated in place at each call like a
forced-inline procedure. It is not a statement block spliced into a surrounding
procedure, rather it behaves like an intrinsic. Some platform-specific intrinsics
will be replaced by this system in the near future.
The general instruction [operand{, operand}] form is intended as a universal syntax
across instruction set architectures: every ISA shares this common grammar while still
exposing its own instructions and registers. The approach is modeled on
Go’s Plan 9–derived assembler, which likewise uses one syntax across all its targets
(Go’s assembler guide, the Plan 9 assembler manual).
That syntax originated with Plan 9 (Ken Thompson’s toolchain) and was carried into Go.
Declaration
name :: asm(params) -> (results) [bindings] {
body
}
- params- input operands. Plain names and types.
- results- output operands. Plain names and types.
- bindings- ties, pins, scratch, width-views, and clobbers and effects.
- body- the instruction stream.
-> (results) and the [bindings] block are optional.
Results may be left unbound at the call site; the compiler ignores the unused ones implicitly, so a template whose results are ABI artifacts need not be destructured.
mfence :: asm() [ #side_effects, #clobber memory ] { mfence }
// NOTE: `#volatile` and `#clobber memory` are both inferred here from
// the use of `mfence`, but are written for clarity.
The body uses Intel operand order (dst, src); the backend lowers per-target.
Physical registers take a % sigil (%rax); parameter and scratch names are
bare (r, acc).
Parameter types
A parameter type is one of: integer, float, boolean, pointer, multi-pointer, or
#simd[N]T.
A $name parameter is a compile-time immediate ($ctrl: u8). It must not be
pointer-like. Its value is only known, and only range-checked, at instantiation.
Bindings
The [...] block holds everything that is not a plain input or output name.
Ties and pins are edges onto names in the signature; scratch, width-views, and
clobbers are declared in the block directly.
| Form | Meaning |
|---|---|
| in -> out | tie: outis read-write, sharingin’s register |
| name = %reg | pin nameto a physical register |
| in -> out = %reg | in/out pinned to a fixed register |
| name: T | scratch register of type T |
| name: T = %reg | scratch register of type Tpinned to a physical register |
| view: T = src | width-view of src’s register at widthT |
| #clobber x | clobber a register, flags, ormemory |
| #volatile | marks a whole template as volatile |
| #align_stack | forces stack realignment on entry to the template |
The right-hand side disambiguates the two = forms: = %reg (a register) is a
pin; = src (a name) is a width-view of another operand.
A bare -> leaves the register to the compiler. = %reg pins to a specific
register for the target platform.
Scratch declarations are template-lifetime registers, allocated once. They live in the binding block, not the body—there is no block scope in a template.
Ties
in -> out binds an input and an output to one register. Lowers to a
read-write operand (+r). Tie with a pin (in -> out = %rax) fixes the
register; tie without one lets the allocator choose.
add_one :: asm(x: u64) -> (r: u64) [ x -> r ] { inc r }
Pins
name = %reg forces a specific physical register. Register names are the
target’s own (%rax, %xmm0, %r11, %al). Two operands may pin the same
register (an in-out that needs no tie):
divmod_u64 :: asm(n: u64, d: u64) -> (quo, rem: u64) [
n -> quo = %rax,
rem = %rdx,
#clobber flags,
] {
xor %rdx, %rdx
div d
}
Scratch
name: T is a working register whose class comes from T (i64 → GP,
#simd[4]f32 → vector). Unpinned scratch is early-clobbered—it can never
alias an input. Pin scratch with name: T = %reg for a fixed register, or use
#clobber %reg if it is only trashed, not named.
Width-views
view: T = src is a second name for src’s register, seen at width T.
One register, two widths; no pin, so the allocator stays free. Integer only, and
T must be narrower than src’s width (a view exists to name a sub-register).
For the setcc-then-arithmetic idiom:
count_less :: asm(x: []i64, n: i64, thr: i64) -> (count: i64) [
acc: i64,
pred: i64,
predb: u8 = pred, // low-8 view of pred
: i64,
#clobber flags,
#clobber memory,
] {
// ... setl predb ; add acc, pred ...
Memory operands
Intel-style effective addresses. The general form is
[base + index*scale + disp]; any component may be omitted:
[base][base + index][base + index*scale]—scale-1,-2,-4,-8[base + index<<scale]—shift form, scale-0..=3[base + index*scale + disp]
[base + index>>scale] is accepted only on targets that encode it (e.g. arm64),
not amd64.
Rules:
- base and index must be 32- or 64-bit integer registers, and the same width.
%rsp/-%espmay be a base but never an index.- a scale requires an index.
- the displacement is a compile-time integer that fits a signed 32-bit value; a register belongs in the index slot, not the displacement.
Size annotation
[base]:T gives the memory operand an explicit access width, for when no
register operand pins it (crc32 r32, r/m8). It is a type ascription—only the
size and class of T are used—not a value cast, so :u8, :i8, :b8 are
identical.
crc32_buf :: asm(init: u32, p: [^]u8, len: i64) -> (crc: u32) [
init -> crc,
i: i64,
#clobber flags,
#clobber memory,
] {
xor i, i
cmp i, len
jge .done
.loop:
crc32 crc, [p + i]:u8
add i, 1
cmp i, len
jl .loop
.done:
}
Labels
.name: defines a label; .name references it. Labels are local to the
template and mangled per instantiation, so a template may be inlined many times
without symbol collisions. There are no global labels.
Prefixes
A prefix is a separate line before the instruction it applies to:
lock
xadd [p], old
A prefix takes no operands, must be immediately followed by an instruction (not
a label or another prefix), and is checked for legality against the following
instruction’s form (lock requires a memory destination; rep/repne require
a string instruction).
Clobbers and effects
Three orthogonal axes:
#clobber %reg- a register is trashed.#clobber flags- the condition codes (flags) are modified.#clobber memory- memory the compiler cannot see is read or written; also forces ordering.
Most clobbers are inferred from the instructions used; write them explicitly only for effects that cannot be inferred (e.g. runtime-dependent AVX-512 masking).
Directives
Placed with the [...] block.
#volatilemarks the whole template as- volatile: it must not be deleted even if its results are unused, nor reordered. This is distinct from-#clobber memory(an ordering/visibility statement) and from-#clobber flags.#align_stackforces stack realignment on entry to the template, for instructions that require an aligned stack.
Semantic checking
Templates are not passed through to the assembler verbatim. The frontend type-checks every instruction against the target’s own encoding tables—the same data the backend encodes from—so most mistakes are caught at compile time, at the offending token, rather than surfacing as an opaque assembler error later.
Checked, per instruction:
- Unknown mnemonic or prefix.Reported with a- did-you-meansuggestion drawn from the target’s mnemonic set (-
movsss→ did you mean-movss,-movsd?). Unknown registers are suggested the same way. - Operand count.Too few or too many operands names the accepted arity.
- Operand kind.Register vs memory vs immediate vs label, matched against each encoding form. A mismatch names the operand and what was expected there (e.g.- operand 2 expected a register, got an immediate).
- Operand size and class.A-
u32into a 64-bit slot, a-#simd[8]f32into an xmm slot, or an integer where a vector register is required is reported with the expected and actual widths/classes. A scalar float is accepted in a vector-register slot (it uses the low lane); a-#simdvector must match the slot width exactly. - Immediate ranges.A constant that does not fit the form’s immediate width is rejected with the value and the width it overflows (-
36893488147419103232does not fit a 32-bit immediate).-$immediates are range-checked at instantiation. - Memory operands.Base/index register class and width agreement,-
%rsp/-%espmisused as an index, a scale without an index, an out-of-range or register-valued displacement.
When several encoding forms exist for a mnemonic, the checker reports against
the closest form—the one the operands most nearly satisfied—so the
suggestion points at the encoding you most likely intended rather than an
unrelated one. Prefix legality (lock on a memory destination; rep/repne
on a string instruction) is checked against the selected form.
Clobbers, condition-code effects, and side effects are inferred from the
instructions used; the explicit #clobber and #side_effects forms are for the
cases the tables cannot infer (see above).
Instantiation
A template is a macro: it expands at each call. Immediate-value ranges for $
parameters are therefore evaluated per call, not at the template definition.
asm groups #
Akin to procedure groups for explicit overloading, asm templates can be grouped too to have overloading behaviour:
store_u32 :: asm(p: ^u32, v: u32) {
mov [p], v
}
store_u64 :: asm(p: ^u64, v: u64) {
mov [p], v
}
store :: asm{
store_u32,
store_u64,
}
Inline asm calls
Sometimes you just want a single instantiation of an asm template which is to be called immediately.
asm(p: ^u64, v: u64) {
mov [p], v
}(&x[i], 123)
This can be only used directly within a call expression, and cannot be used as a regular value since it does not really exist since it is purely a template.
Directive Instructions
-
#byte N[, N]- Directly emit a byte or bytes (represented as integers) as instruction information. -
#skip N- Produces N-bytes of zeros -
#nop N- Produces N-bytes worth of “nops”, emitting the minimal number of “nop”-like instructions -
#align N- Aligns the next instructionNbytes which must be a power of two -
Aligns the next instruction
Examples showing syntax
```
add_one :: asm(x: u64) -> (r: u64) [
x -> r,
]{
inc r
}
add_u64 :: asm(x, y: u64) -> (r: u64) {
mov r, x
add r, y
}
swap :: asm(x, y: u64) -> (a, b: u64) [
x -> a,
y -> b,
]{
xchg a, b
}
rol_imm :: asm(x: u32, $n: i32) -> (r: u32) [
x -> r,
]{
rol r, n
}
rdtsc :: asm() -> (lo, hi: u32) [
lo = %eax,
hi = %edx,
] {
rdtsc
}
cpuid :: asm(leaf: u32) -> (a, b, c, d: u32) [
leaf -> a = %eax,
b = %ebx,
c = %ecx,
d = %edx,
] {
cpuid
}
// [#clobber memory] is inferred
:: asm(p: ^u64, v: u64) {
mov [p], v
}
// [#volatile] is inferred but written explicit for clarity
:: asm() [#volatile] {
mfence
}
dot_f32x4 :: asm(a, b: [^]f32, n: i64) -> (result: f32) [
acc: #simd[4]f32,
tmp: #simd[4]f32,
i: i64,
#clobber flags, // the cmp/jl sets flags
clobber memory, // conservatively: we read memory the compiler can't see
xorps acc, acc // acc = {0,0,0,0}
.loop:
movups tmp, [a + i4] // load 4 floats from a; scale 4 = sizeof(f32)
+ i4] // tmp = 4 floats from b (mulps xmm, m128)
add i, 4
cmp i, n
jl .loop // .loop is frontend-mangled per expansion
// horizontal fold: {a0+a1, a2+a3, ...}
// {sum, sum, sum, sum}
// result = acc[0]
dot_f32x4_v2 :: asm(a, b: [^]f32, n: i64) -> (result: f32) [
acc0: #simd[4]f32,
acc1: #simd[4]f32,
t0: #simd[4]f32,
t1: #simd[4]f32,
i: i64,
] {
vxorps acc0, acc0, acc0
vxorps acc1, acc1, acc1
xor i, i
.loop:
vmovups t0, [a + i<<2] // equivalent to [a + i4]
+ i<<2 + 16]
vfmadd231ps acc0, t0, [b + i<<2] // acc0 += t0 * b[i:][:4]
+ i<<2 + 16] // acc1 += t1 * b[i+4:][:4]
8
cmp i, n
jl .loop
vaddps acc0, acc0, acc1 // combine the two chains
vhaddps acc0, acc0, acc0
vmovss result, acc0, acc0
}
shuffle4 :: asm(v: #simd[4]f32, $ctrl: u8) -> (r: #simd[4]f32) [
v -> r, // xmm in/out tie; r starts as v
shufps r, r, ctrl // permute r's 4 lanes by the imm8 control
memcpy_rep :: asm(dst, src: rawptr, len: uint) -> (end_dst, end_src: rawptr, rem: uint) [
dst -> end_dst = %rdi,
src -> end_src = %rsi,
len -> rem = %rcx,
] {
rep
movsb
}
divmod_u64 :: asm(n: u64, d: u64) -> (quo, rem: u64) [
n -> quo = %rax,
rem = %rdx,
] {
xor %rdx, %rdx // clear high half of the dividend
// rax = rdx:rax / d ; rdx = remainder
crc32_buf :: asm(init: u32, p: [^]u8, len: i64) -> (crc: u32) [
init -> crc,
i: i64,
] {
xor i, i
cmp i, len
jge .done
.loop:
crc32 crc, [p + i + 0]:u8
add i, 1
cmp i, len
jl .loop
.done:
}
atomic_fetch_add :: asm(p: ^i64, delta: i64) -> (old: i64) [
delta -> old,
] {
lock
xadd [p], old // [p] += old; old = previous [p].
count_less_than :: asm(src: [^]i64, n: i64, threshold: i64) -> (count: i64) [
acc: i64, // running count (unpinned scratch -> allocator's choice)
: i64, // predicate register, used at two widths
: u8 = pred, // the low-8 view of pred, for setl
: i64, // loaded element
: i64, // loop index
xor acc, acc
xor i, i
cmp i, n
jge .done
.loop:
mov elem, [src + i8]
xor pred, pred // zero the full 64-bit register first
setl predb // predb = (elem < threshold) ? 1 : 0 -> low byte of pred
// read pred at 64-bit width; upper bits are known 0
1
cmp i, n
jl .loop
.done:
mov count, acc
}
tzcnt :: asm(x: u64) -> (count: u64, was_zero: bool) [
was_zero = %flags.z,
] {
tzcnt count, x
}
bit_reset :: asm(in_val: i32, bit: i32) -> (out_val: i32, f: bool) [
in_val -> out_val,
f = %flags.c,
] {
btr out_val, bit
}
main :: proc() {
// scalar result
:= add_one(41) // -> 42
"add_one:", a1)
// scalar result, aliasing-hazard case
:= add_u64(20, 22) // -> 42 (see NOTE on proc)
"add_u64:", s)
// multiple return values
:= swap(1, 2) // -> 2, 1
"swap:", x, y)
// immediate operand: n must be a compile-time constant
:= rol_imm(0x0000_00FF, 8) // -> 0x0000_FF00
"rol_imm:", rr)
// pinned outputs -> two-field destructure
:= rdtsc()
tsc := (u64(hi) << 32) | u64(lo)
fmt.println("rdtsc:", tsc)
// four-result destructure, result order preserved
:= cpuid(0)
fmt.println("cpuid.0:", ea, eb, ec, ed)
// store, consumed only for its side effect
: u64
store_u64(&slot, 0xDEAD_BEEF)
fmt.println("store_u64:", slot)
// vector kernel: [^]f32 args via raw_data, scalar f32 result
:= [8]f32{1, 2, 3, 4, 5, 6, 7, 8}
ys := [8]f32{8, 7, 6, 5, 4, 3, 2, 1}
d := dot_f32x4_v2(raw_data(xs[:]), raw_data(ys[:]), 8)
fmt.println("dot:", d) // 18+27+...+81 = 120
// pure side-effect, no result binding
}
```