22# SPDX-License-Identifier: Apache-2.0
33from __future__ import annotations
44
5+ import operator
6+ from functools import partial
57from typing import TYPE_CHECKING
68
79from numba_cuda_mlir import types
810from numba_cuda_mlir ._mlir import ir as mlir_ir
9- from numba_cuda_mlir ._mlir .dialects import llvm
10- from numba_cuda_mlir .extending import lowering_registry
11+ from numba_cuda_mlir ._mlir .dialects import arith , llvm
12+ from numba_cuda_mlir .extending import lower_cast , lowering_registry
1113from numba_cuda_mlir .lowering_utilities import convert
1214from numba_cuda_mlir .models import PrimitiveModel , register_model
1315
1416from cudf .core .udf .api import Masked
15- from cudf .core .udf .mlir_backend .masked_typing import MaskedType
17+ from cudf .core .udf .mlir_backend .masked_typing import (
18+ MaskedType ,
19+ NAType ,
20+ na_type ,
21+ )
1622
1723if TYPE_CHECKING :
1824 from numba_cuda_mlir .mlir_lowering import MLIRLower
@@ -62,6 +68,21 @@ def __init__(
6268 super ().__init__ (data_model_manager , masked_type , struct_type )
6369
6470
71+ def _extract_masked_value_valid (struct_val , value_mlir_ty , valid_ty ):
72+ """Pull the ``(value, valid)`` SSA values out of a ``Masked`` struct."""
73+ v = llvm .extractvalue (
74+ res = value_mlir_ty ,
75+ container = struct_val ,
76+ position = mlir_ir .DenseI64ArrayAttr .get ([0 ]),
77+ )
78+ valid = llvm .extractvalue (
79+ res = valid_ty ,
80+ container = struct_val ,
81+ position = mlir_ir .DenseI64ArrayAttr .get ([1 ]),
82+ )
83+ return v , valid
84+
85+
6586def _lower_masked_constructor (
6687 builder : MLIRLower , target : Var , args : list [Var ], kwargs : list
6788) -> None :
@@ -105,6 +126,63 @@ def _lower_masked_getattr(
105126 builder .store_var (target , convert (field_value , target_mlir_ty ))
106127
107128
129+ # ``cast(NA -> Masked)`` and ``cast(scalar -> Masked)``. Both build a Masked
130+ # struct for the target's value type; they differ only in the payload (NA has
131+ # none, so use undef) and the validity bit (NA -> invalid, scalar -> valid).
132+ # Triggered by branch unification, e.g. ``return x if cond else cudf.NA`` or
133+ # ``return 5``.
134+ def _cast_to_masked (context , builder , from_ty , to_ty , val ):
135+ value_mlir_ty = builder .get_mlir_type (to_ty .value_type )
136+ if isinstance (from_ty , NAType ):
137+ value = llvm .UndefOp (value_mlir_ty )
138+ valid = 0
139+ else :
140+ value = convert (val , value_mlir_ty )
141+ valid = 1
142+ valid_const = arith .constant (
143+ result = builder .get_mlir_type (types .boolean ), value = valid
144+ )
145+ return _pack_masked (builder , to_ty , value , valid_const )
146+
147+
148+ # ``cast(Masked -> Masked)``: branch unification across different
149+ # inner widths (e.g. one branch returns Masked(int32), another
150+ # returns Masked(float64); Numba unifies to Masked(float64)).
151+ # Promote the payload, preserve the validity bit.
152+ def _cast_masked_to_masked (context , builder , from_ty , to_ty , val ):
153+ if from_ty .value_type == to_ty .value_type :
154+ return val
155+ st = llvm .StructType (val .type )
156+ m_val , m_valid = _extract_masked_value_valid (val , st .body [0 ], st .body [1 ])
157+ value_mlir_ty = builder .get_mlir_type (to_ty .value_type )
158+ casted = convert (m_val , value_mlir_ty )
159+ return _pack_masked (builder , to_ty , casted , m_valid )
160+
161+
162+ # ``is``/``is not`` against NA are registered for both operand orders
163+ # (``m is NA`` and ``NA is m``), so find the MaskedType operand rather than
164+ # assuming which position it is in.
165+ def _masked_operand (builder , args ):
166+ """Return the ``MaskedType`` operand of a ``Masked``/``NA`` comparison."""
167+ for arg in args :
168+ if isinstance (builder .get_numba_type (arg .name ), MaskedType ):
169+ return arg
170+ raise TypeError ("expected a MaskedType operand" )
171+
172+
173+ # ``is``/``is not`` against NA both reduce to the validity bit: ``m is NA`` ->
174+ # ``not m.valid`` and ``m is not NA`` -> ``m.valid``. Registered for both
175+ # operators (and operand orders) via partials below.
176+ def _lower_masked_na_compare (builder , target , args , kwargs , * , is_null ):
177+ m = builder .load_var (_masked_operand (builder , args ))
178+ st = llvm .StructType (m .type )
179+ _ , valid = _extract_masked_value_valid (m , st .body [0 ], st .body [1 ])
180+ if is_null :
181+ one = arith .constant (valid .type , 1 )
182+ valid = arith .xori (valid , one )
183+ builder .store_var (target , valid )
184+
185+
108186def _register () -> None :
109187 """Register the data model and lowerings with ``numba_cuda_mlir``.
110188
@@ -123,5 +201,17 @@ def _register() -> None:
123201
124202 lowering_registry .lower_getattr_generic (MaskedType )(_lower_masked_getattr )
125203
204+ lower_cast (na_type , MaskedType )(_cast_to_masked )
205+ for _scalar_cls in (types .Integer , types .Float , types .Boolean ):
206+ lower_cast (_scalar_cls , MaskedType )(_cast_to_masked )
207+ lower_cast (MaskedType , MaskedType )(_cast_masked_to_masked )
208+
209+ is_na = partial (_lower_masked_na_compare , is_null = True )
210+ is_not_na = partial (_lower_masked_na_compare , is_null = False )
211+ lower (operator .is_ , MaskedType , NAType )(is_na )
212+ lower (operator .is_ , NAType , MaskedType )(is_na )
213+ lower (operator .is_not , MaskedType , NAType )(is_not_na )
214+ lower (operator .is_not , NAType , MaskedType )(is_not_na )
215+
126216
127217_register ()
0 commit comments