Skip to content

asm templates - #7271

Open
gingerBill wants to merge 51 commits into
masterfrom
bill/inline-asm
Open

asm templates#7271
gingerBill wants to merge 51 commits into
masterfrom
bill/inline-asm

Conversation

@gingerBill

@gingerBill gingerBill commented Aug 9, 2026

Copy link
Copy Markdown
Member

Note: Only -target:amd64 is supported for now.

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.

Rather than free-standing assembly blocks, Odin uses assembly templates. A template is instantiated at each call site like a procedure, which makes it easy to compose and lets it behave much like an intrinsic.

Over time, some of the current platform-specific intrinsics will be reimplemented on top of this asm template system.

Plans:

  • ISAs:
    • AMD64
    • ARM64
    • RISV64
  • Semantic checking for all of the instructions utilizing core:rexcode for all of the encoding table information
  • Infer clobbering from the instructions where possible
    • Not always possible such as with AVX-512 instructions with runtime dependence
  • Good Error messages

Universal Syntax

n.b. A universal syntax does not mean a generic set of mnemonics, it just means the syntax (not the semantics) are shared across ISAs.

  • Explicit registers %rax
  • No need for sigil prefixes on anything as literals share the same syntax as Odin literals
  • All mnemonics are lowercase
  • Width-based forms of a mnemonics are inferred from the operands
  • Memory operands share the same syntax of [base + index*scale + disp]
    • If the platform does not support all of those parameters e.g. disp is not on ARM64, then the compiler the err on that.
    • [base]
    • [base + index]
    • [base + index*scale]
    • [base + index<<scale]
    • [base + index>>scale] (platform specific)
    • [base + index*scale + disp] (platform specific)
    • [..]:type // type interpretation
  • Types are Odin types
  • Specifications are done in a [] block. A specification can state:
    • a parameter is tied to another parameter (i.e. shared the same register) x -> y
    • a parameter is pinned to a specific register: x = %rax
    • define a scratch parameter: acc0: #simd[4]f32 or acc0: #simd[4]f32 = %xmm0
    • Clobbered parameters
      • #clobber memory
      • #clobber cc
      • #clobber %rcx
  • The order of the operands matches Intel-like fashion e.g. mnemonic dst, src

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
}

store_u64 :: asm(p: ^u64, v: u64) [
	#clobber memory, // the compiler can infer this in this specific case, thus it is optional
] {
	mov [p], v
}

// `#side_effects` is optional because the compiler can infer it from the 
// use of the mfence mnemonic
mfence :: asm() #side_effects { 
	mfence
}

dot_f32x4 :: asm(a, b: [^]f32, n: i64) -> (result: f32) [
	acc: #simd[4]f32,
	tmp: #simd[4]f32,
	i:   i64,
	#clobber cc,     // 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,
	#clobber cc,
	#clobber memory,
] {
	vxorps acc0, acc0, acc0
	vxorps acc1, acc1, acc1
	xor    i, i
.loop:
	vmovups     t0, [a + i<<2]
	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,
	#clobber memory,
] {
	rep
	movsb
}

divmod_u64 :: asm(n: u64, d: u64) -> (quo, rem: u64) [
	n -> quo = %rax,
	rem      = %rdx,
	#clobber cc,
] {
	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,
	#clobber cc,
	#clobber memory,
] {
	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,
	#clobber cc,
	#clobber memory,
] {
	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
	#clobber cc,
	#clobber memory,
] {
	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
}
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 := [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)             // 1*8+2*7+...+8*1 = 120

	// pure side-effect, no result binding
	mfence()
}

I know... a class. If only C++ had a nice way to package different things like Odin itself.
@gingerBill
gingerBill marked this pull request as ready for review August 12, 2026 15:49
@gingerBill gingerBill changed the title WIP: asm templates asm templates Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant