Skip to main content

rlx_ir/
capability.rs

1// RLX — versatile ML compiler + runtime.
2// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna.
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! One table describing what every [`OpKind`] *is*.
6//!
7//! Passes constantly need to ask categorical questions about an op — is it
8//! elementwise, is it a fusion boundary, can it be folded on the host, does it
9//! carry a nested body. Historically each caller answered with its own
10//! `matches!` list: [`Op::is_elementwise`] and friends here in `rlx-ir`,
11//! `is_pure` in `rlx-compile`'s constant folder, `is_leaf` in `param_hoist`
12//! *and* again in `rlx-distributed`'s partitioner, `FUSED_KINDS` in the
13//! backend rewriter. Every one of those lists is open — a `matches!` with no
14//! wildcard arm silently answers "no" for a variant nobody remembered to add.
15//!
16//! That is the same failure mode the opcode tables in [`crate::opcodes`] were
17//! written to design out, and the fix is the same: classify every variant
18//! **exactly once**, in an exhaustive `match`. Adding an [`OpKind`] without
19//! classifying it is a compile error, not a silently wrong answer.
20//!
21//! ```
22//! use rlx_ir::{Op, OpKind, capability::OpCaps};
23//!
24//! assert!(OpKind::MatMul.caps().contains(OpCaps::FUSION_BOUNDARY));
25//! assert!(Op::MatMul.is_blas());
26//! assert!(!OpKind::Reshape.caps().contains(OpCaps::ELEMENTWISE));
27//! ```
28//!
29//! # Adding a capability
30//!
31//! Add the flag, then extend the arms below. Because the match is exhaustive
32//! and grouped by capability *set*, a new flag usually means moving a handful
33//! of kinds between existing arms rather than touching all 184 of them.
34
35use crate::op::{Op, OpKind};
36
37/// Number of [`OpKind`] variants.
38pub const N_KINDS: usize = 184;
39
40/// Static properties of an op, independent of its operands or attributes.
41///
42/// A bit set rather than an enum: most questions are independent
43/// (a `FusedMatMulBiasAct` is simultaneously BLAS, a fusion boundary, and a
44/// fused op with a decomposition).
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46pub struct OpCaps(u32);
47
48impl OpCaps {
49    /// No static capability. The honest answer for most ops: they are neither
50    /// elementwise nor BLAS nor anything else the optimizer special-cases.
51    pub const NONE: Self = Self(0);
52
53    /// A graph root with no tensor inputs (`Input` / `Param` / `Constant`).
54    pub const LEAF: Self = Self(1 << 0);
55
56    /// Same shape in, same shape out, computed independently per element —
57    /// the prime fusion candidate.
58    pub const ELEMENTWISE: Self = Self(1 << 1);
59
60    /// Evaluable on the host from constant operands, so the constant folder
61    /// may replace it outright.
62    ///
63    /// Narrower than "deterministic": it also requires that the folder has an
64    /// implementation. An op can be perfectly deterministic and still not
65    /// carry this flag.
66    pub const CONST_FOLDABLE: Self = Self(1 << 2);
67
68    /// Compute-intensive and dispatched to a tuned library / kernel
69    /// (matmul, conv, solves).
70    pub const BLAS: Self = Self(1 << 3);
71
72    /// Elementwise fusion must not span across this op.
73    ///
74    /// Implied by [`BLAS`](Self::BLAS) — every BLAS op carries both flags, so
75    /// this can be tested directly without also testing `BLAS`.
76    pub const FUSION_BOUNDARY: Self = Self(1 << 4);
77
78    /// Collapses one or more axes; drives loop iteration in a fused kernel.
79    pub const REDUCTION: Self = Self(1 << 5);
80
81    /// May appear in an [`Op::TransformRegion`] chain.
82    pub const TRANSFORM: Self = Self(1 << 6);
83
84    /// A composite op with a decomposition into primitives — the backend
85    /// rewriter can unfuse it when a target lacks the native kernel.
86    pub const FUSED: Self = Self(1 << 7);
87
88    /// Carries one or more nested [`Graph`](crate::Graph) bodies. Consumers
89    /// that walk the IR recursively must descend via [`Op::subgraphs`].
90    pub const NESTED_BODY: Self = Self(1 << 8);
91
92    /// Output is not a function of the inputs alone (RNG / sampling). Never
93    /// const-foldable, never CSE-able, and a hard root for hoisting.
94    pub const NONDETERMINISTIC: Self = Self(1 << 9);
95
96    /// Union of two sets (`const`-friendly, so the table can be built in a
97    /// `const fn`).
98    pub const fn union(self, other: Self) -> Self {
99        Self(self.0 | other.0)
100    }
101
102    /// Does this set contain every flag in `other`?
103    pub const fn contains(self, other: Self) -> bool {
104        self.0 & other.0 == other.0
105    }
106
107    /// Does this set share any flag with `other`?
108    pub const fn intersects(self, other: Self) -> bool {
109        self.0 & other.0 != 0
110    }
111
112    /// True when no capability is set.
113    pub const fn is_empty(self) -> bool {
114        self.0 == 0
115    }
116}
117
118impl std::ops::BitOr for OpCaps {
119    type Output = Self;
120    fn bitor(self, rhs: Self) -> Self {
121        self.union(rhs)
122    }
123}
124
125impl OpKind {
126    /// Every [`OpKind`], in declaration order.
127    ///
128    /// Exists so tests can fan out exhaustively over the whole op set — the
129    /// same role `BinaryOp::ALL` and friends play in [`crate::opcodes`].
130    pub const ALL: [OpKind; N_KINDS] = [
131        OpKind::Input,
132        OpKind::Param,
133        OpKind::Constant,
134        OpKind::Activation,
135        OpKind::Cast,
136        OpKind::StopGradient,
137        OpKind::Quantize,
138        OpKind::Dequantize,
139        OpKind::FakeQuantize,
140        OpKind::FakeQuantizeLSQ,
141        OpKind::FakeQuantizeLSQBackwardX,
142        OpKind::FakeQuantizeLSQBackwardScale,
143        OpKind::Binary,
144        OpKind::Compare,
145        OpKind::Where,
146        OpKind::Fma,
147        OpKind::ElementwiseRegion,
148        OpKind::TransformRegion,
149        OpKind::BatchElementwiseRegion,
150        OpKind::MatMul,
151        OpKind::DotGeneral,
152        OpKind::DenseSolve,
153        OpKind::BatchedDenseSolve,
154        OpKind::Cholesky,
155        OpKind::TriangularSolve,
156        OpKind::Det,
157        OpKind::LogDet,
158        OpKind::Sort,
159        OpKind::ArgSort,
160        OpKind::Svd,
161        OpKind::Qr,
162        OpKind::LayerNorm,
163        OpKind::LayerNorm2d,
164        OpKind::GroupNorm,
165        OpKind::BatchNormInference,
166        OpKind::RmsNorm,
167        OpKind::ResizeNearest2x,
168        OpKind::Interpolate3d,
169        OpKind::Attention,
170        OpKind::Rope,
171        OpKind::AxialRope2d,
172        OpKind::Reshape,
173        OpKind::Transpose,
174        OpKind::Narrow,
175        OpKind::Concat,
176        OpKind::KvAppend,
177        OpKind::Expand,
178        OpKind::Gather,
179        OpKind::Reverse,
180        OpKind::Pad,
181        OpKind::Slice,
182        OpKind::Clamp,
183        OpKind::Tile,
184        OpKind::Trilu,
185        OpKind::Reduce,
186        OpKind::Histogram,
187        OpKind::Softmax,
188        OpKind::Cumsum,
189        OpKind::CumProd,
190        OpKind::CumMax,
191        OpKind::ArgMax,
192        OpKind::ArgMin,
193        OpKind::TopK,
194        OpKind::Sample,
195        OpKind::RngNormal,
196        OpKind::RngUniform,
197        OpKind::Conv,
198        OpKind::Im2Col,
199        OpKind::ConvTranspose2d,
200        OpKind::Conv3d,
201        OpKind::ConvTranspose3d,
202        OpKind::Pool,
203        OpKind::ReluBackward,
204        OpKind::ActivationBackward,
205        OpKind::FakeQuantizeBackward,
206        OpKind::ComplexNormSq,
207        OpKind::ComplexNormSqBackward,
208        OpKind::Conjugate,
209        OpKind::MaxPool2dBackward,
210        OpKind::Conv2dBackwardInput,
211        OpKind::Conv2dBackwardWeight,
212        OpKind::MaxPool3dBackward,
213        OpKind::Conv3dBackwardInput,
214        OpKind::Conv3dBackwardWeight,
215        OpKind::SoftmaxCrossEntropy,
216        OpKind::SoftmaxCrossEntropyWithLogits,
217        OpKind::SoftmaxCrossEntropyBackward,
218        OpKind::AttentionBackward,
219        OpKind::AttentionBackwardAll,
220        OpKind::LayerNormBackwardInput,
221        OpKind::LayerNormBackwardGamma,
222        OpKind::RmsNormBackwardInput,
223        OpKind::RmsNormBackwardGamma,
224        OpKind::RmsNormBackwardBeta,
225        OpKind::RopeBackward,
226        OpKind::GroupNormBackwardInput,
227        OpKind::GroupNormBackwardGamma,
228        OpKind::GroupNormBackwardBeta,
229        OpKind::BatchNormInferenceBackwardInput,
230        OpKind::BatchNormInferenceBackwardGamma,
231        OpKind::BatchNormInferenceBackwardBeta,
232        OpKind::CumsumBackward,
233        OpKind::GatherBackward,
234        OpKind::GroupedMatMul,
235        OpKind::DequantGroupedMatMul,
236        OpKind::DequantGroupedMatMulMlx,
237        OpKind::DequantMoEWeights,
238        OpKind::ScaledGroupedMatMul,
239        OpKind::ScatterAdd,
240        OpKind::ScatterNd,
241        OpKind::ScatterElements,
242        OpKind::GatherNd,
243        OpKind::GatherElements,
244        OpKind::LoraMatMul,
245        OpKind::PartitionedConv,
246        OpKind::DequantMatMul,
247        OpKind::SynthMatMul,
248        OpKind::SynthMatMulBackward,
249        OpKind::SynthReconstruct,
250        OpKind::SplineActivation,
251        OpKind::SplineActivationBackwardX,
252        OpKind::SplineActivationBackwardCoeff,
253        OpKind::QMatMul,
254        OpKind::QConv2d,
255        OpKind::ScaledMatMul,
256        OpKind::ScaledQuantize,
257        OpKind::ScaledQuantScale,
258        OpKind::ScaledDequantize,
259        OpKind::SelectiveScan,
260        OpKind::GatedDeltaNet,
261        OpKind::Lstm,
262        OpKind::Gru,
263        OpKind::Rnn,
264        OpKind::Mamba2,
265        OpKind::FusedSwiGLU,
266        OpKind::FusedMatMulBiasAct,
267        OpKind::FusedMatMulResidual,
268        OpKind::FusedConvBiasAct,
269        OpKind::FusedResidualLN,
270        OpKind::FusedResidualRmsNorm,
271        OpKind::FusedAttentionBlock,
272        OpKind::FusedTransformerLayer,
273        OpKind::If,
274        OpKind::While,
275        OpKind::Scan,
276        OpKind::ScanBackward,
277        OpKind::ScanBackwardXs,
278        OpKind::GaussianSplatRender,
279        OpKind::GaussianSplatRenderBackward,
280        OpKind::GaussianSplatPrepare,
281        OpKind::GaussianSplatRasterize,
282        OpKind::Custom,
283        OpKind::CustomFn,
284        OpKind::Fft,
285        OpKind::FftButterflyStage,
286        OpKind::LogMel,
287        OpKind::LogMelBackward,
288        OpKind::WelchPeaks,
289        OpKind::BiMap,
290        OpKind::ReEig,
291        OpKind::LogEig,
292        OpKind::SpdBatchNorm,
293        OpKind::SpdKarcherMean,
294        OpKind::ReEigBackward,
295        OpKind::LogEigBackward,
296        OpKind::SpdBatchNormBackwardX,
297        OpKind::SpdBatchNormBackwardG,
298        OpKind::SpdKarcherMeanWeighted,
299        OpKind::SpdLogMap,
300        OpKind::SpdExpMap,
301        OpKind::SpdParallelTransport,
302        OpKind::SpdMatrixFnBatch,
303        OpKind::SpdLogMapBackward,
304        OpKind::SpdExpMapBackward,
305        OpKind::SpdParallelTransportBackward,
306        OpKind::SpdMatrixFnBatchBackward,
307        OpKind::Eigh,
308        OpKind::EighBackward,
309        OpKind::EighBatch,
310        OpKind::EighBatchBackward,
311        OpKind::AdaLayerNorm,
312        OpKind::AdaLayerNormBackward,
313        OpKind::GatedResidual,
314        OpKind::GatedResidualBackward,
315    ];
316
317    /// Static capabilities of this op kind.
318    ///
319    /// Exhaustive by construction: a new [`OpKind`] variant will not compile
320    /// until it is classified here. If it fits no category, add it to the
321    /// final `OpCaps::NONE` arm — that is a deliberate statement that the
322    /// optimizer treats it as an opaque node, not an oversight.
323    pub const fn caps(self) -> OpCaps {
324        use OpKind::*;
325        match self {
326            Input | Param | Constant => OpCaps::LEAF,
327            Activation | Cast | Binary | Compare => {
328                OpCaps::ELEMENTWISE.union(OpCaps::CONST_FOLDABLE)
329            }
330            StopGradient | Where | Fma | ElementwiseRegion | BatchElementwiseRegion => {
331                OpCaps::ELEMENTWISE
332            }
333            MatMul
334            | DotGeneral
335            | DenseSolve
336            | BatchedDenseSolve
337            | Conv
338            | Im2Col
339            | ConvTranspose2d
340            | Conv3d
341            | ConvTranspose3d
342            | GroupedMatMul
343            | DequantGroupedMatMul
344            | DequantGroupedMatMulMlx
345            | DequantMoEWeights
346            | ScaledGroupedMatMul
347            | DequantMatMul
348            | SynthMatMul
349            | QMatMul
350            | QConv2d
351            | ScaledMatMul
352            | FusedMatMulResidual => OpCaps::BLAS.union(OpCaps::FUSION_BOUNDARY),
353            ResizeNearest2x => OpCaps::TRANSFORM,
354            Reshape | Expand => OpCaps::CONST_FOLDABLE,
355            Reduce | Softmax | TopK => OpCaps::REDUCTION,
356            Sample | RngNormal | RngUniform => OpCaps::NONDETERMINISTIC,
357            LoraMatMul | FusedMatMulBiasAct | FusedConvBiasAct => {
358                OpCaps::BLAS.union(OpCaps::FUSION_BOUNDARY.union(OpCaps::FUSED))
359            }
360            PartitionedConv
361            | SelectiveScan
362            | GatedDeltaNet
363            | Lstm
364            | Gru
365            | Rnn
366            | Mamba2
367            | FusedSwiGLU
368            | FusedResidualLN
369            | FusedResidualRmsNorm
370            | FusedAttentionBlock
371            | FusedTransformerLayer
372            | AdaLayerNorm
373            | GatedResidual => OpCaps::FUSED,
374            If | While | Scan | ScanBackward | ScanBackwardXs | CustomFn => OpCaps::NESTED_BODY,
375            GaussianSplatRender
376            | GaussianSplatRenderBackward
377            | GaussianSplatPrepare
378            | GaussianSplatRasterize => OpCaps::FUSION_BOUNDARY,
379            Quantize
380            | Dequantize
381            | FakeQuantize
382            | FakeQuantizeLSQ
383            | FakeQuantizeLSQBackwardX
384            | FakeQuantizeLSQBackwardScale
385            | TransformRegion
386            | Cholesky
387            | TriangularSolve
388            | Det
389            | LogDet
390            | Sort
391            | ArgSort
392            | Svd
393            | Qr
394            | LayerNorm
395            | LayerNorm2d
396            | GroupNorm
397            | BatchNormInference
398            | RmsNorm
399            | Interpolate3d
400            | Attention
401            | Rope
402            | AxialRope2d
403            | Transpose
404            | Narrow
405            | Concat
406            | KvAppend
407            | Gather
408            | Reverse
409            | Pad
410            | Slice
411            | Clamp
412            | Tile
413            | Trilu
414            | Histogram
415            | Cumsum
416            | CumProd
417            | CumMax
418            | ArgMax
419            | ArgMin
420            | Pool
421            | ReluBackward
422            | ActivationBackward
423            | FakeQuantizeBackward
424            | ComplexNormSq
425            | ComplexNormSqBackward
426            | Conjugate
427            | MaxPool2dBackward
428            | Conv2dBackwardInput
429            | Conv2dBackwardWeight
430            | MaxPool3dBackward
431            | Conv3dBackwardInput
432            | Conv3dBackwardWeight
433            | SoftmaxCrossEntropy
434            | SoftmaxCrossEntropyWithLogits
435            | SoftmaxCrossEntropyBackward
436            | AttentionBackward
437            | AttentionBackwardAll
438            | LayerNormBackwardInput
439            | LayerNormBackwardGamma
440            | RmsNormBackwardInput
441            | RmsNormBackwardGamma
442            | RmsNormBackwardBeta
443            | RopeBackward
444            | GroupNormBackwardInput
445            | GroupNormBackwardGamma
446            | GroupNormBackwardBeta
447            | BatchNormInferenceBackwardInput
448            | BatchNormInferenceBackwardGamma
449            | BatchNormInferenceBackwardBeta
450            | CumsumBackward
451            | GatherBackward
452            | ScatterAdd
453            | ScatterNd
454            | ScatterElements
455            | GatherNd
456            | GatherElements
457            | SynthMatMulBackward
458            | SynthReconstruct
459            | SplineActivation
460            | SplineActivationBackwardX
461            | SplineActivationBackwardCoeff
462            | ScaledQuantize
463            | ScaledQuantScale
464            | ScaledDequantize
465            | Custom
466            | Fft
467            | FftButterflyStage
468            | LogMel
469            | LogMelBackward
470            | WelchPeaks
471            | BiMap
472            | ReEig
473            | LogEig
474            | SpdBatchNorm
475            | SpdKarcherMean
476            | ReEigBackward
477            | LogEigBackward
478            | SpdBatchNormBackwardX
479            | SpdBatchNormBackwardG
480            | SpdKarcherMeanWeighted
481            | SpdLogMap
482            | SpdExpMap
483            | SpdParallelTransport
484            | SpdMatrixFnBatch
485            | SpdLogMapBackward
486            | SpdExpMapBackward
487            | SpdParallelTransportBackward
488            | SpdMatrixFnBatchBackward
489            | Eigh
490            | EighBackward
491            | EighBatch
492            | EighBatchBackward
493            | AdaLayerNormBackward
494            | GatedResidualBackward => OpCaps::NONE,
495        }
496    }
497
498    /// Convenience: does this kind have all of `caps`?
499    pub const fn has(self, caps: OpCaps) -> bool {
500        self.caps().contains(caps)
501    }
502}
503
504// ── Predicates ──────────────────────────────────────────────────
505//
506// These read off the table. They are kept as methods on `Op` because that is
507// where callers already reach for them; the classification itself lives in
508// one place.
509
510impl Op {
511    /// True if this op is element-wise (same shape in, same shape out).
512    /// Element-wise ops are prime fusion candidates.
513    pub fn is_elementwise(&self) -> bool {
514        self.kind().has(OpCaps::ELEMENTWISE)
515    }
516
517    /// True if this op may appear in a [`Op::TransformRegion`] chain.
518    pub fn is_transform_eligible(&self) -> bool {
519        self.kind().has(OpCaps::TRANSFORM)
520    }
521
522    /// True if this op is a BLAS/compute-intensive op that forms a fusion boundary.
523    pub fn is_blas(&self) -> bool {
524        self.kind().has(OpCaps::BLAS)
525    }
526
527    /// True if element-wise fusion must not span across this op.
528    pub fn is_fusion_boundary(&self) -> bool {
529        self.kind().has(OpCaps::FUSION_BOUNDARY)
530    }
531
532    /// True if this op is a reduction (drives loop iteration in fused kernels).
533    pub fn is_reduction(&self) -> bool {
534        self.kind().has(OpCaps::REDUCTION)
535    }
536
537    /// True if this op is a graph root with no tensor inputs.
538    pub fn is_leaf(&self) -> bool {
539        self.kind().has(OpCaps::LEAF)
540    }
541
542    /// True if the constant folder can evaluate this op on the host.
543    pub fn is_const_foldable(&self) -> bool {
544        self.kind().has(OpCaps::CONST_FOLDABLE)
545    }
546
547    /// True if this is a composite op with a decomposition into primitives.
548    pub fn is_fused(&self) -> bool {
549        self.kind().has(OpCaps::FUSED)
550    }
551
552    /// True if this op carries nested [`Graph`](crate::Graph) bodies.
553    /// Equivalent to `!self.subgraphs().is_empty()`, without the walk.
554    pub fn has_nested_body(&self) -> bool {
555        self.kind().has(OpCaps::NESTED_BODY)
556    }
557
558    /// True if the output is not a function of the inputs alone (RNG /
559    /// sampling), so it must never be folded, CSE'd or hoisted.
560    pub fn is_nondeterministic(&self) -> bool {
561        self.kind().has(OpCaps::NONDETERMINISTIC)
562    }
563}
564
565/// Every kind carrying [`OpCaps::FUSED`] — the set the backend rewriter tries
566/// to unfuse when a target lacks the native kernel.
567pub fn fused_kinds() -> Vec<OpKind> {
568    OpKind::ALL
569        .into_iter()
570        .filter(|k| k.has(OpCaps::FUSED))
571        .collect()
572}
573
574#[cfg(test)]
575mod tests {
576    use super::*;
577
578    #[test]
579    fn all_is_complete_and_duplicate_free() {
580        let mut seen = std::collections::HashSet::new();
581        for kind in OpKind::ALL {
582            assert!(seen.insert(kind), "{kind:?} appears twice in OpKind::ALL");
583        }
584        assert_eq!(seen.len(), N_KINDS);
585    }
586
587    #[test]
588    fn blas_implies_fusion_boundary() {
589        // The predicate tests `FUSION_BOUNDARY` directly, so the table must
590        // set both flags rather than relying on caller-side implication.
591        for kind in OpKind::ALL {
592            if kind.has(OpCaps::BLAS) {
593                assert!(
594                    kind.has(OpCaps::FUSION_BOUNDARY),
595                    "{kind:?} is BLAS but not a fusion boundary"
596                );
597            }
598        }
599    }
600
601    #[test]
602    fn leaves_are_not_elementwise() {
603        for kind in OpKind::ALL {
604            if kind.has(OpCaps::LEAF) {
605                assert!(!kind.has(OpCaps::ELEMENTWISE), "{kind:?}");
606                assert!(!kind.has(OpCaps::FUSION_BOUNDARY), "{kind:?}");
607            }
608        }
609    }
610
611    #[test]
612    fn nondeterministic_ops_are_never_const_foldable() {
613        // Folding an RNG op would bake one draw into the graph.
614        for kind in OpKind::ALL {
615            if kind.has(OpCaps::NONDETERMINISTIC) {
616                assert!(!kind.has(OpCaps::CONST_FOLDABLE), "{kind:?}");
617            }
618        }
619    }
620
621    #[test]
622    fn nested_body_flag_agrees_with_the_subgraph_accessor() {
623        // The flag is the cheap answer; `Op::subgraphs` is the real one. They
624        // must not drift — `has_nested_body` is used to decide whether to
625        // recurse at all.
626        let g = || Box::new(crate::Graph::new("b"));
627        let cases: Vec<Op> = vec![
628            Op::MatMul,
629            Op::StopGradient,
630            Op::If {
631                then_branch: g(),
632                else_branch: g(),
633            },
634            Op::While {
635                cond: g(),
636                body: g(),
637                max_iterations: None,
638            },
639            Op::Scan {
640                body: g(),
641                length: 1,
642                save_trajectory: false,
643                num_bcast: 0,
644                num_xs: 0,
645                num_checkpoints: 0,
646            },
647            Op::CustomFn {
648                fwd_body: g(),
649                vjp_body: None,
650                jvp_body: None,
651                num_inputs: 1,
652            },
653        ];
654        for op in cases {
655            assert_eq!(
656                op.has_nested_body(),
657                !op.subgraphs().is_empty(),
658                "{:?} disagrees with its subgraph accessor",
659                op.kind()
660            );
661        }
662    }
663
664    #[test]
665    fn spot_check_known_classifications() {
666        assert!(OpKind::Input.has(OpCaps::LEAF));
667        assert!(OpKind::Activation.has(OpCaps::ELEMENTWISE.union(OpCaps::CONST_FOLDABLE)));
668        assert!(OpKind::MatMul.has(OpCaps::BLAS));
669        assert!(OpKind::Reduce.has(OpCaps::REDUCTION));
670        assert!(OpKind::Scan.has(OpCaps::NESTED_BODY));
671        assert!(OpKind::RngNormal.has(OpCaps::NONDETERMINISTIC));
672        assert!(OpKind::FusedSwiGLU.has(OpCaps::FUSED));
673        // Reshape is host-foldable but not elementwise — a shape-only op.
674        assert!(OpKind::Reshape.has(OpCaps::CONST_FOLDABLE));
675        assert!(!OpKind::Reshape.has(OpCaps::ELEMENTWISE));
676        // Transpose is opaque to the optimizer's categories.
677        assert!(OpKind::Transpose.caps().is_empty());
678    }
679
680    #[test]
681    fn fused_kinds_matches_the_flag() {
682        let listed = fused_kinds();
683        assert!(!listed.is_empty());
684        for kind in &listed {
685            assert!(kind.has(OpCaps::FUSED));
686        }
687        assert_eq!(
688            listed.len(),
689            OpKind::ALL.iter().filter(|k| k.has(OpCaps::FUSED)).count()
690        );
691    }
692}