Rake

Rake 0.3.0-alpha.1 ALPHA

Write one data-parallel kernel for several CPU vector widths. Get the promised machine code—or a compiler error.

Alpha release. Version 0.3.0-alpha.1 coordinates the compiler, editor grammar, documentation, and this site. The source language and machine-level calling rules may change incompatibly before a stable release.

The vector unit Rake promises

Modern CPUs can apply one instruction to several numbers held together in a vector register, a model called SIMD. Rake calls one target-sized vector of values a rack, and each value position inside it a lane. A native Rake profile names a CPU target and the machine-code properties required for that target. Its central promise is that every live rack stays in one physical SIMD register. If the compiler's backend—the part that produces machine code—cannot preserve that representation, compilation stops with the failed obligation.

From one rack to a complete workload

Compute kernels often store each field in its own contiguous column, a layout called a structure of arrays. One rack is one target-sized chunk of one such column: four f32 positions on 128-bit SSE2 or Arm NEON, eight on 256-bit AVX2, or sixteen on 512-bit AVX-512. A stack declares the columns in the dataset. A pack connects that declaration to the complete columns and their element count. A for … using … up to … traversal visits the pack one rack at a time.

The caller supplies two contiguous f32 columns, one dense u8 age column, and a runtime count. The Particles stack below declares their columnar schema; the pack connects that schema to the complete 400-particle storage. On each iteration, particle.position_x and particle.velocity_x are racks containing the current target-width chunk. The narrower age column requires an explicit widen before computation. The source never names a CPU-specific vector function or lane count.

stack Particles {
  f32: position_x, velocity_x;
  u8: age;
}

run advance_x(
  particles: pack Particles,
  <count: i64>,
  <dt: f32>
) -> f32:
  for particle in particles using f32s up to <count>:
    let ages: u32s = widen(particle.age)
    yield particle.position_x + particle.velocity_x * <dt>

The notation exposes distinctions that affect the machine. pack Particles reads as “a pack of Particles”; using f32s names the native compute domain; and angle brackets mark count and dt as one uniform scalar shared by every lane. Seeing many scalar markers in a kernel should provoke the question: could these values vary by lane, or be stored and processed more efficiently?

Inside the stack, each stored type comes before all columns that share it. That makes memory density readable down the left edge: the two four-byte f32 columns are grouped separately from the one-byte u8 column. The plural spelling keeps the relationship compact: f32 is one stored element; f32s is one target-native rack of them.

Compiling the same source for a named profile chooses the rack width. Four hundred f32 values divide exactly, so no tail is needed:

Target profileFloat lanes per rack400-particle traversal
SSE2 / NEON4100 racks
AVX2850 racks
AVX-5121625 racks

The programmer does not maintain three target-specific vector kernels, three loop strides, and three ways to handle a final partial chunk. Rake instead produces a separate executable for each selected target. The choice is made while compiling, not by a width check each time the kernel runs.

What the profile proves for every rack

C and Rust can produce excellent SIMD. Rake additionally makes the required SIMD shape a language contract, so inspecting the emitted instructions is evidence of conformance rather than the only way to discover whether vectorization survived compilation.

Why rejection is a feature

Rust made a compelling modern case for a strict compiler: rejecting plausible programs can be more useful than accepting them when the diagnostic explains which safety rule the program violated and how to repair it. Rake applies that bargain to declared performance properties. A compute kernel is often written because its machine shape matters, so losing vectorization is a failed requirement rather than an incidental missed optimization.

Rake does not turn every performance surprise into a language error. The programmer opts into a named native profile with specific obligations. If an irregular access requires a forbidden gather from non-contiguous addresses, an operation requires scalar helper calls, or register pressure would spill a live rack, the compiler identifies the source operation and the failed obligation before the object ships. The programmer can change the algorithm or choose a profile whose published contract permits the required operation.

Rust's ownership diagnostics →

The useful failure is a compiler error

Consider a rack-wide sine operation when the backend has no compliant packed sine sequence. Ordinary C and Rust compilation can accept the source and emit scalar library calls. Rake refuses to describe that result as native rack execution.

C or Rust

