Module Spoc_framework.Backend_error

include module type of struct include Sarek_backend_error.Backend_error end

Error Type Definitions

type codegen_error = Sarek_backend_error.Backend_error.codegen_error =
  1. | Unknown_intrinsic of {
    1. name : string;
    }
    (*

    Intrinsic function not recognized by this backend

    *)
  2. | Invalid_arg_count of {
    1. intrinsic : string;
    2. expected : int;
    3. got : int;
    }
    (*

    Wrong number of arguments to intrinsic

    *)
  3. | Unsupported_construct of {
    1. construct : string;
    2. reason : string;
    }
    (*

    IR construct not supported by this backend

    *)
  4. | Type_error of {
    1. expr : string;
    2. expected : string;
    3. got : string;
    }
    (*

    Type mismatch in expression

    *)
  5. | Invalid_memory_space of {
    1. decl : string;
    2. space : string;
    }
    (*

    Invalid memory space qualifier for declaration

    *)
  6. | Unsupported_type of {
    1. type_name : string;
    2. backend : string;
    }
    (*

    Type not supported by backend (e.g., fp64 without cl_khr_fp64)

    *)

Error types for backend code generation (IR → source translation)

type runtime_error = Sarek_backend_error.Backend_error.runtime_error =
  1. | No_device_selected of {
    1. operation : string;
    }
    (*

    Operation requires a device but none is set

    *)
  2. | Device_not_found of {
    1. device_id : int;
    2. max_devices : int;
    }
    (*

    Device ID out of range

    *)
  3. | Compilation_failed of {
    1. source : string;
    2. log : string;
    }
    (*

    Kernel compilation failed

    *)
  4. | Module_load_failed of {
    1. size : int;
    2. reason : string;
    }
    (*

    Failed to load compiled module/program

    *)
  5. | Kernel_launch_failed of {
    1. kernel_name : string;
    2. reason : string;
    }
    (*

    Failed to launch kernel on device

    *)
  6. | Memory_allocation_failed of {
    1. bytes : int64;
    2. reason : string;
    }
    (*

    Device memory allocation failed

    *)
  7. | Memory_copy_failed of {
    1. direction : string;
    2. bytes : int;
    3. reason : string;
    }
    (*

    Memory transfer between host and device failed

    *)
  8. | Context_error of {
    1. operation : string;
    2. reason : string;
    }
    (*

    GPU context creation/management failed

    *)
  9. | Synchronization_failed of {
    1. reason : string;
    }
    (*

    Device synchronization failed

    *)

Error types for backend runtime operations

type plugin_error = Sarek_backend_error.Backend_error.plugin_error =
  1. | Unsupported_source_lang of {
    1. lang : string;
    2. backend : string;
    }
    (*

    Source language not supported by backend

    *)
  2. | Backend_unavailable of {
    1. reason : string;
    }
    (*

    Backend not available (missing drivers, no devices, etc.)

    *)
  3. | Library_not_found of {
    1. library : string;
    2. paths : string list;
    }
    (*

    Required backend library not found

    *)
  4. | Initialization_failed of {
    1. backend : string;
    2. reason : string;
    }
    (*

    Backend initialization failed

    *)
  5. | Feature_not_supported of {
    1. feature : string;
    2. backend : string;
    }
    (*

    Feature not supported by this backend

    *)

Error types for backend plugin operations

Parameterized Error Type

type t = Sarek_backend_error.Backend_error.t =
  1. | Codegen of {
    1. backend : string;
    2. error : codegen_error;
    }
  2. | Runtime of {
    1. backend : string;
    2. error : runtime_error;
    }
  3. | Plugin of {
    1. backend : string;
    2. error : plugin_error;
    }

Union type for backend errors, parameterized by backend name

exception Backend_error of t

Exception wrapper for backend errors

Error Construction Helpers

val codegen : backend:string -> codegen_error -> t

Create codegen error for a specific backend

val runtime : backend:string -> runtime_error -> t

Create runtime error for a specific backend

val plugin : backend:string -> plugin_error -> t

Create plugin error for a specific backend

Codegen Error Constructors

val unknown_intrinsic : backend:string -> string -> t
val invalid_arg_count : backend:string -> string -> int -> int -> t
val unsupported_construct : backend:string -> string -> string -> t
val type_error : backend:string -> string -> string -> string -> t
val invalid_memory_space : backend:string -> string -> string -> t
val unsupported_type : backend:string -> string -> t

Runtime Error Constructors

val no_device_selected : backend:string -> string -> t
val device_not_found : backend:string -> int -> int -> t
val compilation_failed : backend:string -> string -> string -> t
val module_load_failed : backend:string -> int -> string -> t
val kernel_launch_failed : backend:string -> string -> string -> t
val memory_allocation_failed : backend:string -> int64 -> string -> t
val memory_copy_failed : backend:string -> string -> int -> string -> t
val context_error : backend:string -> string -> string -> t
val synchronization_failed : backend:string -> string -> t

Plugin Error Constructors

val unsupported_source_lang : backend:string -> string -> t
val backend_unavailable : backend:string -> string -> t
val library_not_found : backend:string -> string -> string list -> t
val initialization_failed : backend:string -> string -> t
val feature_not_supported : backend:string -> string -> t

Error Conversion and Display

val to_string : t -> string

Convert error to human-readable string

val raise_error : t -> 'a

Raise backend error as exception

Shared refusals

