The Loup Native Book — contents
  1. Chapter 1: Getting started
  2. Chapter 2: Native mode overview
  3. Chapter 3: Variables and numerics
  4. Chapter 4: Functions
  5. Chapter 5: Control flow
  6. Chapter 6: Collections
  7. Chapter 7: Structs
  8. Chapter 8: Enums and pattern matching
  9. Chapter 9: Classes and ARC
  10. Chapter 10: Interfaces
  11. Chapter 11: Generics
  12. Chapter 12: Error handling
  13. Chapter 13: Modules and packages
  14. Chapter 14: Template fences
  15. Chapter 15: Ownership verbs
  16. Chapter 16: borrow, excl borrow and noescape
  17. Chapter 17: unique ownership
  18. Chapter 18: Lifetimes and regions
  19. Chapter 19: @unsafe and calling C
  20. Chapter 20: Concurrency with OS threads
  21. Chapter 21: Async on native
  22. Chapter 22: Desktop UI
  23. Chapter 23: SIMD and GPU
  24. Chapter 24: Building and distributing
  25. Chapter 25: Memory safety patterns

Chapter 23: SIMD and GPU

⚠ Planned — not in Loup 1.25. This chapter describes the design in the Language Reference (§1-2.6 and §1-16.8) and the Packages Reference (§21.29, §21.30). The toolchain does not implement it yet, so its listings are design sketches and are not verified.

The specification gives native programs two routes to data-parallel speed: explicit CPU vector types in native.simd, and GPU compute kernels written in a shading language inside a template fence and dispatched through native.gpu. Neither ships in 1.25.1. native.simd, native.gpu and native.webgpu are not standard-library modules ('native.simd' is not a standard-library module), Vec[T, N] is an undefined name, and a wgsl fence parses as an ordinary template fence that nothing can compile or dispatch.

What a native program can use today is the LLVM backend's own optimizer: a --release build compiles through clang -O2 with ThinLTO (chapter 24), which auto-vectorizes simple loops over lists of numbers without any annotation.

CPU SIMD: native.simd

The planned module centres on one generic value type, Vec[T, N]: N lanes of a fixed-width number type, mapped directly to a hardware vector register. sizeof(T) × N must be 16, 32 or 64 bytes (an SSE/NEON, AVX or AVX-512 register); other widths are compile errors. Vectors are immutable values. They are built with splat(x) (every lane x), of(...) or load(list, offset), written back with store(list, offset), combined lane-wise with + - * /, and reduced or transformed with functions such as dot, sqrt, min, max and fma. A dot product:

Fragment — not a complete program
@mode(host.native)

from native.simd import (Vec, dot);

func dotProduct(a: list[float32], b: list[float32]): float32 {
    var sum: float32 = 0.0f32;
    var i = 0;
    while (i + 8 <= a.length()) {
        let va = Vec[float32, 8].load(a, i);
        let vb = Vec[float32, 8].load(b, i);
        sum = sum + dot(va, vb);
        i = i + 8;
    }
    while (i < a.length()) {
        sum = sum + a[i] * b[i];
        i = i + 1;
    }
    return sum;
}

Listing 23-1: A dot product with eight-lane vectors (design sketch)

The tail loop handles the elements that do not fill a whole vector. The @vectorize annotation is specified as a hint asking the compiler to vectorize a loop it would otherwise leave scalar.

SIMD is a host.native and freestanding feature. Using native.simd, Vec[T, N] or @vectorize in a host.dual file is specified as E_NATIVE_ONLY_IN_DUAL, because a host.dual program must also run as WebAssembly (§1-2.6).

GPU compute: shader fences

Three template-fence languages hold GPU shader source: wgsl (WebGPU), metal (Apple platforms only) and glsl (§1-16.8). A shader fence evaluates to the shader text; {$ … $} interpolations may only insert compile-time constants (int, float32, bool), and a runtime value is E_GPU_FENCE_RUNTIME. native.gpu compiles the text into a kernel and runs it over buffers (§21.30):

Fragment — not a complete program
@mode(host.native)

from native.gpu import (GpuKernel, GpuBuffer, dispatch);

const WGSIZE: int = 256;

async func addOnGpu(a: list[float32], b: list[float32]): Task[list[float32]] {
    let kernel = GpuKernel.compile(```(wgsl):(none)
        @group(0) @binding(0) var<storage, read_write> out: array<f32>;
        @group(0) @binding(1) var<storage, read>       x:   array<f32>;
        @group(0) @binding(2) var<storage, read>       y:   array<f32>;

        @compute @workgroup_size({$ WGSIZE $}, 1, 1)
        fn main(@builtin(global_invocation_id) gid: vec3u) {
            let i = gid.x;
            if (i < arrayLength(&out)) {
                out[i] = x[i] + y[i];
            }
        }
    ```);

    let outBuf = GpuBuffer[float32].alloc(a.length());
    let xBuf = GpuBuffer[float32].alloc(a.length());
    let yBuf = GpuBuffer[float32].alloc(b.length());
    await xBuf.copyFromHost(a);
    await yBuf.copyFromHost(b);

    let groups = (a.length() + WGSIZE - 1) / WGSIZE;
    await dispatch(kernel, [groups], [outBuf, xBuf, yBuf]);
    return await outBuf.copyToHost();
}

Listing 23-2: Adding two vectors on the GPU (design sketch)

Buffers are bound in order (binding(0), binding(1), …). Copies and dispatch are async because the GPU runs independently of the CPU, so GPU code also waits on the native async executor (chapter 21). The backend is chosen at build time (--gpu-backend wgpu|cuda|metal|vulkan, default wgpu) or at run time. The same wgsl text can also feed a render pipeline through native.webgpu (§21.29), which is the layer meant to be shared with browser code.

Structs shared with a shader need an agreed layout. @repr(C) fixes a Loup struct's layout (chapter 19); the shader's struct must then follow WGSL's own alignment rules, which pad a vec3<f32> to 16 bytes, so the Loup side adds explicit padding fields.

When the design lands

The usual guidance applies. GPU compute pays off for thousands to millions of independent, arithmetic-heavy elements whose data stays on the GPU between steps; CPU SIMD wins for smaller inputs, branchy code, and work that has to return to the CPU often. Before either, write the straightforward loop and measure a --release build, since the optimizer may already vectorize it.

Summary

  • native.simd (Vec[T, N], lane-wise operators, dot/sqrt/fma, @vectorize) is specified for host.native and freestanding and is ⚠ planned.
  • GPU kernels are specified as wgsl, metal or glsl fences compiled by native.gpu.GpuKernel.compile and run with dispatch; native.gpu and native.webgpu are ⚠ planned, and depend on native async.
  • Today, rely on the LLVM backend's auto-vectorization in --release builds.
  • Chapter 24 covers building and distributing native programs.