The program compiles. Under the strict comparison build, the resulting loop contains scalar sinf calls. A different toolchain or flag set may make a different choice.

Rake

The parser and type checker understand sin(values), but machine-code emission stops before assembly.

call to 'sin' is not supported by native crunch lowering

The contract C, Rust, and SIMD crates do not provide

ApproachWhat remains the programmer's responsibility
C / Rust scalar loopConfirm that autovectorization—the compiler's automatic conversion of a scalar loop into vector operations—happened, used the intended width, avoided helper calls, and did not regress after a source or compiler change.
SIMD cratesPortable vector types can remove some target-specific syntax, but they do not ordinarily make Rake's combined one-register, no-spill, no-call, and no-replacement-with-scalar-operations contract a condition of compilation.
C / Rust intrinsicsMaintain target-width implementations and dispatch. Intrinsics are compiler-provided functions for requesting particular machine operations; the compiler still owns register allocation and may spill intermediate vectors.
Rake native profileSelect the target profile. The compiler either proves the advertised machine properties or rejects the program.
Current alpha boundary. These examples state the canonical source contract. The compiler capability report records how much of that contract a particular build implements. The AVX2 and NEON backends enforce the native-rack rules for supported register-level kernels; native run/pack traversal and the SSE2 and AVX-512 backends remain under construction.

Inspect and run the C, Rust, and Rake proof harness →

Masks make per-lane choices explicit

Ordinary rack arithmetic performs the same operation in every lane. Conditional work needs a way to say which lanes participate without turning the rack back into a scalar loop.

A tine names a boolean choice for every lane. A through block computes a candidate where that tine is true. A sweep selects the first matching candidate for each lane and requires a final catch-all value, so every lane has a result.

Rake writes a tine as #active. The hash resembles a perforated mask: values fall through its open lanes and are held back by the rest. The mark has one role wherever the mask is declared or used.

↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ logical rack lanes ════════════════════════════════════ | tine A |███░░░███░░░███░░░██████| named lane mask ════════════════════════════════════ ↓ ↓ ↓ ═══════════════════════════════════ | tine B |░░░███░░░░░░███░░░░░░███| another candidate ═══════════════════════════════════ ↓ ↓ sweep: < < total per-lane result

Fusion turns bindings into a verified dataflow region

A binding gives a computed value a name. A later reference tells the compiler to use that value again. Neither construct creates a pointer or requires a memory load. Physical storage is a separate compiler decision: a live value can occupy a register, or an ordinary compiler can temporarily write it to memory and reload it later. That temporary write is a spill.

crunch advance(positions: f32s, velocities: f32s) -> f32s:
  | scaled: f32s <| velocities * <0.5>
  | result: f32s <| positions + scaled
  return result

<| exists only in a fused-flow binding. Read it from right to left: the expression flows into the name. Consecutive fused-flow bindings form one pure, connected machine-level computation. Rake must keep that region inline, call-free, spill-free, and free of hidden scalar work, or reject the definition.

Every accepted rack already obeys Rake's one-register and no-spill rules, including racks introduced with an ordinary let. Fusion adds a stronger boundary around the whole chain: its operations must remain pure, connected, and contiguous through lowering. The compiler cannot interrupt the chain with memory operations or an unknown helper call.

scaled is a readable alias inside the fused graph. It creates no storage, evaluation point, or rounding boundary, so the compiler immediately sees the graph as positions + velocities * <0.5>. The backend then chooses the cheapest legal target operations. On AVX2 and AArch64 NEON, this graph becomes one native fused multiply-add and the dead multiply disappears.

The language contract also permits the backend to reassociate, factor, distribute, eliminate common expressions, and form other fused instructions when its target cost model finds a cheaper graph. The current alpha implements the first useful slice: transparent alias substitution, dead-intermediate removal, and multiply-add contraction. The broader algebraic optimizer remains under construction.

fma(a, b, c) is needed only when correctness specifically depends on one fused multiply-add operation with one rounding step. Rake provides no slower arithmetic mode for preserving instruction boundaries that ordinary operators did not request. Named strict reductions and scans retain their published ascending-lane order; fusion does not silently reassociate those operations.

Language syntax tour