val reject_soa_params : backend:string -> string list -> unit

Refuse a non-empty ~soa_params on a backend whose emitter has no Structure-of-Arrays lowering (backlog-214).

Framework_sig.generate_source offers ?soa_params to every backend, but only an emitter that actually lowers a named vector parameter to N per-leaf bindings plus one shared length can honour it. A backend without that lowering used to bind the argument away as ?soa_params:_ and return its ordinary packed-AoS source — ONE binding per vector — while the launch side expands an SoA-dispatched vector into N RSA_Buffers plus one RSA_Vector_Length. That mismatch is never a compile error, and how badly it fails is per backend rather than uniform, which is why the refusal is stated here in terms of the mismatch and not of its symptom:

THE GENERAL FORM, and the only part that holds for every kernel shape: every parameter declared AFTER the vector receives the wrong entry. Nothing catches it on three of the five: Execute.check_launch_args checks arity, but against the CALLER's vector list before expansion, so it cannot see this, and it is the shift Execute.expand_to_run_source_args warns about for a leaf-count disagreement. What that shift then MEANS is a property of the parameter list, not of SoA, and the three cases below are examples rather than the consequence:

  • at N = 2 with a pointer parameter declared after the vector (the shape the probe kernels in this repository's five refusal tests happen to have), a length slot receives a pointer value and the pointer slot reads its 8 bytes out of a 4-byte length cell;
  • at N = 3, the third leaf lands in the following pointer slot — a VALID device pointer to the wrong buffer. Silent corruption, nothing to trap on;
  • with the SoA vector declared LAST, nothing after it shifts into a pointer slot at all.

So "not a crash" and "silently wrong data" are each too narrow, and so is any single mechanism.

  • CUDA/C and HIP bind into the bare pointer array cuLaunchKernel/hipModuleLaunchKernel take (Cuda_shared.bind_args, Hip_shared.bind_args) with nothing comparing it against the compiled kernel's signature, so a caller-supplied value can reach the device as an address and a trap IS among the outcomes;
  • Metal binds positionally too (Metal_plugin_base by list position via atIndex:, its expected_count being Kernel_args.count and so caller-derived), but no caller-supplied ADDRESS ever crosses: buffers go through setBuffer as MTLBuffer objects and scalars through setBytes as a driver-allocated inline copy (Metal_api, the Buffer/Int32 arms), against parameters declared device T* x [[buffer(k)]], constant int &sarek_x_length [[buffer(k+1)]] (Sarek_ir_metal.gen_buffer_param). Under the shift a length index receives a buffer whose first 4 bytes are read as the length, and a pointer index receives inline setBytes data (a valid 4-byte allocation indexed out of bounds) or another buffer (valid address, wrong buffer). Wrong data, out-of-bounds reads within valid allocations, or an encode Metal's own API validation rejects — but nothing that traps in the CUDA sense, so this backend does NOT inherit that half of the claim;
  • OpenCL shares that caller-derived preflight count, but has a late check it did not put there: binding goes through the checked clSetKernelArg funnel (Opencl_api.Kernel.set_arg_mem), which raises on CL_INVALID_ARG_INDEX once the index runs past the compiled kernel's argument list;
  • Vulkan has a second, source-derived count (Vulkan_api_kernel.validate_buffer_indices, whose expected_count is read from the GLSL binding = N declarations), so there the mismatch surfaces late as a buffer-count rejection naming the two numbers — and saying nothing about SoA.

As of backlog-214 no caller in this tree reaches any of that: Execute.soa_dispatch restricts SoA to the CUDA/PTX device and Soa_launch gates on PTX being in the backend's supported_source_langs. So this is a boundary, not a live bug fix. What it changes is where the guarantee lives: it was one caller-side predicate and nothing else, and a backend that cannot honour the request now says so instead of answering with the wrong ABI.

Scope of what this raises, stated narrowly on purpose: it says what THIS backend does with vector parameters. It deliberately does not name which other backend does support SoA — that set is expected to grow (backlog-215), and a message enumerating it would go stale in a file that is not edited when it does. It is also not re-exported through Make: Make closes over one backend string per error module, and Cuda_error's is "CUDA" for both the CUDA/PTX backend that implements SoA and the CUDA/C backend that refuses it, so a Make-based version could not tell the caller which one answered.

One case is deliberately refused although it would happen to work: a record with a SINGLE leaf. Soa.plan permits it, and at N = 1 the SoA argument list (one leaf buffer plus one length) has the same shape as the AoS one, so the AoS source would bind correctly. It is refused anyway, because that correctness is a coincidence of the leaf count rather than a property of the emitter: the same call means something different the moment the record gains a second field, and a carve-out that silently changes meaning under an unrelated edit is worse than a refusal.

[] returns (), so every in-tree caller — all of which pass an omitted or empty list — and the caller-side fast path are byte-for-byte unaffected. An out-of-tree caller already passing a non-empty list to one of these five now raises; that is the intended change.

val print_error : t -> unit

Print error to stderr

val with_default : default:'a -> (unit -> 'a) -> 'a

Execute function with default fallback on error

val to_result : (unit -> 'a) -> ('a, t) Stdlib.result

Convert error to Result type

val result_to_string : ('a, t) Stdlib.result -> ('a, string) Stdlib.result

Map Result error to string

Backend-Specific Modules

Helper module for creating backend-specific error interfaces. Each backend can instantiate this functor with their name.