Sarek_lower_irmodule Ir = Sarek_ir_ppxval elttype_of_typ : Sarek_types.typ -> Ir.elttypeConvert Sarek_types.typ to Sarek_ir_ppx.elttype
val constr_payload_elttypes : Sarek_types.typ option -> Ir.elttype listFlatten a variant constructor's payload type into its IR field list. A multi-argument constructor (MkPair of float32 * float32) carries a tuple payload type; it is FLATTENED to one IR field per component (TFloat32; TFloat32) so it lines up field-for-field with the multi-binder pattern (PConstr ("MkPair", ["x"; "y"])) and the flat _0/_1 tagged-union payload every source generator already emits. A single, non-tuple payload stays one field. Only scalar-primitive tuple components flatten; any other tuple keeps the (placeholder) single-field mapping.
val is_scalar_prim_typ : Sarek_types.typ -> boolScalar-primitive predicate on a source typ, matching the component set accepted for flattened tuple payloads (mirrors prim_component_elttype).
val elttype_prim_tag : Ir.elttype -> string optionval prim_component_elttype : Sarek_types.typ -> Ir.elttype optionScalar-primitive check on the SOURCE type of a tuple component. This must match on Sarek_types.typ directly, NOT via elttype_of_typ: that conversion maps TTuple/TFun to a placeholder Ir.TInt32 (see its NOTE), so routing the primitivity check through it would silently accept nested tuples or function components as int32 fields and synthesize a wrong layout. Returns the component's IR type iff it is a supported scalar.
val tuple_record_name : Ir.elttype list -> stringMangled nominal name of the record synthesized for a tuple shape, e.g. _tup_float32_int32 for float32 * int32. Kept in sync with the host-side Sarek_tuple_vec builder so both agree on the element identity.
The single located error raised for any tuple whose components are not all scalar primitives — shared by the vector-element path, the kernel-local slot path (slot_elttype_of_typ), the match-scrutinee guard in lower_stmt, and (backlog-194) the tuple-literal arm of lower_expr itself. loc should be the offending source expression when known (Ppxlib.Location.none otherwise).
The message no longer enumerates POSITIONS. It used to, and the enumeration was already wrong the moment backlog-194 added the lower_expr arm: that arm is the generic TETuple fall-through, so it also fires for a tuple used as a constructor payload, a call argument or a record field value — positions the list did not name, leaving the user reading a set of cases that excluded theirs. A position-independent sentence is the one this file can keep true.
val raise_aggregate_equality_error :
loc:Ppxlib.Location.t ->
what:string ->
ty:Sarek_types.typ ->
'aRaise the aggregate-equality refusal (backlog-194) with the SAME body the typer's Aggregate_equality_operand renders — Sarek_error owns that text and both sites call it, so the two cannot word it differently — plus one sentence saying this site is the one that fired.
That sentence is not decoration. Without it the two gates are indistinguishable in stderr, and the negative cases that exist to pin the TYPER gate would keep passing with the typer gate deleted, because this backstop catches the same kernels and prints the same words. A gate that another gate can satisfy on its behalf is not pinned. make test_negative therefore asserts BOTH polarities: the typer cases require the shared text AND the absence of this sentence, and test_poly_aggregate_eq requires it.
The sentence states WHERE the refusal happened, not WHY. An earlier revision said the operand type "only became known after monomorphisation", which this arm's condition does not establish — it tests the operand types and nothing else. The polymorphic-helper route is the known way to get here and is named as an example, not as the diagnosis.
%s is used deliberately for the shared body: it is data, not a format string.
val tuple_record_fields :
Sarek_types.typ list ->
string * (string * Ir.elttype) listSynthesized record fields (positional _0.._n) for a tuple's component types. Raises if any component is not a scalar primitive.
val is_primitive_tuple : Sarek_types.typ -> boolIs t a tuple whose components are all scalar primitives (the shape we synthesize a record for)?
val slot_elttype_of_typ : Sarek_types.typ -> Ir.elttypeIR type for a value that occupies a *data slot* — a vector element, or a kernel-local binding (let/match-bound). A primitive-component tuple becomes its synthesized positional record (_tup_*, fields _0.._n) so the struct backends (CUDA/OpenCL/GLSL/Metal) emit the right compound type instead of the elttype_of_typ placeholder int (see that function's NOTE); every other type uses the ordinary mapping. A non-primitive tuple raises the located tuple-component error via tuple_record_fields, mirroring vector-of-tuple scope. This is the sole difference from elttype_of_typ: the placeholder there stays intact for genuinely non-data flows (function-typed helper bindings), while data uses route here.
val memspace_of_memspace : Sarek_types.memspace -> Ir.memspaceConvert Sarek_types.memspace to Sarek_ir_ppx.memspace
val expr_names :
Ir.expr ->
string list ->
(string, unit) Stdlib.Hashtbl.t ->
unitval lvalue_names :
Ir.lvalue ->
string list ->
(string, unit) Stdlib.Hashtbl.t ->
unitval stmt_names :
Ir.stmt ->
string list ->
(string, unit) Stdlib.Hashtbl.t ->
unitval stmt_binders : Ir.stmt -> (string, unit) Stdlib.Hashtbl.t -> unitval expr_barrier : Ir.expr -> string optiontype state = {fun_map : (string, Sarek_typed_ast.tparam list * Sarek_typed_ast.texpr)
Stdlib.Hashtbl.t;lowering_stack : (string, unit) Stdlib.Hashtbl.t;lowered_funs : (string, Ir.helper_func) Stdlib.Hashtbl.t;Lowered helper functions: name -> helper_func
*)mutable lowered_funs_order : string list;Order in which functions were lowered (for dependency ordering)
*)types : (string, (string * Ir.elttype) list) Stdlib.Hashtbl.t;Collected record types: type_name -> (field_name, field_type); ...
variants : (string, (string * Ir.elttype list) list) Stdlib.Hashtbl.t;Collected variant types: type_name -> (constructor_name, payload_types); ...
mod_consts : (string, Ir.var * Ir.expr) Stdlib.Hashtbl.t;Module-level constants, by name, with their LOWERED initializer (backlog-160). A helper body that names one must carry its own SLet: the kernel-body copy is in a different scope, and helpers are emitted out-of-line. See prefix_referenced_consts.
mutable mod_consts_order : string list;Declaration order, reversed. Prefixing must be deterministic AND must respect dependency order — a later constant may reference an earlier one — so the prefix is emitted in declaration order, not hashtable order.
*)}Lowering state
val create_state :
(string, Sarek_typed_ast.tparam list * Sarek_typed_ast.texpr)
Stdlib.Hashtbl.t ->
stateval register_tuple_type : state -> Sarek_types.typ -> unitRegister the synthesized _tup_* record for a primitive-component tuple in the codegen types table, so a kernel-local slot typed by that record (see make_var/slot_elttype_of_typ) has its struct definition emitted. Idempotent; a no-op for non-tuple or non-primitive-tuple types (the latter is rejected at the slot-typing site by tuple_record_fields). The realistic data sources for a local tuple (a tuple literal, an if/match whose branches are tuple literals, a tuple-typed vector element) already register it elsewhere; this call makes the binding site self-sufficient regardless.
val register_types_from_typ : state -> Sarek_types.typ -> unitRegister every record type reachable from ty in the codegen types table, so the struct-emitting backends write out its struct definition.
Hoisted out of lower_kernel, which applied it to PARAMETER types only. That was the whole reason a shared array of a record failed to COMPILE on OpenCL and Vulkan (backlog-206): in
let%shared (s : tri) = 4l in
if tid = 0l then s.(tid).a <- 7.0tri appears nowhere but the shared declaration — no parameter carries it, and there is no record literal to route through TERecord's registration — so nothing put it in state.types and the emitted kernel said __local Test_tri s[4]; with no Test_tri anywhere above it. Measured: OpenCL "error: unknown type name 'Test_shared_record_slots_tri'", Vulkan a glslang syntax error at the declaration. The SAME kernel with a whole-slot store (s.(tid) <- {a = ...}) compiled and ran, because the literal registered the type — which is what made this look like a shared-memory gap rather than the type-collection gap it is.
VARIANTS are deliberately NOT handled here, and the reason is narrower than it first looks. The common shape — a kernel that puts constructor applications into the array — is covered, because TEConstr registers the type into state.variants. What is NOT covered, stated rather than left to be discovered: copying a variant in from elsewhere, e.g.
let%shared (s : color) = 4l in
s.(tid) <- v.(tid)over a variant-element vector parameter, contains no TEConstr, and the TVec arm below bottoms out at | _ -> () for TVariant — so on the struct-emitting backends that is the same undeclared-type-name failure this function fixes for records, still open for variants.
Now MEASURED rather than reasoned, and the measurement moves the seam. The shared array is not what makes that shape fail. A kernel whose ONLY variant occurrence is a color vector PARAMETER, with no shared array anywhere and no TEConstr —
fun (v : color vector) (out : float32 vector) ->
match v.(tid) with Red -> out.(tid) <- 0.0 | Value x -> out.(tid) <- x— already fails today, identically: kern_types = [], OpenCL ×2 "unknown type name 'M_color'" on the PARAMETER declaration, Vulkan ×2 a glslang syntax error. So the gap is variant registration in general, not a shared-memory seam; the shared-array shape above is one instance of it, and it is one this function's records fix does not reach.
It is still NOT fixed here, but not for the reason first written. "Adding a TVariant arm would change what gets emitted for ordinary variant parameters, unmeasured" is wrong where it matters: for a parameter-only variant there is nothing working to regress — that kernel does not compile. The real obstacle is that this function writes state.types, the RECORD table, and a variant belongs in state.variants; a TVariant arm here would declare a variant as a record struct. And registration alone is not even sufficient: with the type unregistered the emitter also drops the match-arm payload binder and emits out[tid] = x; with no x declared (measured on the same kernel — the binder comes from kern_variants, which TEConstr fills and this traversal does not touch). Fixing variants means routing to state.variants, which is a change with its own evidence to gather, not a one-line arm here.
The interpreter and PTX halves of backlog-206 DO handle TVariant, so the treatment is deliberately uneven and this is where it is uneven.
val ir_binop : Sarek_ast.binop -> Sarek_types.typ -> Ir.binopConvert Sarek_ast.binop to Sarek_ir_ppx.binop
val ir_unop : Sarek_ast.unop -> Ir.unopConvert Sarek_ast.unop to Sarek_ir_ppx.unop
val lower_memspace : Sarek_types.memspace -> Ir.memspaceConvert memspace
val make_var : string -> int -> Sarek_types.typ -> bool -> Ir.varCreate a var from typed var info. Uses slot_elttype_of_typ (not the bare elttype_of_typ) so a kernel-local binding of primitive-tuple type is typed by its synthesized _tup_* record rather than the placeholder int — the struct backends then declare and read the slot as the right compound type. Function-typed references still fall through to the placeholder.
val lower_decl : mutable_:bool -> int -> string -> Sarek_types.typ -> Ir.declLower a declaration
Transform a statement to ensure it returns a value. This adds SReturn to leaf statements without re-traversing the original AST.
val is_trivial_ir_expr : Ir.expr -> booltrue iff e is a syntactically trivial IR expression (EVar or EConst). Trivial expressions have no side effects, so duplicating them in the tree built by lower_lsr is semantically inert.
val lower_lsr :
loc:Ppxlib.Location.t ->
Ir.expr ->
Ir.expr ->
Ir.elttype ->
Ir.exprLower a lsr b (logical/unsigned right shift) to an IR expression tree built only from existing IR nodes.
Ir.Shr is emitted as an *arithmetic* (sign-extending) shift by every consumer (CUDA/OpenCL/Metal/GLSL/WGSL emit plain >> on a signed C/GLSL int type; PTX and the interpreter use shr.s32/Int32.shift_right - see G phase 1 in the unpublished fix-critical-semantics-evidence.md note). There is no IR node for a logical shift and none may be added (formal/codegen-ptx models Shr itself), so lsr is expressed via the classic arithmetic-shift identity, width-aware via width_bits:
lshr (a, n)
=
if n = 0 then a
else
ashr (a, n) lxor (ashr (a, width - 1) lsl ((width - n) land (width - 1)))ashr(a, width-1) is all-1s when a is negative and all-0s otherwise; shifted left by (width - n) land (width - 1) (equal to width - n for every n in 1..width-1) it isolates exactly the n sign-extended bits that ashr(a, n) filled in, and XOR-ing them off recovers the zero-filled logical shift.
Why the land (width - 1) mask, not just an n = 0 guard. PTX's selp and WGSL's select() (see Sarek_ir_ptx_expr.ml and Sarek_ir_wgsl.ml) evaluate BOTH branches of the resulting EIf before selecting one - the EIf only picks which *value* is used, it does not skip *computing* the other branch's subexpressions on those backends. So even though the then-branch (a_ir) is selected when n = 0, the else-branch's internal Sub(width_bits, b_ir) is still evaluated, and without masking it would be exactly width_bits (shift-by-32/64), which is undefined/rejected on some backends. Masking with width_bits - 1 keeps that shift count in 0..width_bits-1 for every n, including n = 0 (where it reduces to a well-defined shift-by-0, unused anyway since the EIf selects a_ir), while leaving the result unchanged for n in 1..width_bits-1 (masking a value already in range is a no-op). Shift amounts with n < 0 or n >= width are unspecified, matching the pre-existing behaviour of Shl/Shr on out-of-range counts.
Duplication / side-effect safety. a_ir and b_ir each appear three times in the tree above (in sign_fill, in arith_shift/top_bits, and in the final EIf's branches/condition). Sarek_ir_ppx.expr is documented as "pure, no side effects" and has no let-binding form (only Ir.stmt's SLet/SLetMut bind values, and those wrap a *statement* continuation, not an expression one) - so there is no way to evaluate a subexpression once and reuse the result across multiple expression positions. Hoisting via a synthetic EApp call (which would single-evaluate its arguments) is not viable either: PTX - the backend this rewrite specifically targets - does not implement device function calls (Sarek_ir_ptx_expr.ml rejects EApp outright). Consequently, if a_ir or b_ir is *not* trivial (e.g. it embeds an EIntrinsic atomic call), this tree would silently evaluate that operand's side effect multiple times. Rather than accept that, this function restricts the rewrite to trivial operands (EVar/EConst, see is_trivial_ir_expr) and raises a located PPX error for every other case, directing the user to hoist the operand into a `let` before the shift. This tree is NOT safe for arbitrary operands - only for trivial ones.
val lower_expr : state -> Sarek_typed_ast.texpr -> Ir.exprConvert a typed expression to IR expression
val lower_stmt : state -> Sarek_typed_ast.texpr -> Ir.stmtConvert a typed expression to IR statement
val lower_lvalue : state -> Sarek_typed_ast.texpr -> string -> Ir.lvalueval lower_pattern : Sarek_typed_ast.tpattern -> Ir.patternval extract_pattern_vars : Sarek_typed_ast.tpattern -> string listval lower_param : Sarek_typed_ast.tparam -> Ir.declConvert a kernel parameter to IR declaration
val lower_kernel : Sarek_typed_ast.tkernel -> Ir.kernelLower a complete kernel
val lower_return_value : Sarek_typed_ast.tkernel -> Ir.decl optionGet the return value declaration for a kernel