These complete programs use the canonical source language. Rake reports each construct's implementation status separately: frontend acceptance establishes syntax and types, while a production target additionally requires instruction selection, register assignment, assembly, executable comparison, and inspection of the encoded machine code.

Crunch: uniform rack arithmetic

A crunch applies one expression to every lane. Here <0.5> is an explicit scalar broadcast. Each intermediate name is assigned once, which makes the data dependencies explicit and lets the compiler check the whole sequence as one fused region. The corresponding straight-line operation set is implemented by the Rake-owned x86-64 AVX2 backend and verified after assembly.

crunch advance(positions: f32s, velocities: f32s) -> f32s:
  | scaled: f32s <| velocities * <0.5>
  | result: f32s <| positions + scaled
  return result

Rake: safe masked work and a total sweep

The corresponding masked operation is verified by the AVX2 backend. The #valid tine guards sqrt, and the final _ arm defines all unmatched lanes. Native lowering explicitly replaces inactive operands with benign values before partial operations. The emitted function uses AVX2 vector masks and blends without scalar lane branches.

rake safe_root(values: f32s) -> f32s:
  tine #valid when values >= <0.0>

  through #valid else <0.0> into rooted:
    sqrt(values)

  return sweep:
    | #valid => rooted
    | _      => <0.0>

Run: stack, pack, and target-sized traversal

Canonical language contract; native traversal remains in flight. A stack groups columns by their compact storage types. A pack supplies the complete columns to run, and using f32s selects one physical rack and its logical lane count. up to <count> makes the uniform record bound explicit. A narrower column cannot enter arithmetic until widen converts its values while preserving that lane count. The frontend contract covers the declarations, widening, and loop shape. Native memory traversal, safe final partial racks, and the machine-level convention for calling the function from C—its ABI—are still under construction.

stack Samples {
  f32: value;
  u8: quality;
}

run scale_values(
  input: pack Samples,
  <count: i64>,
  <scale: f32>
) -> f32:
  for chunk in input using f32s up to <count>:
    let quality: u32s = widen(chunk.quality)
    yield chunk.value * <scale>

Rake and ISPC: two approaches to explicit lane-parallel code

Intel's ISPC is a mature C-like language for writing one program that runs across a group of CPU vector lanes. ISPC and Rake therefore belong to the same useful language family: both let programmers write one lane-parallel program and retarget it across hardware widths. ISPC presents this single-program, multiple-data (SPMD) model through an imperative C-family language. Rake explores an expression-oriented, dataflow-heavy syntax with a stricter compile-or-reject machine contract. More than one good design for this problem is an advantage for systems programmers.

A substantial shared foundation

The same operation in two syntaxes

Both examples update every particle in a structure-of-arrays dataset. ISPC traverses the columns in compile-time gangs, while Rake traverses them in target-sized racks.

ISPC: imperative SPMD

export void advance_x(
    uniform float position_x[],
    uniform float velocity_x[],
    uniform float output_x[],
    uniform int count,
    uniform float dt) {
  foreach (i = 0 ... count)
    output_x[i] = position_x[i]
                + velocity_x[i] * dt;
}

Rake: expression-oriented dataflow

stack Particles {
  f32: position_x, velocity_x;
}

run advance_x(
  particles: pack Particles,
  <count: i64>,
  <dt: f32>
) -> f32:
  for particle in particles using f32s up to <count>:
    yield particle.position_x
      + particle.velocity_x * <dt>

ISPC keeps the familiar loop, index, assignment, pointer, and C interoperability model. Values inside the gang are varying unless marked uniform. Rake moves the traversal and columnar schema into for … using … up to …, pack, and stack, while angle brackets mark scalar values. Rake's core vector forms favor pure expressions and SSA-like bindings, although Rake is not defined as a purely functional language.

Deliberate differences

