Sarek_parseval extract_param_from_pattern : Ppxlib.pattern -> Sarek_ast.paramval extract_type_from_pattern : Ppxlib.pattern -> Sarek_ast.type_expr optionval collect_fun_params :
Ppxlib.expression ->
Ppxlib.pattern list * Sarek_parse_helpers.fun_body optionval parse_binop : string -> Sarek_ast.binop optionval parse_unop : string -> Sarek_ast.unop optionval parse_type : Ppxlib.core_type -> Sarek_ast.type_exprThe refusal for a when guard on a match case (backlog-191).
Before this refusal existed, parse_expression's Pexp_match arm read pc_lhs and pc_rhs and never looked at pc_guard. The guard was dropped in the PARSER — upstream of type-checking, of every lowering pass and of backend selection — so the arm became unconditional in the one AST all backends are generated from. The kernel compiled and computed a different function than its source says.
How silent that was depends on the arm shape, and the two halves pull against each other. A wrong-ANSWER repro needs two arms on the SAME constructor, one guarded and one not — the guard is then the only thing choosing between them, so dropping it changes the result. But dropping it also leaves two syntactically identical arms, which is exactly OCaml's warning 11 redundant-case; under the (:standard -w -32-33-34-69) flags the e2e suites use, 11 is left on as an error. So for the shape that demonstrates a wrong answer there WAS a diagnostic, just one pointing at a redundant case rather than at a dropped guard. The genuinely silent shapes are the ones where no two arms share a constructor: the guard is dropped with nothing to make the arms look redundant, and the arm becomes unconditional with no warning at all. Either way the refusal is the fix — this is not a bug you can rely on warning 11 to catch.
Why this refuses rather than lowering the guard. Three things would have to change together, and none of them is local:
Sarek_ir_ppx's EMatch and SMatch hold (pattern * _) list. There is no guard slot to lower INTO, so a guard needs a new field on the match node — and not on one type. There are two IRs: Sarek_ir_ppx (the PPX compile-time IR, in sarek_frontend) and Sarek_ir_types (spoc/ir, the runtime types the code generators read), bridged by Sarek_ir_conv; the device emitters consume the SECOND, and Sarek_ast carries a third like-named EMatch on the surface AST. The field has to be threaded through each representation and through the conversion between them, and the set of places that read those constructors is large and open — see the paragraph below the bullets.Sarek_ir_ptx_expr.check_match_exhaustive) reasons on constructor coverage alone. A guarded arm covers its constructor syntactically but not semantically, so once guards exist a match can fail at run time — and the device backends have no trap to fail INTO (the interpreter's Pattern_match_failure has no GPU counterpart).On the first bullet's blast radius: it is not a closed list, and this comment does not try to close it. git grep -l "EMatch\|SMatch" reports 37 non-test source files, spread over sarek/ppx, sarek/codegen, sarek/interp, sarek/transpile, sarek/sarek and spoc/ir, plus the negative and golden tests and the formal/type-safety Rocq models with their extracted PatternModel/ConstrModel. Among them: at least the six device emitters (Sarek_ir_cuda, Sarek_ir_opencl, Sarek_ir_metal, Sarek_ir_ptx with its _expr/_stmt halves, Sarek_ir_glsl, Sarek_ir_wgsl) and roughly a dozen IR passes and traversals (tag erasure, vector inlining, softmath, monomorphisation, defunctionalisation, the three tailrec passes, convergence analysis, fusion, Sarek_lower_ir, Sarek_ir_conv, and spoc/ir's analysis/codegen/pp), alongside the native and interpreter evaluators. Re-run the grep for the current set rather than trusting an enumeration: an earlier round of this comment stated it as a closed "every consumer" list that named four emitters and omitted, among others, Sarek_ir_glsl and Sarek_ir_wgsl.
So this is "not yet supported", not "cannot be supported": the guarded-arm subset that is always followed by an unguarded arm for the same constructor is implementable. It is a feature with an IR change in it, not a fix, and until it is built a refusal is the only honest answer.
{ r with f = e } — functional record update.
Pexp_record's second component is the with base. It was read as _base and never used, so { r with x = 1.0 } parsed to exactly the same ERecord as { x = 1.0 } and the base was gone (backlog-192).
It was NOT silent, and an earlier revision of this comment and of record_update_msg said it was. Measured on this tree by removing this refusal and building sarek/tests/negative/test_record_update.ml: the kernel fails to compile with OCaml's own
Error: Some record fields are undefined: y
pinned to the user's { p with x = 1.0 } expression. The mechanism is that the PPX re-emits the record literal into the generated native fallback with the original location, so OCaml sees a literal missing a field and rejects it. Any with that actually omits a field omits it there too, so this is not a property of that one case — but "silently wrong device code", the when guard's failure mode, is not what this was. What it was is a diagnostic that names a missing field and never mentions that the with was discarded, which is why the refusal is still worth having.
The refusal is also not "cannot be supported": there is no copy-then-overwrite form in the record lowering, and adding one is a feature.
A record field named through a functor application. The field longident used to fall back to the literal name "field" for anything that was not Lident/Ldot — a field name the record almost certainly does not have, so the drop surfaced (if at all) as an unrelated unknown-field error.
UNREACHABLE from OCaml source: the grammar's field position is mk_longident(mod_longident, LIDENT) and mod_longident has no functor-application production, so r.F(X).f is a syntax error — checked with ocamlc -stop-after parsing on let f r = r.F(X).lbl, which reports "Syntax error". The arm exists so that no Longident shape can reach a fabricated field name if that ever changes.
A labelled or optional argument at a CALL SITE. Pexp_apply's argument list is (arg_label * expression) list and the label was read as _, so f ~b:2 ~a:1 was lowered as the positional f 2 1 — silently passing the arguments in written order rather than in declared order. A labelled PARAMETER is already refused by collect_fun_params, so no kernel-visible function can legitimately be called this way.
A functor application in the module path of let open ... in.
UNREACHABLE from OCaml source: let open F(X) in e parses to Pexp_open over a Pmod_apply, not over a Pmod_ident carrying a Lapply longident (checked with ocamlc -stop-after parsing), and Pmod_apply is refused by parse_expression's final arm through Sarek_unsupported. The arm is kept so the Longident match stays total: the reachable defect it replaces was the | _ -> [] beside it, which mapped every path DEEPER than M.N to the empty path — and an empty path is not inert, Sarek_native_gen_expr's TEOpen arm reaches failwith "empty module path in TEOpen". Measured on this tree before the fix: a kernel containing let open A.B.C in failed with "Sarek internal error: Failure(\"empty module path in TEOpen\")".
A return-type annotation on the kernel function itself. There is no slot for it in Sarek_ast.kernel — kern_params and kern_body are the whole signature — so it was read by nobody.
Both payload readers used to handle an unannotated module constant inconsistently: the let module fold DROPPED the binding (so a kernel using it failed with an unbound variable pointing at the USE), while the top-level fold refused it with a message that was false for the let x : t = e spelling, whose annotation neither of them looked at.
val constrain_body :
Sarek_ast.type_expr option ->
Sarek_ast.expr ->
Sarek_ast.exprApply a declared result type to a function body as a constraint.
Sarek_ast.MFun has no type field, so a module-item helper's declared result type had nowhere to go and was dropped. ETyped carries it without changing any representation: Sarek_typer's ETyped arm unifies and returns the INNER typed expression with the substituted type, so no node reaches the IR and no lowering pass sees anything new.
HOW MUCH THIS CONSTRAINS, exactly. For a MONOMORPHIC annotation, the declared type. For one containing a type VARIABLE it is weaker than the source says, and that is a property of this route rather than of the annotation: Sarek_typer's ETyped arm converts with type_of_type_expr_env, which allocates a FRESH type-variable context, while an MFun's parameters are converted in the context built for the item. So in let f (x : 'a) : 'a = ... the two 'as become different variables, and the annotation constrains the SHAPE of the result rather than its identity with the parameter. The ELetRec route (a kernel-BODY helper) does not share the limitation: it converts the result type in the same tvar_ctx as the parameters.
Not refused, because sarek/tests/e2e/test_module_poly.ml legitimately writes let[@sarek.module] identity (x : 'a) : 'a = x. Recorded here and in kb/sarek/ppx/parser.md instead. Found by the cross-runtime review.
val binding_result_type :
Ppxlib.value_binding ->
int ->
Sarek_ast.type_expr optionThe declared RESULT type of a let binding with nparams parameters.
Three spellings reach here and only one of them had a reader before backlog-192: let (x : t) = e (in the pattern, read), let x : t = e (in pvb_constraint — dropped), and let f x : t = e (in Pexp_function's constraint slot — dropped). The second is the spelling nearly everything in this tree uses.
A whole-binding annotation on a FUNCTION is refused rather than peeled. An earlier revision of this function peeled one arrow per parameter and used the result — which silently discarded every DOMAIN the user had written: let (f : float32 -> int32) = fun (x : int32) -> x was accepted as an int32 -> int32 helper, the declared float32 read by nobody. That is the same defect class the sweep exists to close, introduced by the sweep's own first draft (found by the cross-runtime review). Refusing costs nothing, because a kernel function's parameters must ALREADY carry their own annotations — extract_param_from_pattern raises "Kernel parameters must have type annotations" otherwise — so the domain half of a whole-binding arrow is always redundant with them, and never checked against them.
Parse let%shared: let%shared name : type = size in body Syntax: let%shared tile : float32 array in body let%shared tile : float32 array = 64 in body
val parse_superstep :
(Ppxlib.expression -> Sarek_ast.expr) ->
Ppxlib.expression ->
Sarek_ast.expr_descParse let%superstep: let%superstep ~divergent name = body in cont Syntax: let%superstep load = tile.(i) <- v in cont let%superstep ~divergent final = ... in cont
val parse_expression : Ppxlib.expression -> Sarek_ast.exprParse an expression
val parse_assign_form :
Ppxlib.expression ->
Ppxlib.expression ->
Sarek_ast.expr_descExtract body for mutable assignment: x := v
val parse_pragma_form :
Ppxlib.expression ->
Ppxlib.expression ->
Sarek_ast.expr_descExtract body for pragma form: pragma "opt1"; "opt2" body
val parse_create_array_form :
Ppxlib.expression ->
Ppxlib.expression ->
Sarek_ast.expr_descExtract body for create_array form: create_array size memspace
val parse_binop_or_app_form :
Ppxlib.expression ->
string ->
Ppxlib.expression ->
Ppxlib.expression ->
Sarek_ast.expr_descExtract body for binary operator or function application arm
val parse_let_form :
Ppxlib.value_binding ->
Ppxlib.expression ->
Sarek_ast.expr_descExtract body for let binding arm
val parse_named_let_form :
Ppxlib.value_binding ->
Ppxlib.expression ->
Sarek_ast.expr_descEvery field of a kernel-payload type_declaration that parse_payload does not read, refused rather than dropped.
A payload type declaration is consumed by the PPX and never reaches OCaml, so a field nobody reads here is a field nobody reads at all. The two attributes Sarek itself declares are accepted and ignored on purpose: inside a payload every type declaration is already a Sarek type, so @@sarek.type there is redundant rather than dropped.
val parse_kernel_function : Ppxlib.expression -> Sarek_ast.kernelParse a function expression into a kernel
val parse_payload : Ppxlib.expression -> Sarek_ast.kernelParse from ppxlib payload