Odin's New Inline Assembly Templates

Lobsters Hottest News

Summary

The article details new inline assembly templates for the Odin programming language, featuring a universal syntax inspired by Go's assembler and supporting amd64 targets.

<p><a href="https://lobste.rs/s/loltt2/odin_s_new_inline_assembly_templates">Comments</a></p>
Original Article
View Cached Full Text

Cached at: 08/18/26, 08:22 PM

# Inline asm Templates Overview Source: [https://odin-lang.org/docs/inline-asm/](https://odin-lang.org/docs/inline-asm/) > **Note:**Currently amd64 targets only \(e\.g\.`windows\_amd64`,`linux\_amd64`,`darwin\_amd64`\)\. ## Overview[\#](https://odin-lang.org/docs/inline-asm/#overview) 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](https://go.dev/doc/asm),[the Plan 9 assembler manual](https://9p.io/sys/doc/asm.html)\)\. That syntax originated with Plan 9 \(Ken Thompson’s toolchain\) and was carried into Go\. Odin’s inline assembly uses a context\-free grammar, but this does not mean any mnemonics are shared across ISAs—only the syntax itself is\. ## Declaration[\#](https://odin-lang.org/docs/inline-asm/#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, clobbers, and effects\. - `body`\- the instruction stream\. Both`\-\> \(results\)`and the`\[bindings\]`block are optional\. 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`\)\. 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() [ #volatile, #clobber memory ] { mfence } // NOTE: `#volatile` and `#clobber memory` are both inferred here from // the use of `mfence`, but are written for clarity. ``` ### Parameter types[\#](https://odin-lang.org/docs/inline-asm/#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, and its value is only known—and only range\-checked—at instantiation\. ## Bindings[\#](https://odin-lang.org/docs/inline-asm/#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\. FormMeaning`in \-\> out`tie:`out`is read\-write, sharing`in`’s register`name = %reg`pin`name`to 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`T`pinned to a physical register`view: T = src`width\-view of`src`’s register at width`T``\#clobber x`clobber a register,`flags`, or`memory``\#volatile`marks a whole template as volatile`\#align\_stack`forces stack realignment on entry to the templateThe 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, while`= %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\. ### Registers[\#](https://odin-lang.org/docs/inline-asm/#registers) Explicit registers are prefixed with`%`to prevent namespace collisions with user\-provided parameters and with other global constants in parent scopes\. Register names are the target’s own \(e\.g\.`%rax`,`%xmm0`,`%r11`,`%al`on AMD64\), named specifically for each platform rather than given generalized names\. Depending on the template, it may be common to use explicit registers everywhere, or common to use scratch parameters instead\. ### Ties[\#](https://odin-lang.org/docs/inline-asm/#ties) `in \-\> out`binds an input and an output to one register\. It lowers to a read\-write operand \(`\+r`\)\. A tie with a pin \(`in \-\> out = %rax`\) fixes the register; a tie without one lets the allocator choose\. ``` add_one :: asm(x: u64) -> (r: u64) [ x -> r ] { inc r } ``` ### Pins[\#](https://odin-lang.org/docs/inline-asm/#pins) `name = %reg`forces a specific physical register\. 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[\#](https://odin-lang.org/docs/inline-asm/#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[\#](https://odin-lang.org/docs/inline-asm/#width-views) `view: T = src`is a second name for`src`’s register, seen at width`T`\. One register, two widths; with no pin, the allocator stays free\. It is 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 i: i64, #clobber flags, #clobber memory, ] { // ... setl predb ; add acc, pred ... } ``` ## Memory operands[\#](https://odin-lang.org/docs/inline-asm/#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`/`%esp`may 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[\#](https://odin-lang.org/docs/inline-asm/#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[\#](https://odin-lang.org/docs/inline-asm/#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[\#](https://odin-lang.org/docs/inline-asm/#prefixes) A prefix is a separate line before the instruction it applies to: 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\)\. ## Data and alignment directives[\#](https://odin-lang.org/docs/inline-asm/#data-and-alignment-directives) These directives are written in the body and emit raw bytes or control layout in the instruction stream: - `\#byte N\[, N\]`— directly emit a byte or bytes \(represented as integers\) as instruction information\. - `\#skip N`— produce N bytes of zeros\. - `\#nop N`— produce N bytes’ worth of “nops”, emitting the minimal number of “nop”\-like instructions\. - `\#align N`— align the next instruction to`N`bytes, which must be a power of two\. ## Clobbers and effects[\#](https://odin-lang.org/docs/inline-asm/#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\)\. ## Template directives[\#](https://odin-lang.org/docs/inline-asm/#template-directives) Placed within the`\[\.\.\.\]`block: - `\#volatile`marks 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\_stack`forces stack realignment on entry to the template, for instructions that require an aligned stack\. ## Semantic checking[\#](https://odin-lang.org/docs/inline-asm/#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\-mean*suggestion 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`u32`into a 64\-bit slot, a`\#simd\[8\]f32`into 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`\#simd`vector 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 \(`36893488147419103232`does not fit a 32\-bit immediate\)\.`$`immediates are range\-checked at instantiation\. - **Memory operands\.**Base/index register class and width agreement,`%rsp`/`%esp`misused 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`\#volatile`forms are for the cases the tables cannot infer \(see above\)\. ## Instantiation[\#](https://odin-lang.org/docs/inline-asm/#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[\#](https://odin-lang.org/docs/inline-asm/#asm-groups) Like procedure groups \(which provide explicit overloading\),`asm`templates can also be grouped to give 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[\#](https://odin-lang.org/docs/inline-asm/#inline-asm-calls) Sometimes you just want a single instantiation of an`asm`template that is called immediately: ``` asm(p: ^u64, v: u64) { mov [p], v }(&x[i], 123) ``` This form can only be used directly within a call expression; it cannot be used as a regular value, because it does not really exist—it is purely a template\. ## Examples Showing The Syntax[\#](https://odin-lang.org/docs/inline-asm/#examples-showing-the-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 store_u64 :: asm(p: ^u64, v: u64) { mov [p], v } // [#volatile] is inferred but written explicit for clarity mfence :: 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} xor i, i .loop: movups tmp, [a + i*4] // load 4 floats from a; scale 4 = sizeof(f32) mulps tmp, [b + i*4] // tmp *= 4 floats from b (mulps xmm, m128) addps acc, tmp add i, 4 cmp i, n jl .loop // .loop is frontend-mangled per expansion haddps acc, acc // horizontal fold: {a0+a1, a2+a3, ...} haddps acc, acc // {sum, sum, sum, sum} movss result, acc // 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 + i*4] vmovups t1, [a + i<<2 + 16] vfmadd231ps acc0, t0, [b + i<<2] // acc0 += t0 * b[i:][:4] vfmadd231ps acc1, t1, [b + i<<2 + 16] // acc1 += t1 * b[i+4:][:4] add i, 8 cmp i, n jl .loop vaddps acc0, acc0, acc1 // combine the two chains vhaddps acc0, acc0, acc0 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 div d // 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) pred: i64, // predicate register, used at two widths predb: u8 = pred, // the low-8 view of `pred`, for setl elem: i64, // loaded element i: i64, // loop index ] { xor acc, acc xor i, i cmp i, n jge .done .loop: mov elem, [src + i*8] xor pred, pred // zero the full 64-bit register first cmp elem, threshold setl predb // predb = (elem < threshold) ? 1 : 0 -> low byte of pred add acc, pred // read pred at 64-bit width; upper bits are known 0 add i, 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 a1 := add_one(41) // -> 42 fmt.println("add_one:", a1) // scalar result, aliasing-hazard case s := add_u64(20, 22) // -> 42 (see NOTE on proc) fmt.println("add_u64:", s) // multiple return values x, y := swap(1, 2) // -> 2, 1 fmt.println("swap:", x, y) // immediate operand: n must be a compile-time constant rr := rol_imm(0x0000_00FF, 8) // -> 0x0000_FF00 fmt.println("rol_imm:", rr) // pinned outputs -> two-field destructure lo, hi := rdtsc() tsc := (u64(hi) << 32) | u64(lo) fmt.println("rdtsc:", tsc) // four-result destructure, result order preserved ea, eb, ec, ed := cpuid(0) fmt.println("cpuid.0:", ea, eb, ec, ed) // store, consumed only for its side effect slot: u64 store_u64(&slot, 0xDEAD_BEEF) fmt.println("store_u64:", slot) // vector kernel: [^]f32 args via raw_data, scalar f32 result xs := make_aligned([]f32, 8, 16) ys := make_aligned([]f32, 8, 16) copy(xs, []f32{1, 2, 3, 4, 5, 6, 7, 8}) copy(ys, []f32{8, 7, 6, 5, 4, 3, 2, 1}) d := dot_f32x4_v2(raw_data(xs[:]), raw_data(ys[:]), 8) fmt.println("dot:", d) // 1*8+2*7+...+8*1 = 120 // pure side-effect, no result binding mfence() } ```

Similar Articles

Odin dev-2026-06 Released

Lobsters Hottest

Odin dev-2026-06 has been released. Odin is a data-oriented programming language designed for high-performance systems development.

Understanding the Odin Programming Language

Hacker News Top

A book teaching the Odin programming language, covering manual memory management, parametric polymorphism, and data-oriented design. Version 1.10 has been released with updates.

Odin 1.0 Announcement

Lobsters Hottest

Odin programming language announces that it will release version 1.0 in January 2027 (Odin 2027), and has published a roadmap including complete specification, inline assembly, new standard library, etc.

Tom's Namespaces: An Odin Fanfic

Lobsters Hottest

A narrative-style technical post exploring code organization in the Odin programming language, highlighting its package system, the lack of package-local namespaces, and the resulting developer workflow challenges.

Writing Portable ARM64 Assembly

Hacker News Top

A guide to writing ARM64 assembly code that is portable across Apple's Darwin and Linux/BSD systems, covering differences in ABI, symbol naming, and vector mnemonics.