Design choiceISPCRake
Syntax and mental modelImperative C-family SPMD with loops, assignments, pointers, functions, and familiar control flow.Expression-oriented vector dataflow with explicit scalar markers, fused bindings, tines, through blocks, and total sweeps.
Parallel unitA gang of program instances. The selected gang may be wider than one native CPU vector.A CPU rack is exactly one native vector register; the proposed GPU mapping is described in the next section.
SoA modelsoa<n> creates an explicitly n-wide SoA representation and supports layout conversion.stack declares columns; pack supplies runtime storage and count; the target profile selects the traversal width.
Divergence vocabularyOrdinary if, loops, and function calls acquire uniform or varying behavior from their operands.Named tines expose masks; safe through blocks produce candidates; a total sweep selects each lane's result.
CPU machine contractISPC preserves gang semantics while the LLVM compiler infrastructure owns instruction selection and register allocation.Rake additionally rejects splitting, scalar lane loops, rack spills, and helper calls wherever the selected profile forbids them.
Backend strategyLLVM-based native CPU compilation plus documented Intel Xe GPU targets.Rake-owned CPU instruction selection and allocation; a proposed cross-vendor GPU backend with a separate driver trust boundary.
MaturityEstablished production compiler, broad language, C/C++ interoperability, tasking, libraries, tools, and deployed CPU/GPU targets.Experimental alpha with verified AVX2 and NEON register-level slices; pack traversal, broader CPU profiles, and GPU emission remain unfinished.

ISPC language and target guide →    ISPC for Intel Xe →

ISPC proves the model in production

ISPC is neither abandoned nor a mainstream application language. It is a successful specialist compiler used for performance-critical kernels beside larger C and C++ systems. OSPRay's CPU renderer is based on ISPC and uses SSE4, AVX, AVX2, AVX-512, and Arm NEON. Embree supports ISPC for parallel renderer code, and CMake has recognized ISPC as a compilation language since version 3.19.

That placement explains the small public profile. An application developer encounters OSPRay or Embree while the .ispc files remain an implementation detail. Rendering and scientific visualization also produce less general-language discussion than CUDA, shader languages, C++ intrinsics, or Rust SIMD libraries. Intel's name can suggest a narrower hardware target than ISPC's CPU support, which includes x86 and Arm.

OSPRay CPU implementation →    Embree ISPC support →    CMake ISPC language support →

Where Rake can still differ

Rake does not claim to have invented portable SPMD-on-SIMD. ISPC established that one lane-parallel program can be productive across CPU vector widths, and its mature language, tooling, interoperability, and deployments are substantial advantages.

ISPC guarantees the program's SPMD semantics and generally emits excellent vector code. Its performance guide nevertheless documents cases where irregular accesses require non-contiguous vector loads and stores called gathers and scatters, or one-at-a-time serialized loads. Those are legitimate implementations of ISPC semantics. Rake's narrower contribution is to let a selected profile forbid such implementations and turn the physical lowering into an acceptance condition.

ISPC performance guide →

ISPC proves that this programming model is useful. Rake explores a more functional and compositional member of the same family, with explicit pack/rack and SoA structure and machine-shape violations reported as compiler errors. Choose ISPC today for a mature C-family SPMD language, excellent CPU SIMD, close C/C++ integration, and shipped Intel GPU support. Rake's stricter CPU contract is currently implemented only for its admitted AVX2 and NEON slices. Pack traversal, broader CPU profiles, and the cross-vendor GPU proposition remain unfinished.

GPU compute: where the guarantee ends

Rake can carry its fail-closed philosophy into GPU compute, but the CPU promise cannot be copied word for word. Vulkan runs many copies, or shader invocations, of a GPU program. A subgroup is a set of those invocations that execute together and exchange values directly. A CPU rack is one value held in one SIMD register; a GPU rack would be one logical subgroup, with one lane owned by each invocation.

SPIR-V defines the portable boundary

On a CPU, Rake controls instruction selection, register assignment, textual assembly, and the resulting native object file. For Vulkan GPUs, Rake would instead emit SPIR-V, a portable binary program that a hardware vendor's driver later translates into device-specific instructions. Rake can therefore verify the SPIR-V module and its declared requirements, but the driver owns the final GPU instructions and register allocation.

Rake source semantics │ ├── CPU profile ── Rake machine form ── register allocation ── assembly ── native object │ Rake controls and verifies ────────────────┘ │ └── GPU profile ── validated SPIR-V ── Vulkan driver ── device-specific GPU code Rake proves ───┘ driver controls ────────────────┘

A GPU rack maps to a subgroup

A SPIR-V vector is normally a small two-, three-, or four-component value inside one shader invocation. A Vulkan subgroup is a set of invocations that can exchange data and execute collective operations efficiently. Hardware commonly executes those invocations together as a wave or warp, although Vulkan defines subgroup behavior rather than a vendor instruction format.

A Rake GPU profile should map one logical rack lane to one subgroup invocation. Rack addition then becomes one scalar addition in every invocation. A rack reduction becomes a SPIR-V subgroup reduction. A shuffle becomes a subgroup shuffle. Tines become explicit per-invocation predicates. Treating a 32-lane rack as OpTypeVector 32 would describe the wrong execution model.

PropertyCPU rackGPU rack
Physical interpretationOne native SIMD registerOne logical subgroup spread across shader invocations
Lane storageOne component of the registerOne scalar value owned by each invocation
Width selectionElement type and CPU profileRequired subgroup size in the Vulkan pipeline profile
Portable evidenceRegister-assigned program and encoded instructionsValidated SPIR-V, declared capabilities, and pipeline requirements
Final register allocationOwned by RakeOwned by the Vulkan driver

How 400 particles traverse GPU racks

The dataset contains 400 particle elements. One GPU rack covers one logical subgroup's worth of those elements, with one element assigned to each active invocation. A vulkan-subgroup32 profile therefore traverses the dataset as twelve complete racks and one rack with sixteen active invocations. A vulkan-subgroup64 profile uses six complete racks and one with sixteen active invocations. Vulkan dispatch is rounded to the configured workgroup size, the larger programmer-declared batch that may contain several subgroups. Rake's generated bounds predicate prevents padded invocations from touching memory or performing unsafe partial operations.

Selected profileLogical rack width400-particle decomposition
x86-avx28 values50 complete racks
vulkan-subgroup3232 invocations12 complete + 16 active
vulkan-subgroup6464 invocations6 complete + 16 active

What Rake could guarantee before the driver

A named Vulkan profile would include a SPIR-V version, required Vulkan features, subgroup width, floating-point modes, memory model, and permitted operation set. Pipeline creation would check the physical device and reject a profile whose required subgroup size or capabilities are unavailable.

What portable SPIR-V cannot guarantee

Vulkan drivers compile SPIR-V into device-specific executables during pipeline creation. The driver may change instruction selection, schedule operations, allocate registers, or spill values. Consequently a portable Rake SPIR-V profile cannot promise any of the following properties solely from the emitted module:

A stronger device-certified mode

Rake could recover a stronger guarantee for a named physical device and driver. A certification tool would create the Vulkan pipeline, capture executable statistics and any exposed internal representation, and apply a vendor-specific checker for spills, register use, helper code, and prohibited instructions. The certificate would bind the SPIR-V hash, pipeline state, device identity, driver version, subgroup size, and checker version.

Vulkan exposes pipeline executable statistics and internal representations through an optional developer-tools extension. Those representations are implementation-dependent and may include final shader assembly. A missing extension or unavailable assembly would make certification fail rather than weaken the claim. The resulting certificate would prove properties for one device and driver combination; it would not extend to every device that accepts the SPIR-V module.

Why use Rake for GPU kernels?

Shader languages such as GLSL and HLSL, as well as ISPC and other compilers, can emit excellent SPIR-V, including the subgroup operations described above. Rake's prospective advantage is the compilation contract around those operations: the same lane semantics used for CPU racks, explicit SoA traversal and predication, named reproducible GPU profiles, and rejection whenever the emitted SPIR-V cannot preserve the selected profile.

Rake's combined contract would let one kernel express CPU SIMD and GPU subgroup execution without relying on CPU autovectorization or accepting silent GPU emulation. The advantage comes from validation and refusal. SPIR-V already has the necessary expressive capability.

Proposed GPU contract. Rake does not currently have a production SPIR-V backend, Vulkan runtime profile, or device-certification pipeline. The GPU contract becomes a real advantage only after those components have executable semantics, negative tests, SPIR-V inspection, device fixtures, and published evidence.

SPIR-V specification →    Vulkan subgroup model →    Pipeline executable inspection →

