Sarek_codegen.Sarek_ir_glslmodule Codegen_error : sig ... endLocal error module — same raised exception as the package-level Vulkan_error.
module Dispatch = Sarek_ir_intrinsic_dispatchRaise a located invalid-argument-count error (atomic-arity helper for the shared Dispatch.emit_atomic).
type state = {variants : (string * (string * Sarek_ir_types.elttype list) list) list;smod_name : string;copysign_name : string;fmod_name : string;f64_helpers : Sarek_ir_types.helper_func list;needs_int64 : bool;f64_needs_copysign : bool;helper_vec_param_indices : (string, int list) Stdlib.Hashtbl.t;}Everything one run of generate_with_types needs to know that is not reachable from the IR node it is currently emitting. It is a VALUE threaded through the emit functions as their first argument, not module state, and that is the whole point of backlog-185/200: these eight fields were seven module-level refs and one module-level Hashtbl.t. Being module-level made the emitter non-reentrant in two ways. Two generations running at once (two domains, or a nested call) interleaved their writes, so one read the other's helper names, variants and vec-param indices. And any caller entering below generate_with_types — gen_expr or gen_stmt directly, as the unit tests do — saw whatever the last full generation happened to leave behind. Sequential whole-kernel generation was the one safe case, and only because generate_with_types overwrote all seven refs and cleared the table on entry; that reset is what this record makes unnecessary.
Every field is per-generation. Seven are IMMUTABLE, computed once by state from the kernel before any byte is emitted:
variants — the kernel's own kern_variants. Read at exactly one site: the find_constr_types lookup in gen_stmt's SMatch arm, recovering a constructor's payload types so the binders can be declared. The EMatch arm needs no such lookup — it rewrites its binders through Sarek_ir_codegen.subst_ematch_payloads, which derives the payload field names from the layout and never consults the variant table.smod_name — name of the integer-remainder helper (sarek_smod by default), from compute_smod_name, chosen so it cannot collide with a user identifier. Both the declaration (gen_smod_helper) and the call site (gen_expr's Mod arm) read this one field, so they cannot disagree.copysign_name — same for the sign-copy helper (sarek_copysign), from compute_copysign_name: declared by gen_copysign_helper, called from the copysign arm of glsl_backend's pre_hook.fmod_name — same for the C-fmod helper (sarek_fmod), from compute_fmod_name. GLSL's mod() builtin is floor-based (divisor-signed remainder), NOT the truncated C fmod the Float32.fmod/Float64.fmod intrinsics contract for, and GLSL has no fmod builtin — so both spellings lower to this helper, declared by gen_fmod_helper and called from the fmod arm of pre_hook.f64_helpers — the software f64-transcendental helper family (Sarek_ir_softmath) this kernel needs, transitively closed over cross-calls, from compute_f64_softmath. GLSL core has no double-precision overload for sin/cos/exp/log/pow/… so Float64 transcendentals lower to calls into this family, emitted forward-declared in the preamble by gen_f64_softmath_helpers. Empty for kernels with no f64 transcendental.needs_int64 — whether this generation needs 64-bit integers, gating the GL_ARB_gpu_shader_int64 extension line. Three independent triggers, all ORed together by compute_f64_softmath: a member of f64_helpers that manipulates exponent/mantissa fields via f64_bits/bits_f64, a non-finite f64 constant (spelled int64BitsToDouble), or the user's own int64. The pure-polynomial softmath members (sin, cos, tan, atan, atan2) contribute nothing.f64_needs_copysign — whether any member of f64_helpers calls copysign (sinh/tanh/atan/atan2/asin/acos). The kernel IR carries no copysign node in that case, so Sarek_ir_analysis.kernel_uses_copysign would miss it; this field forces the helper to be emitted anyway so the softmath bodies find it.The eighth, helper_vec_param_indices, is the one field that is still MUTABLE, because it is not derivable up-front: it maps a helper's name to the indices of its vector parameters, is written as each helper is emitted (gen_helper_func) and read later in the same generation at the EApp call sites (gen_expr) that must drop those arguments — in GLSL a buffer array cannot be passed as a function parameter. It is a FRESH table allocated per generation by state, not one table shared by every generation: nothing outside the state value that generate_with_types built can reach it, and it dies with that value. That is why there is no Hashtbl.clear anywhere — there is never anything left over from a previous kernel to clear.
The software f64-transcendental helpers (Sarek_ir_softmath) are named __sarek_f64_<fn> — a valid PTX identifier, but GLSL reserves identifiers containing consecutive underscores (GLSL 4.5 §3.7), so glslang warns and a stricter compiler may reject them. Re-spell the leading __ as a single sarek_f64_ prefix for GLSL emission only; the shared IR (and thus PTX output) keeps the __sarek_f64_* names unchanged. Applied at every point GLSL emits such a name — the EVar cross-calls and call sites (via escape_glsl_name) and the function definitions/prototypes. Lookup keys (helper_by_name, the vec-param table) stay on the raw name.
Escape reserved GLSL keywords by adding 'v' suffix (avoids double underscore with the _length suffix of glsl_array_length_name), and re-spell software-transcendental helper names (mangle_softmath_ident).
The GLSL identifier carrying the length of vector parameter name.
One definition, deliberately. This name has two halves that must agree: gen_push_constants declares it (a push-constant field plus a #define alias), and gen_expr uses it for EArrayLen. Before backlog-156 those were two separate string constructions in this same file — <name>_len on the declaring side, sarek_<name>_length on the using side — and nothing compared them, so every kernel taking a length emitted GLSL naming an identifier the shader never declared. Both halves go through here so the disagreement is not expressible.
The shadowing sets that rename_pc_shadowing_locals matches against are the same name and would be a third place to spell it, so they do not spell it either: param_macro_names calls this, and generate and generate_with_types both call param_macro_names.
The spelling is the cross-backend one: CUDA, OpenCL, Metal and WGSL all call this sarek_<name>_length, and so does PTX, via Sarek_ir_ptx_types.length_param_name — five of the six emitters, with GLSL the odd one out until now. The sarek_ prefix also keeps the identifier out of the user namespace, unlike the old bare <name>_len.
val glsl_type_of_elttype : Sarek_ir_types.elttype -> stringMap Sarek IR element type to GLSL type string
Format an f64 literal exactly as gen_expr's EConst (CFloat64 _) arm does (the GLSL lowercase lf double suffix), for the composition constants used by the exp2/log2/cbrt lowerings below.
Root Sarek_ir_softmath helpers a Float64 transcendental name needs, or None if name has a native GLSL f64 builtin (sqrt/floor/…/fma/min/max) or is not a transcendental. The direct cases resolve through Sarek_ir_softmath.helper_name; exp2/log2/cbrt have no dedicated helper and are lowered by composition over exp/log/pow, so their roots are those. This is the single source of truth shared by the call-site emission (gen_f64_transcendental) and the per-generation family closure (compute_f64_softmath) — they must agree on which names are routed.
The Float64 scalar conversion intrinsics that every implementation agrees on: of_int, of_int32, of_float32, to_int32.
They are declared in Sarek_float64/Float64.ml and type-check in the DSL, but had no GLSL arm at all, so any kernel using one died with "Unknown intrinsic" — notably Float64.of_int32, which is the conversion user code reaches for first since int32 is the DSL's integer type.
Two are DELIBERATELY ABSENT. to_int and to_float32 are declared with device templates that round/truncate while their ocaml field — which is what Sarek_float64_native.ml mirrors and executes — is Stdlib.int_of_float (63-bit) and the IDENTITY respectively. So three implementations already answer differently, and adding GLSL arms would make that disagreement reachable on another backend rather than resolve it. Deciding those two semantics is tracked separately.
This lives in pre_hook rather than arm because the module path is needed to scope it, and only pre_hook receives it. The scoping is deliberate: the match is on bare NAME, and pre_hook runs BEFORE the path-qualified pure registry, so without the is_f64 guard any module declaring of_int or to_int32 would be silently captured here and lowered as a GLSL constructor cast, shadowing its own device template. Float32 declares both names today.
val gen_expr : state -> Stdlib.Buffer.t -> Sarek_ir_types.expr -> unitval gen_binop : Sarek_ir_types.binop -> stringval gen_unop : Sarek_ir_types.unop -> stringval gen_glsl_polyfill :
state ->
Stdlib.Buffer.t ->
is_f64:bool ->
string ->
Sarek_ir_types.expr list ->
unitGLSL has no cbrt/hypot/expm1/log1p/log10 builtins under any name (unlike fabs/rsqrt/atan2, which are simple renames — see Sarek_pure_registry.glsl_override_name). These need a multi-token expression instead of a function-name substitution, so they're special-cased here ahead of both the unqualified match arms and the pure registry, applying uniformly to qualified (Float32.cbrt) and unqualified calls alike. cbrt uses sign(x)*pow(abs(x),...) rather than bare pow because GLSL's pow is undefined for a negative base. log10 is derived from the natural log builtin: GLSL exposes log (base e) and log2 but no base-10 form, so log10(x) = log(x)/log(10). Routing log10 through here (ahead of the pure registry) is required: log10 IS present in the pure-registry float32/float64 tables and would otherwise emit the raw un-suffixed log10(...) that glslang rejects — the same latent-invalid-GLSL class as fabs/copysign (#246/#256).
is_f64 carries the operand precision (derived from the intrinsic path — a Float64 component). It governs the precision of any numeric LITERAL that a builtin then computes on: on the double route the literal must carry the GLSL double suffix (GLSL 4.5 §4.1.4), otherwise it defaults to float and pins the result to single precision even where the double overload of the surrounding builtin exists. The suffix is spelled lowercase lf to match the generator's own EConst (CFloat64 _) output (see gen_expr); GLSL accepts lf and LF interchangeably, so a single casing keeps every double literal in the emitted shader uniform. This bites exactly the two polyfills that feed a literal into a transcendental / irrational computation:
log10: log(10.0) — an irrational, evaluated by the float log overload; log(10.0lf) uses the double overload.cbrt: the exponent 1.0/3.0 — a non-terminating fraction; 1.0/3.0 is a float division (~7 digits), 1.0lf/3.0lf a double one. hypot (no literal), expm1 and log1p (their only literal is 1.0, which is exactly representable and promotes to double losslessly, and is never itself the argument of a builtin) are precision-safe as-is and left byte-for-byte unchanged so their existing goldens do not move.val gen_f64_transcendental :
state ->
Stdlib.Buffer.t ->
string ->
Sarek_ir_types.expr list ->
unitLower a Float64 transcendental (identified by f64_root_helpers) to the software Sarek_ir_softmath family. The direct cases emit a call to the reserved __sarek_f64_<name> helper; exp2/log2/cbrt have no dedicated helper and are composed over exp/log/pow (matching PTX's own exp2/log2 handling and the f32 cbrt polyfill shape, but with the f64 pow). The helper bodies themselves are emitted, forward-declared, in the preamble by gen_f64_softmath_helpers, gated on this generation's st.f64_helpers (see state).
val glsl_backend : state -> Sarek_ir_types.expr Dispatch.specval gen_lvalue : state -> Stdlib.Buffer.t -> Sarek_ir_types.lvalue -> unitval gen_match_pattern :
Stdlib.Buffer.t ->
string ->
string ->
string ->
string list ->
(string -> Sarek_ir_types.elttype list option) ->
unitGenerate match case pattern with variable bindings
val gen_var_decl :
state ->
Stdlib.Buffer.t ->
string ->
string ->
Sarek_ir_types.elttype ->
Sarek_ir_types.expr ->
unitGenerate variable declaration with optional initialization
val gen_array_decl :
state ->
Stdlib.Buffer.t ->
string ->
string ->
Sarek_ir_types.elttype ->
Sarek_ir_types.expr ->
unitGenerate array declaration
The GLSL surface is GL_KHR_cooperative_matrix: a coopmat<T, scope, rows, cols, use> type, coopMatLoad / coopMatStore against a storage buffer, and coopMatMulAdd. Every one of these functions is total on the IR it is given and raises where the IR admits something this extension cannot spell, rather than emitting text that glslang would reject with a message about a line the user never wrote.
val glsl_coopmat_component : Sarek_coopmat_types.component_type -> stringThe GLSL scalar type of a fragment component.
Deliberately NOT routed through glsl_type_of_elttype: the IR element types and the cooperative-matrix component types are different alphabets that happen to overlap, and there is no elttype at all for s8, u32 or f16-as-a-coopmat-component. Mapping them through a shared function would force one of the two to grow constructors it has no other use for.
val glsl_coopmat_scope : Sarek_coopmat_types.scope -> stringval glsl_coopmat_use : Sarek_coopmat_types.use -> stringval glsl_coopmat_type : Sarek_coopmat_types.fragment -> stringThe full coopmat<...> type of a fragment.
Rows and columns come from Sarek_coopmat_types.fragment_dims, which DERIVES them from the use and the shape (A is m x k, B is k x n, an accumulator is m x n). Emitting m and n here regardless of use — the obvious wrong version — produces a shader that compiles and computes nonsense on any non-square shape, which is exactly the class of defect the derived dimensions exist to make unspellable.
val gen_coopmat_op : state -> Sarek_ir_types.coopmat_op -> stringval gen_stmt :
state ->
Stdlib.Buffer.t ->
string ->
Sarek_ir_types.stmt ->
unitval rename_pc_shadowing_locals :
pc_names:string list ->
len_names:string list ->
Sarek_ir_types.stmt ->
Sarek_ir_types.stmtAlpha-rename kernel-body binders whose name collides with a push-constant macro.
Scalar params are exposed to the body as preprocessor macros (#define width pc.width), and each vector param exposes a length macro (#define sarek_v_length pc.sarek_v_length); a macro rewrites every matching token in main — including the declared name of a local. A helper inlined at its call site (e.g. a [@sarek.module] function whose formal is named like a kernel scalar) emits a self-binding int width = width;, which the macro turns into int pc.width = pc.width; — a syntax error ("unexpected DOT"). Helper functions are guarded too, but only partly by the #undef/#define dance in gen_helper_func: that dance is computed from PARAMETER names alone, so a helper whose BODY declares a local named like a scalar kernel param was never covered. That is why gen_helper_func now runs this pass over every helper body it emits — on GLSL the symptom is a hard glslang rejection, and on the WGSL twin (which has no macros but does substitute) it is a silently wrong value.
Both scalar-param macros (pc_names) and vector-length macros (len_names) are treated as collisions; a colliding binder is rewritten to a fresh sarek_pc_shadow_* name that no macro touches, so semantics are preserved and the declaration is valid. Delegates the shared traversal to Sarek_ir_codegen.rename_shadowing_locals, supplying only the GLSL collision set (escaped-name membership in pc_names/len_names) and fresh-name scheme. GLSL-only: no other backend uses macros for params.
val gen_helper_func :
state ->
pc_names:string list ->
len_names:string list ->
Stdlib.Buffer.t ->
Sarek_ir_types.helper_func ->
unitGenerate helper function with #undef/#define guards to avoid macro collisions. Push constant macros (e.g., #define max_iter pc.max_iter) would otherwise expand function parameters with the same name, causing syntax errors.
val glsl_header :
kernel_name:string ->
?block:(int * int * int) ->
?uses_float64:bool ->
?uses_int64:bool ->
?uses_coopmat:bool ->
?uses_uint8:bool ->
unit ->
stringGenerate GLSL compute shader header.
val gen_buffer_binding :
Stdlib.Buffer.t ->
int ->
Sarek_ir_types.var ->
Sarek_ir_types.elttype ->
unitGenerate buffer binding for a vector parameter
val gen_push_constants : Stdlib.Buffer.t -> Sarek_ir_types.decl list -> unitCollect shared array declarations from a statement tree. Returns list of (name, elem_type, size_expr)
Generate shared declarations at module scope
val gen_smod_helper : state -> Stdlib.Buffer.t -> Sarek_ir_types.kernel -> unitEmit the sarek_smod integer-remainder helper when the kernel uses mod.
Integer Mod is lowered (in gen_expr) to a call to this helper rather than to the GLSL % operator: % is undefined for negative operands and lowers to OpSMod (divisor-signed) on RADV, giving -7 % 2 = +1 instead of C's -1. The helper computes the C-truncated, dividend-signed remainder as a - b * (a / b); GLSL integer / truncates toward zero, so the result carries the dividend's sign and matches OCaml Int32.rem / the interpreter / PTX rem.s32 / OpenCL %. Routing through a function (not inlining the arithmetic) guarantees each operand is evaluated exactly once - critical for operands with side effects (value-returning atomics, effectful helper calls) that are legal integer expressions and reach the Mod node unguarded.
int-only for now: int64 on the Vulkan backend is unwired (no GL_ARB_gpu_shader_int64 extension is emitted anywhere), so an int64 kernel already fails to compile independently of mod. If int64 Vulkan lands, add an int64_t <name>(int64_t, int64_t) overload here (GLSL resolves the overload by argument type at the call site) and gate the extension on int64 usage, mirroring the float64 path in glsl_header.
The helper name is this generation's st.smod_name field (see compute_smod_name and state), not a literal, so it cannot collide with a user param or helper identifier.
val compute_collision_safe_name :
Sarek_ir_types.kernel ->
base:string ->
stringChoose a collision-safe name for the integer-remainder helper of kernel k.
The helper is declared at GLSL top level and called from expressions, so its name must avoid every identifier sharing that scope. Two collision sources (both observed by CodeRabbit on PR #255):
#define <name> pc.<name>, which would macro-expand the helper declaration and every call; a vector param becomes the storage-buffer array identifier <name>. Both use escape_glsl_name.hf.hf_name (see gen_helper_func); a user helper named sarek_smod would duplicate the symbol.If the default sarek_smod is taken, return the first free sarek_smod_1, sarek_smod_2, ... Local (SLet) names are function-scoped, not top-level, and are left to the future reserved-prefix policy noted in the impl brief (the same class already affects the sarek_<arr>_length intrinsic name).
val compute_smod_name : Sarek_ir_types.kernel -> stringval compute_copysign_name : Sarek_ir_types.kernel -> stringChoose a collision-safe name for the sign-copy helper of kernel k. Same scope and collision rules as compute_smod_name (see its doc); the two helpers use distinct bases (sarek_smod / sarek_copysign) so they never collide with each other, only with user param/helper identifiers.
val compute_fmod_name : Sarek_ir_types.kernel -> stringChoose a collision-safe name for the C-fmod helper of kernel k. Same scope and collision rules as compute_smod_name; the distinct base (sarek_fmod) keeps it from colliding with the other helpers, only with user param/helper identifiers.
val gen_copysign_helper :
state ->
Stdlib.Buffer.t ->
Sarek_ir_types.kernel ->
unitEmit the sarek_copysign sign-copy helper when the kernel uses copysign.
GLSL has no copysign builtin. The exact, branch-free lowering transfers the IEEE-754 sign bit of y onto the magnitude of x via integer bit ops, correct for every input including ±0 (where abs(x)*sign(y) fails, since GLSL sign(0)=0) and NaN sign transfer.
Two overloads are emitted, resolved by argument type at the call site:
float: always emitted when copysign is used. floatBitsToUint / uintBitsToFloat are core since GLSL 3.30, so this needs no extension.double: emitted only when the kernel also uses float64, because it uses unpackDouble2x32 / packDouble2x32 and the double type itself, all gated behind GL_ARB_gpu_shader_fp64 — the extension glsl_header already emits under the same kernel_uses_float64 condition. A Float64.copysign kernel is float64 by construction, so its call always finds the double overload; the (then-unused) float overload is harmless dead code. A Float32.copysign-only kernel gets just the float overload.The helper name is this generation's st.copysign_name field (see compute_copysign_name and state), not a literal, so it cannot collide with a user param or helper identifier.
val gen_fmod_helper : state -> Stdlib.Buffer.t -> Sarek_ir_types.kernel -> unitEmit the sarek_fmod C-conformant fmod helper when the kernel uses fmod.
GLSL has no fmod builtin, and its mod() is floor-based (divisor-signed), so it cannot implement the truncated C fmod the intrinsic contracts for. The earlier single-pass x - y * trunc(x / y) was wrong two ways (both caught in review): (1) for an infinite divisor it gives x - inf*trunc(0) = x - inf*0 = NaN, where C defines fmod(x, ±inf) = x; (2) for large |x/y| the quotient x/y loses integer precision, so trunc is wrong and the result can leave 0,|y|) — exactly why #252's PTX [emit_float_fmod] is an ITERATIVE reduction, not a single pass. This emits a C-conformant body instead: - domain guards, per C: NaN operand, [|x| = inf], or [y = 0] → NaN; [|y| = inf] (finite [x]) → [x]; [|x| < |y|] → [x]; - otherwise an exact reduction by power-of-two scaling: scale [d = |y|] up by [×2] (exact) to the largest [|y|·2^k ≤ |x|], then walk it back down to [|y|] subtracting whenever [r ≥ d]. Every [×2]/[×0.5] is exact, and each subtraction runs with [d ≤ r < 2d] so it is exact by Sterbenz — the result is therefore bit-exact vs C [fmod] (stronger than the PTX path's correctly-rounded div/fma reduction). The two loops each run at most [exp(|x|) − exp(|y|)] ≤ ~277 (f32) / ~2098 (f64) iterations — bounded by the exponent span, so termination is guaranteed (the same bound argument as the PTX reduction, which shrinks [|r|] by the mantissa width per round). The dividend's sign (incl. [-0]) is restored with a bit-level copy. Two overloads are emitted, resolved by argument type at the call site (same scheme as {!gen_copysign_helper}): - [float]: always emitted when [fmod] is used. [isnan]/[isinf]/[floatBitsTo*] are core since GLSL 3.30/4.10, needing no extension. - [double]: emitted only when the kernel also uses float64. It uses the genDType [isnan]/[isinf] overloads and [un/packDouble2x32], all gated behind [GL_ARB_gpu_shader_fp64] (already emitted under the same [kernel_uses_float64] condition, as for {!gen_copysign_helper}). The helper name is this generation's [st.fmod_name] field (see {!compute_fmod_name} and {!type:state}), never a literal, so it cannot collide with a user param or helper identifier.
val compute_f64_softmath :
Sarek_ir_types.kernel ->
Sarek_ir_types.helper_func list * bool * boolResolve the software f64-transcendental helper family a kernel needs, as the triple (f64_helpers, needs_int64, f64_needs_copysign) that state stores in the three same-named fields of the per-generation state. Collects the Float64 transcendental intrinsics the kernel invokes (Sarek_ir_analysis.kernel_float64_intrinsics), maps each to its root helper(s) (f64_root_helpers), and closes the set over softmath cross-calls (tan→sin/cos, pow→exp/log, log10→log, …). The retained order is Sarek_ir_softmath.all_helpers's stable family order, so the emitted preamble is deterministic across runs.
val state : Sarek_ir_types.kernel -> stateThe per-generation state for emitting k: the seven derived fields, plus a table for helper_vec_param_indices freshly allocated here, so no two calls share one.
val gen_f64_softmath_helpers :
state ->
pc_names:string list ->
len_names:string list ->
Stdlib.Buffer.t ->
unitEmit the needed software f64-transcendental helpers (st.f64_helpers). The whole family is forward-declared first, so cross-calls resolve regardless of definition order, then the bodies are emitted via gen_helper_func (which applies the same push-constant-macro #undef/#define guarding as user helpers). Emitted after sarek_copysign (which the bodies may call) and before user helpers (which may call these).
val param_macro_names : Sarek_ir_types.decl list -> string list * string listThe two macro-name sets a kernel's parameter list induces, as one value: (pc_names, len_names).
One construction, deliberately. gen_push_constants emits a #define <s> pc.<s> for every scalar parameter and a #define sarek_<v>_length pc.sarek_<v>_length for every vector parameter; rename_pc_shadowing_locals needs both sets to know which body locals those macros would rewrite. Both generate and generate_with_types need them, and until backlog-156's close-out each built them by its own copy of this filter_map pair. That is the same two-halves-one-concept shape glsl_array_length_name exists to close, one level up: reverting either copy alone left the whole tree green while the other entry point emitted GLSL glslangValidator rejects with "unexpected DOT". So there is one construction and both entry points call it — the copies cannot drift because there are no copies.
Note which entry point production uses: Vulkan_plugin calls generate_with_types; generate is reached only from the transpiler and the benchmark generator. A check that exercises only one of them is checking the other one's twin.
val reject_float16_kernel : Sarek_ir_types.kernel -> unitval gen_record_def :
Stdlib.Buffer.t ->
(string * (string * Sarek_ir_types.elttype) list) ->
unitGenerate GLSL record type definition - simple struct without tag
val gen_variant_def :
Stdlib.Buffer.t ->
(string * (string * Sarek_ir_types.elttype list) list) ->
unitGenerate GLSL variant type definition
val generate_with_types :
?block:(int * int * int) ->
?log:(string -> unit) ->
types:(string * (string * Sarek_ir_types.elttype) list) list ->
Sarek_ir_types.kernel ->
stringGenerate GLSL source with custom type definitions.
val generate :
?block:(int * int * int) ->
?log:(string -> unit) ->
Sarek_ir_types.kernel ->
stringGenerate complete GLSL source for a kernel.
A special case of generate_with_types with the kernel's OWN type declarations, which is the only thing every production caller ever passed: ~types has exactly the type of the kern_types field (Sarek_ir_types.kernel), so the parameter was redundant with the record it travels in. This used to be a separate 30-80 line copy of the emit sequence that silently omitted record typedefs, variant typedefs and the kernel's variants — source referencing an undeclared struct, with no error. Delegating keeps one emit path per backend.