Sarek_ir_typesSarek_ir_types - Pure type definitions for GPU kernel IR
This module contains only type definitions with no external dependencies. Used by spoc_framework for typed generate_source signature.
module Type_id : sig ... endRuntime type identities with equality proofs.
type elttype = | TInt32| TInt64| TFloat16IEEE binary16 storage type. Values are stored/loaded as binary16; arithmetic promotes to TFloat32, computes there, and rounds back on store. There is deliberately no CFloat16 constant: f16 values are produced by conversion (ECast (TFloat16, _)), never by a literal.
| TFloat32| TFloat64| TUint8Unsigned 8-bit integer storage type (backlog-62 slice 3).
It exists for one reason and its scope is deliberately that narrow: it is the element type of a cooperative-matrix OPERAND BUFFER. Every one of the twelve integer configurations the local RX 7900 XTX advertises has 8-bit operands with a 32-bit accumulator, and coopMatLoad requires the backing array's element type to MATCH the fragment's component type — so there is no route to an integer fragment through a wider buffer, and no way to reach the strict-contract tensor-core path without an 8-bit element type in the IR.
There is no arithmetic on it. No binop, no literal, no cast to or from it is emitted by any backend; a TUint8 value reaches a kernel only by being read by Sarek_ir_types.coopmat_op.CM_load and leaves only by Sarek_ir_types.coopmat_op.CM_store. That is not an oversight to be filled in later — widening it into a general arithmetic type is a separate decision with its own promotion and overflow questions, and this slice measured none of them.
| TBool| TUnit| TRecord of string * (string * elttype) listRecord type: name and field list
*)| TVariant of string * (string * elttype list) listVariant type: name and constructor list with arg types
*)| TArray of elttype * memspaceArray type with element type and memory space
*)| TVec of elttypeVector (GPU array parameter)
*)Element types
Variables with type info
type expr = | EConst of const| EVar of var| EBinop of binop * expr * expr| EUnop of unop * expr| EArrayRead of string * exprarridx
| EArrayReadExpr of expr * exprbase_expridx for complex bases
| ERecordField of expr * stringr.field
*)| EIntrinsic of string list * string * expr listmodule path, name, args
*)| ECast of elttype * expr| ETuple of expr list| EApp of expr * expr list| ERecord of string * (string * expr) listRecord construction: type name, field values
*)| EVariant of string * string * expr listVariant construction: type name, constructor, args
*)| EArrayLen of stringArray length intrinsic
*)| EArrayCreate of elttype * expr * memspaceelem type, size, memspace
*)| EIf of expr * expr * exprcondition, then, else - value-returning if
*)| EMatch of expr * (pattern * expr) listscrutinee, cases - value-returning match
*)Expressions (pure, no side effects)
type stmt = | SAssign of lvalue * expr| SSeq of stmt list| SIf of expr * stmt * stmt option| SWhile of expr * stmt| SFor of var * expr * expr * for_dir * stmt| SMatch of expr * (pattern * stmt) list| SReturn of expr| SBarrierBlock-level barrier (__syncthreads)
*)| SWarpBarrierWarp-level sync (__syncwarp)
*)| SExpr of exprSide-effecting expression
*)| SEmpty| SLet of var * expr * stmtLet binding: let v = e in body
*)| SLetMut of var * expr * stmtMutable let: let v = ref e in body
*)| SPragma of string list * stmtPragma hints wrapping a statement
*)| SMemFenceMemory fence (threadfence)
*)| SBlock of stmtScoped block - creates a C scope for variable isolation
*)| SNative of {gpu : framework:string -> string;Generate GPU code for framework
*)ocaml : ocaml_closure;Typed OCaml fallback
*)}Inline native GPU code with OCaml fallback
*)| SCoopmat of coopmat_opA cooperative-matrix (tensor-core) operation — backlog-62 slice 3.
Why ONE statement constructor carrying an operation family, rather than four constructors. Seventeen places in this repository match exhaustively on stmt, and most of them are backends whose only correct response to any of these operations is the same refusal. Four constructors would be sixty-eight arms to write and to keep in agreement; one is seventeen, and a backend that handles SCoopmat at all is then forced by the compiler to consider every member of coopmat_op in one place where the four cases sit next to each other.
Why fragments are NOT vars and NOT an elttype. A fragment is a subgroup-cooperative value: the whole subgroup collectively holds rows * columns components and each invocation holds a few of them at an implementation-defined position. It cannot be indexed, assigned to, added, cast, passed to a helper, or stored in an array. Giving it an elttype would make every one of those spellable in the IR and would oblige ~36 exhaustive elttype matches to invent an answer for a type none of them can represent. Fragments therefore live in their own namespace, addressed by name, and the only things that can be done to one are the four below.
Statements (imperative, side effects)
and coopmat_op = | CM_decl of {name : string;frag : Sarek_coopmat_types.fragment;}Bring a fragment into scope for the rest of the enclosing block.
Statement-level rather than a scoping form like SLet, because GLSL, MSL and C all admit a declaration in the middle of a block and because D = A * B + C wants four fragments live at once — nesting four SLet-shaped binders to express that is noise with no invariant behind it.
| CM_load of {dst : string;frag : Sarek_coopmat_types.fragment;src : string;index : expr;stride : expr;}Fill dst from the buffer src, row-major, starting at element index, with stride elements between consecutive rows.
frag is repeated here rather than looked up from the CM_decl: a codegen backend must be able to emit this statement without carrying a fragment environment, and an interpreter must be able to CHECK the two agree. A single source of truth that every consumer has to reconstruct is not a single source of truth.
Column-major is deliberately absent. It is one more enumerant in GLSL, but it is a second layout to verify on hardware and this slice measured only row-major — an emitted layout nothing has executed is a claim without evidence.
*)| CM_store of {src : string;frag : Sarek_coopmat_types.fragment;dst : string;index : expr;stride : expr;}| CM_muladd of {dst : string;a : string;b : string;c : string;cfg : Sarek_coopmat_types.config;}dst = a * b + c, the tensor-core instruction itself.
cfg is the whole point of carrying a configuration rather than four fragments: it is what the device gate is keyed on, it is what says whether the accumulation SATURATES (a property of the operation and not of any operand), and it is what Sarek_coopmat_types.accumulation_is_exact reads to decide whether this statement is under the strict contract or needs the relaxation of docs/design/f16-relaxed-accuracy.md §1.6.
The four things that can be done with a cooperative-matrix fragment.
Fragment names live in a namespace of their own, separate from var. They are plain strings for the same reason an SShared array name is: a fragment is not an l-value, cannot be captured, and cannot escape the kernel body, so there is nothing for a var's mutability or type field to carry that Sarek_coopmat_types.fragment does not already say.
Declarations
Helper function (device function called from kernel)
and native_arg = | NA_Int32 of int32| NA_Int64 of int64| NA_Float32 of float| NA_Float64 of float| NA_Vec of native_vecNative argument type for kernel execution. Typed arguments with runtime type witnesses - used by PPX-generated native functions.
and ('elt, 'underlying) native_vec_ops = {length : int;elem_size : int;type_name : string;type_id : 'elt Type_id.t;get_f32 : int -> float;set_f32 : int -> float -> unit;get_f64 : int -> float;set_f64 : int -> float -> unit;get_i32 : int -> int32;set_i32 : int -> int32 -> unit;get_i64 : int -> int64;set_i64 : int -> int64 -> unit;get_typed : int -> 'elt;set_typed : int -> 'elt -> unit;underlying_type_id : 'underlying Type_id.t;underlying : 'underlying;}and ocaml_closure = {run : block:(int * int * int) ->
grid:(int * int * int) ->
native_arg array ->
unit;}val vec_get_custom : 'a. 'a Type_id.t -> native_arg -> int -> 'aGet element from NA_Vec as a type checked custom value.
val vec_set_custom : 'a. 'a Type_id.t -> native_arg -> int -> 'a -> unitSet element in NA_Vec from a type checked custom value.
val vec_length : native_arg -> intGet length from NA_Vec
val vec_as_vector : 'a. 'a Type_id.t -> native_arg -> 'aGet the checked underlying vector/buffer value.
type native_fn_t = | NativeFn of parallel:bool ->
block:(int * int * int) ->
grid:(int * int * int) ->
native_arg array ->
unitNative function type for V2 execution. Uses typed native_arg.
type kernel = {kern_name : string;kern_params : decl list;kern_locals : decl list;kern_body : stmt;kern_types : (string * (string * elttype) list) list;Record type definitions: (type_name, (field_name, field_type); ...)
kern_variants : (string * (string * elttype list) list) list;Variant type definitions: (type_name, (constructor_name, payload_types); ...)
kern_funcs : helper_func list;Helper functions defined in kernel scope
*)kern_native_fn : native_fn_t option;Optional pre-compiled native function for CPU execution
*)}Kernel representation
val default_kernel : kernelA kernel with every field at its empty value, for use as the base of a record update: {default_kernel with kern_name = "k"; kern_body = b}.
WHY THIS EXISTS. OCaml requires every field at every record literal, so adding one field to kernel used to mean editing all 119 construction sites in the tree — which is why the type has been avoided rather than extended. A record UPDATE names only the fields it sets, so once a site is written this way a new field costs it nothing.
Prefer this over spelling out the empty fields. make_kernel is the same thing with labels, for new code that would otherwise set most fields.
val make_kernel :
?params:decl list ->
?locals:decl list ->
?types:(string * (string * elttype) list) list ->
?variants:(string * (string * elttype list) list) list ->
?funcs:helper_func list ->
?native_fn:native_fn_t ->
name:string ->
body:stmt ->
unit ->
kerneldefault_kernel with labels. ~name and ~body are required because a kernel with neither is not a kernel; everything else defaults to empty.