The wider language landscape

ISPC is Rake's closest established relative. Several neighboring systems solve parts of the CPU-and-GPU problem at a different level of abstraction or with a different machine contract.

Language familyWhat it providesHow its contract differs
HalideA functional, C++-embedded language for image and stencil pipelines with separate algorithm and scheduling choices across CPUs and GPUs.Vector widths, tiling, placement, and other scheduling decisions are supplied by a programmer or autoscheduler rather than certified as Rake-style physical rack obligations.
FutharkA statically typed, purely functional array language that compiles data-parallel programs to CUDA, OpenCL, or multithreaded CPU code.Its central abstraction is bulk and nested array parallelism rather than one target-sized rack that must remain one CPU register.
CUDA, HIP, shader languages, and SlangGPU-oriented SPMD or SIMT programming over threads, warps, waves, and subgroups, with targets such as portable SPIR-V, Microsoft's DXIL format, and native GPU code.They expose the GPU execution model well, but do not make one source construct a portable CPU SIMD register with Rake's native fail-closed contract.
TaichiA Python-embedded, data-oriented DSL that compiles parallel kernels for multicore CPUs and GPUs.It concentrates on portable parallel iteration and data layout rather than proof of a fixed physical SIMD representation.
MojoA systems and AI language with explicit parameterized SIMD values, vectorized algorithms, and CPU/GPU facilities.The vector width is part of the SIMD type, and Mojo documents that an oversized value may be split across registers. Rake's rack width comes from the profile and splitting is a rejection condition.
C++ and Rust SIMD librariesPortable vector types and algorithms reduce direct use of architecture-specific intrinsics.They remain library interfaces and do not ordinarily combine one-register identity, no spills, no helper calls, and no scalar lane loops into a compiler acceptance rule.
Research array and staging languagesImpala/AnyDSL, Lift/Shine, Accelerate, and Dex explore staged compilation, rewrite systems, and functional data-parallel programming.They are valuable prior art, but none is a widely deployed direct replacement for ISPC's C-family SPMD-on-SIMD niche.

What this alpha ships

The language contract includes native-backed operations, forms whose implementations stop in the frontend, and targets that remain design work. The table identifies each implementation boundary in version 0.3.0-alpha.1.

Area0.3.0-alpha.1 status
Native CPURake-owned x86-64 AVX2 and AArch64 NEON backends for supported crunch and predicated rake programs, including uniform 32-bit floating-point (f32) parameters, no-spill allocation, textual assembly, and post-assembly verification
Language contractrun, pack traversal, stacks, packs, and explicit widening are canonical source constructs; the capability report identifies the frontend and native subset implemented by this build
Other targetsAVX-512, SSE2, and GPU native backends remain unimplemented
C boundaryThe tested alpha crunch boundary uses the eight vector-argument registers provided by the System V x86-64 calling convention; no production run wrapper or stable cross-release ABI yet
PerformanceNo representative published benchmark or generated-code performance claim
Future syntaxSome forms parse for design work but fail with source-located WIP: diagnostics

The compiler's --print-capabilities report describes the parser and type checker's accepted language surface. The native backend rejects every construct it cannot yet lower without violating rack identity, mask safety, fusion, or no-spill allocation. Native crunches and predicated rakes substitute fused aliases and optimize ordinary multiply-add graphs into native FMA instructions. Source-level fma(...) requires the same one-rounding arithmetic when correctness depends on it. Broader reassociation, factoring, distribution, and common-expression optimization remain roadmap work. AVX2 also supports strict f32 reductions, which combine all lanes into one value, and inclusive scans, which produce every running partial result. Records, tuples, lane rearrangement, lambdas, and expression pipelines remain unavailable.

Install

Rake is written in the OCaml programming language and built with its Dune build system. The repository uses Nix to provide pinned versions of those tools and the system assembler. These commands build rakec and verify one supported crunch through the native AVX2 object-code gate.

$ git clone https://github.com/rakelang/rake
$ cd rake && nix develop --command dune build
$ nix develop --command dune exec rakec -- --version
$ nix develop --command dune exec rakec -- --verify-native --target x86-avx2 test/native/docs_crunch.rk