1#  Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
2#  See https://llvm.org/LICENSE.txt for license information.
3#  SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4
5"""Experimental MLIR-PyTACO with sparse tensor support.
6
7See http://tensor-compiler.org/ for TACO tensor compiler.
8
9This module implements the Python classes for PyTACO index notation. These
10include classes for data types, tensor dimension formats (aka mode formats),
11tensor dimension orderings (aka mode ordering), tensor storage formats, and
12tensors.
13
14The PyTACO API doesn't follow the naming conversion required by the style guide
15for this module. As such, we first implement the supporting classes and routines
16following the style guide, and then define the type aliases and constants to
17support the PyTACO API in the pytaco_api module.
18"""
19
20from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union
21
22import abc
23import ctypes
24import dataclasses
25import enum
26import numpy as np
27import functools
28import operator
29import os
30import threading
31
32# Import MLIR related modules.
33from mlir import execution_engine
34from mlir import ir
35from mlir import runtime
36from mlir.dialects import arith
37from mlir.dialects import bufferization
38from mlir.dialects import builtin
39from mlir.dialects import func
40from mlir.dialects import linalg
41from mlir.dialects import sparse_tensor
42from mlir.dialects.linalg.opdsl import lang
43
44from . import mlir_pytaco_utils as utils
45
46# TACO naming prefixes.
47_TACO_INDEX_PREFIX = "i"
48_TACO_TENSOR_PREFIX = "A"
49
50# Bitwidths for pointers and indices.
51_POINTER_BIT_WIDTH = 0
52_INDEX_BIT_WIDTH = 0
53# The entry point to the JIT compiled program.
54_ENTRY_NAME = "main"
55
56# Type aliases for type annotation.
57_UnaryOp = Callable[[Any], Any]
58_BinaryOp = Callable[[Any, Any], Any]
59_ExprVisitor = Callable[..., None]
60_ExprInfoDict = Dict["IndexExpr", "_ExprInfo"]
61_LogicalOp = Callable[[bool, bool], bool]
62_ModeFormatOp = Callable[["ModeFormat", "ModeFormat"], "ModeFormat"]
63_SubtreeLeafChecker = Optional[Callable[..., bool]]
64
65
66class Type(enum.Enum):
67  """The data types supported by TACO.
68
69  We use numpy data types to implement the enum data types.
70  """
71  INT8 = np.int8
72  INT16 = np.int16
73  INT32 = np.int32
74  INT64 = np.int64
75  # numpy _ctype_from_dtype_scalar can't handle np.float16 yet.
76  FLOAT32 = np.float32
77  FLOAT64 = np.float64
78
79
80# All floating point type enums.
81_FLOAT_TYPES = (Type.FLOAT32, Type.FLOAT64)
82# All integral type enums.
83_INT_TYPES = (Type.INT8, Type.INT16, Type.INT32, Type.INT64)
84# Type alias for any numpy type used to implement the runtime support for the
85# enum data types.
86_AnyRuntimeType = Union[np.int8, np.int16, np.int32, np.int64, np.float32,
87                        np.float64]
88
89
90@dataclasses.dataclass(frozen=True)
91class DType:
92  """The data type class.
93
94  We support the TACO API dtype class with an alias of this class.
95
96  The following methods are defined by the TACO API:
97    is_float: Returns whether the data type represents a floating point value.
98    is_int:   Returns whether the data type represents an integral value.
99
100  Attributes:
101    kind: A Type enum representing the data type.
102    value: The numpy data type for the TACO data type.
103  """
104  kind: Type = Type.FLOAT32
105
106  def is_float(self) -> bool:
107    """Returns whether the data type represents a floating point value."""
108    return self.kind in _FLOAT_TYPES
109
110  def is_int(self) -> bool:
111    """Returns whether the data type represents an integral value."""
112    return self.kind in _INT_TYPES
113
114  @property
115  def value(self) -> _AnyRuntimeType:
116    """Returns the numpy dtype for the data type."""
117    return self.kind.value
118
119
120def _dtype_to_mlir_str(dtype: DType) -> str:
121  """Returns the MLIR string for the given dtype."""
122  dtype_to_str = {
123      Type.INT16: "i8",
124      Type.INT16: "i16",
125      Type.INT32: "i32",
126      Type.INT64: "i64",
127      Type.FLOAT32: "f32",
128      Type.FLOAT64: "f64"
129  }
130  return dtype_to_str[dtype.kind]
131
132
133def _nptype_to_taco_type(ty: np.dtype) -> DType:
134  """Returns the TACO type for the given numpy type."""
135  nptype_to_dtype = {
136      np.int8: Type.INT8,
137      np.int16: Type.INT16,
138      np.int32: Type.INT32,
139      np.int64: Type.INT64,
140      np.float32: Type.FLOAT32,
141      np.float64: Type.FLOAT64
142  }
143  return DType(nptype_to_dtype[ty])
144
145
146def _mlir_type_from_taco_type(dtype: DType) -> ir.Type:
147  """Returns the MLIR type corresponding to the given TACO type."""
148  dtype_to_irtype = {
149      Type.INT8: ir.IntegerType.get_signless(8),
150      Type.INT16: ir.IntegerType.get_signless(16),
151      Type.INT32: ir.IntegerType.get_signless(32),
152      Type.INT64: ir.IntegerType.get_signless(64),
153      Type.FLOAT32: ir.F32Type.get(),
154      Type.FLOAT64: ir.F64Type.get()
155  }
156  return dtype_to_irtype[dtype.kind]
157
158def _ctype_pointer_from_array(array: np.ndarray) -> ctypes.pointer:
159  """Returns the ctype pointer for the given numpy array."""
160  return ctypes.pointer(
161      ctypes.pointer(runtime.get_ranked_memref_descriptor(array)))
162
163
164class ModeFormat(enum.Enum):
165  """The tensor dimension storage format class.
166
167  We support the TACO API mode_format class with an alias of this class.
168
169  In TACO, a tensor dimension is called a mode and the storage format for a
170  tensor dimension is called a mode format.
171  """
172  DENSE = sparse_tensor.DimLevelType.dense
173  COMPRESSED = sparse_tensor.DimLevelType.compressed
174
175
176def _mode_format_operation(a: ModeFormat, b: ModeFormat,
177                           op: _LogicalOp) -> ModeFormat:
178  """Implements the given operator on ModeFormat."""
179  return (ModeFormat.COMPRESSED
180          if op(a == ModeFormat.COMPRESSED, b == ModeFormat.COMPRESSED) else
181          ModeFormat.DENSE)
182
183
184def _mode_format_estimator(op: _BinaryOp) -> _ModeFormatOp:
185  """Produces a ModeFormat operator for the given binary operator.
186
187  The ModeFormat operator is used as a heuristic to derive the destination
188  dimension sparsity from the source dimension sparsity. In particular, if the
189  binary operator produces a disjunction of the zero values from its source
190  operands, such as the MUL operator, we return a ModeFormat operator that
191  uses operator.or_. That is, we estimate that a dimension for the MUL
192  operation result to be sparse if either of its source operands is sparse.
193
194  On the other hand, if the binary operator produces a conjunction of the
195  zero values from its source operands, such as the ADD operator, we return
196  a ModeFormat operator that uses operator.and_. In this case, we estimate
197  that a dimension for the ADD operation result to be sparse if both of its
198  source operands are sparse.
199
200  Args:
201    op: A _BinaryOp object representing a supporting operator on tensors.
202
203  Returns:
204    A ModeFormatOp for estimating the destination dimension sparsity from
205    the source dimension sparsity.
206  """
207  conjunction = functools.partial(_mode_format_operation, op=operator.and_)
208  disjunction = functools.partial(_mode_format_operation, op=operator.or_)
209  return conjunction if op(0, 1) != 0 else disjunction
210
211
212def _all_instance_of(collection: Iterable, cls: Any) -> bool:
213  """Returns true if all elements of the iterable is an instance of cls."""
214  return all(isinstance(e, cls) for e in collection)
215
216
217def _identity_ordering(rank: int) -> List[int]:
218  """Returns the identity ordering for tensor of given rank."""
219  return list(range(rank))
220
221
222@dataclasses.dataclass(frozen=True)
223class ModeOrdering:
224  """The tensor dimension ordering class.
225
226  We support the TACO API mode_ordering class with an alias of this class.
227
228  Attributes:
229    ordering: A list of integers representing the ordering of the tensor
230      dimensions.
231  """
232  ordering: List[int]
233
234  def __post_init__(self) -> None:
235    """Verifies the value in ordering.
236
237    Raises:
238       ValueError: If ordering is not a list of integers.
239    """
240    if (not isinstance(self.ordering, list) or
241        not _all_instance_of(self.ordering, int)):
242      raise ValueError("Ordering must be a list of integers: "
243                       f"{self.ordering}")
244    # Check that ordering is a permutation of the dimension numbers.
245    if sorted(self.ordering) != _identity_ordering(self.rank()):
246      raise ValueError(f"Invalid ordering: {self.ordering} != "
247                       f"permutation{_identity_ordering(self.rank())}.")
248
249  def rank(self) -> int:
250    """Returns the number of dimensions represented by the ordering."""
251    return len(self.ordering)
252
253
254@dataclasses.dataclass(frozen=True)
255class ModeFormatPack:
256  """The tensor dimension format class.
257
258  We support the TACO API mode_format_pack class with an alias of this class.
259
260  The storage format of a tensor contains one mode_format for each tensor
261  dimension.
262
263  Attributes:
264    formats: A list of ModeFormat representing the storage format for each of
265      the tensor dimension.
266  """
267  formats: List[ModeFormat]
268
269  def __post_init__(self) -> None:
270    """Verifies the value in formats.
271
272    Raises:
273       ValueError: If formats is not a list of ModeFormats.
274    """
275    if (not isinstance(self.formats, list) or
276        not _all_instance_of(self.formats, ModeFormat)):
277      raise ValueError("Formats must be a list of ModeFormat: "
278                       f"{self.formats}")
279
280  def rank(self) -> int:
281    """Returns the number of dimensions represented by the format pack."""
282    return len(self.formats)
283
284
285@dataclasses.dataclass
286class Format:
287  """The tensor format class defined by the TACO API.
288
289  Attributes:
290    format_pack: A ModeFormatPack representing the storage format for the tensor
291      dimensions.
292    ordering: A ModeOrdering representing the tensor dimension ordering in the
293      storage.
294  """
295  format_pack: ModeFormatPack
296  ordering: Optional[ModeOrdering] = None
297
298  def __post_init__(self) -> None:
299    """Verifies and fixes up the values in format_pack and ordering.
300
301    Verifies and fixes up the values in format_pack and ordering to supports the
302    initializer syntax defined by the TACO API. If format_pack is a list of
303    ModeFormat, replaces it with ModeFormatPack constructed from the list. If
304    ordering is not provided, set ordering to the natural ordering for the rank
305    corresponding to format_pack.
306
307    Raises:
308       ValueError: If format_pack is not an instance of ModeFormatPack or if
309         ordering is not an instance of ModeOrdering.
310    """
311    if isinstance(self.format_pack, list):
312      if not _all_instance_of(self.format_pack, ModeFormat):
313        raise ValueError(f"Expected a list of ModeFormat: {self.format_pack}")
314      self.format_pack = ModeFormatPack(self.format_pack)
315    if not isinstance(self.format_pack, ModeFormatPack):
316      raise ValueError(f"Expected ModeFormatpack: {self.format_pack}")
317
318    if self.ordering is None:
319      self.ordering = ModeOrdering(list(range(self.rank())))
320    if isinstance(self.ordering, list):
321      if not _all_instance_of(self.ordering, int):
322        raise ValueError(f"Expected a list of integer: {self.ordering}")
323      self.ordering = ModeOrdering(self.ordering)
324    if not isinstance(self.ordering, ModeOrdering):
325      raise ValueError(f"Expected ModeOrdering: {self.ordering}")
326
327    if self.format_pack.rank() != self.ordering.rank():
328      raise ValueError("Inconsistent ModeFormatPack and ModeOrdering: "
329                       f"len({self.format_pack}) != "
330                       f"len({self.ordering})")
331
332  def rank(self) -> int:
333    """Returns the number of dimensions represented by the format."""
334    return self.format_pack.rank()
335
336  def get_permutation_and_sparsity(self) -> Tuple[np.ndarray, np.ndarray]:
337    """Constructs the numpy arrays for the permutation and sparsity."""
338    perm = np.array(self.ordering.ordering, dtype=np.ulonglong)
339    a = [0 if s == ModeFormat.DENSE else 1 for s in self.format_pack.formats]
340    sparse = np.array(a, dtype=np.uint8)
341    return (perm, sparse)
342
343  def mlir_tensor_attr(self) -> Optional[sparse_tensor.EncodingAttr]:
344    """Constructs the MLIR attributes for the tensor format."""
345    order = (
346        range(self.rank()) if
347        (self.ordering is None) else self.ordering.ordering)
348    mlir_storage_format = [f.value for f in self.format_pack.formats]
349    return sparse_tensor.EncodingAttr.get(mlir_storage_format,
350                                          ir.AffineMap.get_permutation(order),
351                                          _POINTER_BIT_WIDTH, _INDEX_BIT_WIDTH)
352
353
354def _make_format(formats: List[ModeFormat],
355                 ordering: Optional[List[int]] = None) -> Format:
356  """Constructs a format from a list of ModeFormat and an optional ordering.
357
358  Args:
359    formats: A list of ModeFormat, one for each dimension of a tensor.
360    ordering: An optional list of integer, for the ordering of the tensor
361      dimensions. When an ordering is not given, the identity ordering is used.
362
363  Returns:
364    A tensor format object.
365
366  Raises:
367    ValueError: If formats is not a list of ModeFormat or the length of formats
368      is not consistent with the len of ordering.
369  """
370  ordering = ordering or _identity_ordering(len(formats))
371  return Format(ModeFormatPack(formats), ModeOrdering(ordering))
372
373
374class IndexExpr(abc.ABC):
375  """The index notation base class.
376
377  We support the TACO API index_expression class with an alias of this class.
378  """
379
380  def _verify_operand_and_build_expr(self, rhs, op: _BinaryOp) -> "_BinaryExpr":
381    """Verifies the RHS operand and returns a binary expression.
382
383    Args:
384      rhs: The RHS of the binary operation, which could be any Python object
385        from user inputs.
386      op: A _BinaryOp object representing the binary operator.
387
388    Raises:
389      ValueError: If rhs is not an IndexExpr.
390    """
391    if not isinstance(rhs, IndexExpr):
392      raise ValueError(f"Expected IndexExpr: {rhs}")
393    return _BinaryExpr(op, self, rhs)
394
395  def _build_unary_expr(self, op: _UnaryOp) -> "_UnaryExpr":
396    """Build a unary expression.
397
398    Args:
399      op: A _UnaryOp object representing the unary operation.
400    """
401    return _UnaryExpr(op, self)
402
403  def __add__(self, rhs) -> "_BinaryExpr":
404    """Defines the operator +.
405
406    Args:
407      rhs: The value being added, which could be any Python object from user
408        inputs.
409
410    Returns:
411      A _BinaryExpr object representing the operation.
412
413    Raises:
414      ValueError: If rhs is not an IndexExpr.
415    """
416    return self._verify_operand_and_build_expr(rhs, operator.add)
417
418  def __mul__(self, rhs) -> "_BinaryExpr":
419    """Defines the operator *.
420
421    Args:
422      rhs: The value being multiplied, which could be any Python object from
423        user inputs.
424
425    Returns:
426      A _BinaryExpr object representing the operation.
427
428    Raises:
429      ValueError: If rhs is not an IndexExpr.
430    """
431    return self._verify_operand_and_build_expr(rhs, operator.mul)
432
433  def __abs__(self) -> "_UnaryExpr":
434    """Defines the operator abs.
435
436    Returns:
437      A _UnaryExpr object representing the operation.
438    """
439    return self._build_unary_expr(operator.abs)
440
441  def __neg__(self) -> "_UnaryExpr":
442    """Defines the operator neg.
443
444    Returns:
445      A _UnaryExpr object representing the operation.
446    """
447    return self._build_unary_expr(operator.neg)
448
449  def __sub__(self, rhs) -> "_BinaryExpr":
450    """Defines the operator -.
451
452    Args:
453      rhs: The value being subtracted, which could be any Python object from
454        user inputs.
455
456    Returns:
457      A _BinaryExpr object representing the operation.
458
459    Raises:
460      ValueError: If rhs is not an IndexExpr.
461    """
462    return self._verify_operand_and_build_expr(rhs, operator.sub)
463
464  @abc.abstractmethod
465  def _visit(self,
466             func: _ExprVisitor,
467             args,
468             *,
469             leaf_checker: _SubtreeLeafChecker = None) -> None:
470    """A post-order visitor.
471
472    Args:
473      func: A callable applied to each node in the expression tree.
474      args: The variable-length arguments passed to the callable. These
475        arguments are grouped as an iterable and will be unpacked before passing
476        to the callable. This is to enable the keyword argument only syntax
477        after this argument.
478      leaf_checker: A callable object to identify nodes that should be treated
479        as leaf nodes to support partial tree visiting.
480    """
481    pass
482
483  @abc.abstractmethod
484  def _emit_expression(
485      self,
486      expr_to_opnd: Dict["IndexExpr", lang.OperandDef],
487      expr_to_info: _ExprInfoDict,
488  ) -> lang.ScalarExpression:
489    """Emits MLIR for the expression tree.
490
491    Args:
492      expr_to_opnd: A dictionary for looking up structured op input operands for
493        the input nodes of the structured op.
494      expr_to_info: A dictionary for looking up code generation information for
495        expressions.
496
497    Returns:
498      A linalg dialect ScalarExpression for the expression.
499    """
500    pass
501
502  @abc.abstractmethod
503  def dtype(self) -> DType:
504    """Returns the data type for the result of the expression."""
505    pass
506
507  def _emit_structured_op(self, expr_to_info: _ExprInfoDict) -> None:
508    """Emits a structured op in the linalg dialect for the expression tree.
509
510    We define a DefineOpcallable in the domain specific language for the linalg
511    dialect and execute the callable to generate the structured op. Self is the
512    root of the expression tree for the structured op.
513
514    Args:
515      expr_to_info: A dictionary for looking up code generation information for
516        expressions.
517    """
518    op_info = expr_to_info[self].structop_info
519    op_name = op_info.dst_name
520    op_def = lang.LinalgOpDef(name=op_name)
521    op_callable = lang.DefinedOpCallable(op_name, op_def)
522
523    # Collect the input expression nodes for the structured op.
524    expr_inputs = []
525    self._visit(
526        _gather_structured_op_input,
527        (self, expr_to_info, expr_inputs),
528        leaf_checker=_is_structured_op_leaf,
529    )
530
531    # Create a linalg structured op operand for each input expression node and
532    # build a dictionary for looking up the information.
533    expr_to_input_opnd = {
534        e: _emit_structured_op_input(e, expr_to_info, op_def)
535        for e in expr_inputs
536    }
537
538    # Emit the expression tree, which produces the value assigned to the
539    # destination tensor.
540    value = self._emit_expression(expr_to_input_opnd, expr_to_info)
541    # Emit the structured op representation for the destination tensor.
542    dst_opnd = _emit_operand(op_def, op_info.dst_indices, op_info.dst_name,
543                             lang.OperandKind.OUTPUT_TENSOR)
544    dst_dim_syms = _mlir_dimensions_from_index_vars(op_info.dst_indices)
545    dst_use = lang.TensorUse(dst_opnd, dst_dim_syms)
546
547    expr_info = expr_to_info[self]
548    # If the structured op reduces some indices, explicitly represent the
549    # reduction. This is done by generating a ReduceFn for the dimensions being
550    # reduced in the linalg dialect and calling the function with the value
551    # being reduced. We only support add reduction currently.
552    if expr_info.reduce_indices:
553      reduce_dims = _mlir_dimensions_from_index_vars(expr_info.reduce_indices)
554      value = lang.ReduceFn.add[reduce_dims](value)
555
556    # Emit the assignment as a comprehension in the linalg dialect.
557    comp = lang.Comprehension((dst_use, value))
558    op_def.comprehensions.append(comp)
559
560    # The structured op in the linalg dialect requires an explicit
561    # initialization for the destination tensor. Emit MLIR to initialize the
562    # destination tensor.
563    init = op_info.emit_tensor_init()
564
565    # Collect MLIR values for the linalg input operands, with the assumption
566    # that dictionary preserves the insertion order.
567    args = [
568        expr_to_info[expr].mlir_value
569        for expr, opnd in expr_to_input_opnd.items()
570    ]
571    # Execute the DefineOpcallable object for the linalg dialect operation to
572    # emit MLIR for the linalg structured op.
573    expr_info.mlir_value = op_callable(*args, outs=[init])
574
575  def _identify_structured_ops(
576      self,
577      expr_to_info: _ExprInfoDict,
578      dst: "Tensor",
579      dst_indices: Tuple["IndexVar", ...],
580  ) -> List["IndexExpr"]:
581    """Returns expression nodes for the roots of the identified structured ops.
582
583    A structured op in the linalg dialect only supports reduction performed on
584    the whole expression. If the expression tree contains reduction that are
585    performed on part of the expression tree, the expression tree needs to be
586    implemented with multiple structured ops. This routine identifies all the
587    expression nodes that contain reduction as the root of structured ops in the
588    linalg dialect.
589
590    Args:
591      expr_to_info: A dictionary for looking up code generation information for
592        expressions.
593      dst: A destination Tensor that accepts the value of the expression tree.
594      dst_indices: The indices used by the destination index expression.
595
596    Returns:
597      An ordered list of IndexExpr for the root expressions of the structured
598      ops, where child expressions go before parent expressions that use their
599      results.
600    """
601    reduce_indices = tuple(
602        set(expr_to_info[self].src_indices) - set(dst_indices))
603    for reduce_index in reduce_indices:
604      _mark_structured_op_root(self, reduce_index, expr_to_info)
605
606    self._visit(_accumulate_reduce_indices, (expr_to_info,))
607    structop_roots = []
608    self._visit(_gather_structured_op, (expr_to_info, structop_roots))
609
610    # Handle the root of the top level expression.
611    if not structop_roots or structop_roots[-1] != self:
612      # The top level expression is not a reduction. Add the top level
613      # expression as a structured op root.
614      structop_roots.append(self)
615
616    # Use user specified information for the destination tensor to build an
617    # _StructOpInfo for the top level expression.
618    expr_to_info[self].structop_info = _StructOpInfo(dst_indices,
619                                                     tuple(dst.shape),
620                                                     dst.dtype, dst.name,
621                                                     dst.format)
622
623    return structop_roots
624
625  def _validate_and_collect_expr_info(
626      self,
627      dst: "Tensor",
628      dst_indices: Tuple["IndexVar", ...],
629  ) -> _ExprInfoDict:
630    """Propagates expression information for validation.
631
632    Propagates the indices used by child expression nodes to parent expression
633    nodes. Also collects and validates the sizes for the dimensions
634    corresponding to the indices.
635
636    Args:
637      dst: A destination Tensor that accepts the value of the expression tree.
638      dst_indices: The indices used by the destination index expression.
639
640    Raises:
641      ValueError if there is any inconsistency in indices or dimensional
642      values.
643
644    Returns:
645      A dictionary of (IndexExpr, _ExprInfo).
646    """
647    expr_to_info = {}
648    # Validate the expression tree and construct expression information.
649    self._visit(_validate_and_collect_expr_info, (expr_to_info,))
650
651    # Validate the destination dimension information.
652    info = expr_to_info[self]
653    index_to_dim_info = {i: d for i, d in zip(info.src_indices, info.dim_infos)}
654    for i, d, in zip(dst_indices, dst.shape):
655      if i not in index_to_dim_info:
656        raise ValueError("Destination IndexVar not used in the "
657                         f"source expression: {i}")
658      else:
659        if d != index_to_dim_info[i].dim and index_to_dim_info[i].dim != -1:
660          raise ValueError(f"Inconsistent destination dimension for {i}: "
661                           f"{d} vs {index_to_dim_info[i].dim}")
662
663    return expr_to_info
664
665  def _emit_assignment(
666      self,
667      module: ir.Module,
668      dst: "Tensor",
669      dst_indices: Tuple["IndexVar", ...],
670      expr_to_info: _ExprInfoDict,
671      input_accesses: List["Access"],
672  ) -> None:
673    """Emits an MLIR function for assigning the expression to a tensor."""
674    input_types = [a.tensor.mlir_tensor_type() for a in input_accesses]
675
676    # Build the kernel for the operations.
677    with ir.InsertionPoint(module.body):
678
679      @func.FuncOp.from_py_func(*input_types, name=_ENTRY_NAME)
680      def linalg_funcop(*args):
681        # Set up the mapping from the Access nodes to their MLIR values.
682        for e, mlir in zip(input_accesses, args):
683          expr_to_info[e].mlir_value = mlir
684
685        # Emit structured ops in the linalg dialect to implement the assignment.
686        for structop_root in self._identify_structured_ops(
687            expr_to_info, dst, dst_indices):
688          structop_root._emit_structured_op(expr_to_info)
689          dst._record_stats(expr_to_info[structop_root].structop_info)
690
691        # The function returns the MLIR value of the root expression.
692        return expr_to_info[self].mlir_value
693
694      linalg_funcop.func_op.attributes[
695          "llvm.emit_c_interface"] = ir.UnitAttr.get()
696
697  def get_input_accesses(self) -> List["Access"]:
698    """Compute the list of input accesses for the expression."""
699    input_accesses = []
700    self._visit(_gather_input_accesses_index_vars, (input_accesses,))
701    return input_accesses
702
703  def compile(
704      self,
705      dst: "Tensor",
706      dst_indices: Tuple["IndexVar", ...],
707  ) -> execution_engine.ExecutionEngine:
708    """Compiles the tensor assignment dst[dst_indices] = expression.
709
710    Args:
711      dst: The destination tensor.
712      dst_indices: The tuple of IndexVar used to access the destination tensor.
713
714    Returns:
715      The execution engine for the tensor assignment.
716
717    Raises:
718      ValueError: If the expression is not proper or not supported.
719    """
720    expr_to_info = self._validate_and_collect_expr_info(dst, dst_indices)
721    input_accesses = self.get_input_accesses()
722
723    # Build and compile the module to produce the execution engine.
724    with ir.Context(), ir.Location.unknown():
725      module = ir.Module.create()
726      self._emit_assignment(module, dst, dst_indices, expr_to_info,
727                            input_accesses)
728      engine = utils.compile_and_build_engine(module)
729
730    return engine
731
732
733class _AtomicCounter:
734  """An atomic counter."""
735
736  def __init__(self):
737    self._counter = 0
738    self._counter_lock = threading.Lock()
739
740  def increment(self) -> int:
741    """Increments the counter by one and returns the old value."""
742    old_value = self._counter
743    with self._counter_lock:
744      self._counter = self._counter + 1
745    return old_value
746
747
748class IndexVar(IndexExpr):
749  """The tensor index class.
750
751  We support the TACO API index_var class with an alias of this class.
752
753  An IndexVar object represents an index variable in tensor index notation.
754
755  Attributes:
756    name: A unique string name of the IndexVar.
757  """
758  _counter = _AtomicCounter()
759
760  def __init__(self):
761    id = self._counter.increment()
762    self._name = f"{_TACO_INDEX_PREFIX}{id}"
763
764  def __repr__(self) -> str:
765    return f"IndexVar(name={repr(self._name)})"
766
767  @property
768  def name(self) -> str:
769    """Returns the name of the IndexVar."""
770    return self._name
771
772  def _visit(self,
773             func: _ExprVisitor,
774             args,
775             *,
776             leaf_checker: _SubtreeLeafChecker = None) -> None:
777    """A post-order visitor."""
778    if leaf_checker:
779      assert leaf_checker(self, *args)
780    func(self, *args)
781
782  def _emit_expression(
783      self,
784      expr_to_opnd: Dict[IndexExpr, lang.OperandDef],
785      expr_to_info: _ExprInfoDict,
786  ) -> lang.ScalarExpression:
787    """Emits a index value casted to the data type of the tensor expression."""
788    dim = getattr(lang.D, self.name)
789    index = lang.index(dim)
790    int_value = lang.TypeFn.cast_unsigned(lang.TV.I64, index)
791    return lang.TypeFn.cast_unsigned(lang.T, int_value)
792
793  def dtype(self) -> DType:
794    """Returns the data type for the index value.
795
796    This is unreachable for IndexVar.
797    """
798    assert 0
799
800
801def get_index_vars(n: int) -> List[IndexVar]:
802  """Returns a list of n IndexVar.
803
804  This routine is defined by the TACO API.
805
806  Args:
807    n: An integer representing the number of IndexVar to get.
808
809  Returns:
810    A list of IndexVar.
811
812  Raises:
813    ValueError: if n is not a positive integer.
814  """
815  if not isinstance(n, int) or n <= 0:
816    raise ValueError(f"Expected an integer: {n}.")
817  # If lock contention ever becomes an issue, we could implement a bulk getter
818  # that returns a range by only claiming the lock once.
819  return [IndexVar() for i in range(n)]
820
821
822def _mlir_symbols_from_index_vars(
823    index_vars: Tuple[IndexVar, ...]) -> Tuple[lang.SymbolDef, ...]:
824  """Returns a tuple of MLIR symbols for the given tuple of index_var."""
825  return tuple(getattr(lang.S, i.name) for i in index_vars)
826
827
828def _mlir_dimensions_from_index_vars(
829    index_vars: Tuple[IndexVar, ...]) -> Tuple[lang.DimDef, ...]:
830  """Returns a tuple of MLIR dimensions for the given tuple of index_var."""
831  return tuple(getattr(lang.D, i.name) for i in index_vars)
832
833
834def _mlir_tensor_type(
835    dtype: DType, shape: Tuple[int, ...],
836    attr: Optional[sparse_tensor.EncodingAttr]) -> ir.RankedTensorType:
837  """Returns an MLIR tensor type.
838
839  Args:
840    dtype: An DType object for the element data type of the tensor.
841    shape: A tuple of integer for the shape of the tensor.
842    attr: An optional MLIR sparse tensor attribute, only provided if the tensor
843      is a sparse tensor.
844
845  Returns:
846    An MLIR ranked tensor type.
847  """
848  ir_type = _mlir_type_from_taco_type(dtype)
849  return ir.RankedTensorType.get(shape, ir_type, attr)
850
851
852@dataclasses.dataclass(frozen=True)
853class _StructOpInfo:
854  """Information for generating a structured op in the linalg dialect.
855
856  This information is associated with an expression node that serves as the
857  root for an expression subtree implemented with a structured op.
858
859  Attributes:
860    dst_indices: A tuple of IndexVar, representing the result dimensions of the
861      structured op. This is used to construct the temporary variable for the
862      tensor to hold the structured op result.
863    dst_dims: A tuple of int, representing the result shape of the structured
864      op.
865    dst_dtype: A DType representing the data type of the structured op result.
866    dst_name: A string representing the name of the structured op result.
867    dst_format: An optional Format object representing the destination tensor
868      format. None represents a true dense tensor.
869  """
870  dst_indices: Tuple[IndexVar, ...]
871  dst_dims: Tuple[int, ...]
872  dst_dtype: DType
873  dst_name: str
874  dst_format: Optional[Format]
875
876  def __post_init__(self) -> None:
877    """Verifies the integrity of the attribute values."""
878    assert len(self.dst_indices) == len(self.dst_dims)
879
880  def emit_tensor_init(self) -> ir.RankedTensorType:
881    """Returns an initialization for the destination tensor."""
882    if self.dst_format is None or self.dst_format.rank() == 0:
883      # Initialize the dense tensor.
884      ir_type = _mlir_type_from_taco_type(self.dst_dtype)
885      tensor = linalg.InitTensorOp(self.dst_dims, ir_type).result
886      zero = arith.ConstantOp(ir_type, 0.0)
887      return linalg.fill(zero, outs=[tensor])
888
889    # Initialize the sparse tensor.
890    mlir_type = _mlir_tensor_type(self.dst_dtype, self.dst_dims,
891                                  self.dst_format.mlir_tensor_attr())
892    index_type = ir.IndexType.get()
893    return bufferization.AllocTensorOp(mlir_type, [], None, None)
894
895
896class _Stats:
897  """Information to describe how a tensor expression is implemented.
898
899  Currently, we only record the temporary tensors introduced for splitting the
900  original expression.
901  """
902
903  def __init__(self):
904    self._temps = []
905
906  def __repr__(self) -> str:
907    return f"_Stats({repr(self._temps)})"
908
909  def add_element(self, structop: _StructOpInfo):
910    """Adds a temporary tensor."""
911    self._temps.append(structop)
912
913  def get_total(self) -> int:
914    """Gets the total number of temporary tensors."""
915    return len(self._temps)
916
917  def _get_element(self, idx: int) -> _StructOpInfo:
918    """Gets the ith temporary tensor."""
919    assert idx < self.get_total()
920    return self._temps[idx]
921
922  def get_dimensions(self, idx: int) -> Tuple[int]:
923    """Gets the dimensions for the ith temporary tensor."""
924    return self._get_element(idx).dst_dims
925
926  def get_formats(self, idx: int) -> Tuple[ModeFormat]:
927    """Gets the ModeFormats for the ith temporary tensor."""
928    return tuple(self._get_element(idx).dst_format.format_pack.formats)
929
930
931class _SparseValueInfo(enum.Enum):
932  """Describes how a sparse tensor value is stored.
933  _UNPACKED: The sparse tensor value is stored as (coordnates, values) in
934    Python.
935  _PACKED: The sparse tensor value is stored as a C pointer to a packed MLIR
936    sparse tensor.
937  """
938  _UNPACKED = 0
939  _PACKED = 1
940
941
942@dataclasses.dataclass(frozen=True)
943class _Assignment:
944  """Records an assignment to a tensor T as T[indices] = expression."""
945  indices: Tuple["IndexVar", ...]
946  expression: "IndexExpr"
947
948
949class Tensor:
950  """The tensor class.
951
952  We support the TACO API tensor class with an alias of this class.
953
954  This class is part of the TACO API with the following methods:
955    insert: Inserts a value to the given coordinate in the tensor.
956    to_array: Returns a numpy ndarray for the tensor.
957
958  TACO API also defines the following arrtibutes for the class:
959    dtype: A dtype object representing the data type of the tensor.
960    format: A format object representing the storage format of the tensor.
961    name: A string object representing the name of the tensor.
962    order: An integral rank of the tensor.
963    shape: A list of integers representing the shape of the tensor.
964
965  We currently ignore the tensor dimension ordering for dense tensor.
966  """
967  _counter = _AtomicCounter()
968
969  def _get_unique_name(self) -> str:
970    """Returns a unique name for creating a new Tensor."""
971    return f"{_TACO_TENSOR_PREFIX}{self._counter.increment()}"
972
973  def _init_format(self, fmt: Union[ModeFormat, List[ModeFormat],
974                                    Format]) -> None:
975    """Process the fmt argument for the Tensor constructor.
976
977    Args:
978      fmt: This argument can be a ModeFormat, List[ModeFormat], or format. If
979        this argument is a ModeFormat, uses this ModeFormat for all the tensor
980        dimensions. If this argument is a list of ModeFormat, the len of the
981        list should equal to the rank of the tensor. If this argument is a
982        format, uses it for the format of the tensor.
983
984    Raises:
985      ValueError: If fmt is not one of the expected type or is inconsistent
986        with the rank of the tensor. This is because fmt could be an users
987        input.
988    """
989    if isinstance(fmt, ModeFormat):
990      self._format = _make_format([fmt] * self.order)
991    elif isinstance(fmt, list):
992      if len(fmt) == self.order and isinstance(fmt[0], ModeFormat):
993        self._format = _make_format(fmt)
994      else:
995        raise ValueError("Inconsistent shape and format: "
996                         f"{self._shape}, {fmt}.")
997    elif isinstance(fmt, Format):
998      if fmt.rank() != self.order:
999        raise ValueError("Inconsistent shape and format: "
1000                         f"{self._shape}, {fmt}.")
1001      else:
1002        self._format = fmt
1003    else:
1004      raise ValueError(f"Invalid format argument: {fmt}.")
1005
1006  def __init__(self,
1007               value_or_shape: Optional[Union[List[int], Tuple[int, ...], float,
1008                                              int]] = None,
1009               fmt: Optional[Union[ModeFormat, List[ModeFormat],
1010                                   Format]] = None,
1011               dtype: Optional[DType] = None,
1012               name: Optional[str] = None,
1013               is_dense: bool = False):
1014    """The tensor constructor interface defined by TACO API.
1015
1016    Args:
1017      value_or_shape: This argument is optional and can be int, float,
1018        List[int], or Tuple[int, ...]. If this argument is an int or float,
1019        creates a scalar tensor and initializes it with the value. If this
1020        argument is a list or tuple of int, uses it as the shape to create a
1021        tensor.
1022      fmt: This argument can be a ModeFormat, List[ModeFormat], or format. If
1023        this argument is a ModeFormat, uses this ModeFormat for all the tensor
1024        dimensions. If this argument is a list of ModeFormat, the len of the
1025        list should equal to the rank of the tensor. If this argument is a
1026        format, uses it for the format of the tensor.
1027      dtype: An object of dtype, representing the data type of the tensor.
1028      name: A string name of the tensor. If a name is not given, creates a
1029        unique name for the tensor.
1030      is_dense: A boolean variable to indicate whether the tensor is a dense
1031        tensor without any sparsity annotation.
1032
1033    Raises:
1034      ValueError: If there is any inconsistency among the input arguments.
1035    """
1036    # Take care of the argument default values common to both sparse tensors
1037    # and dense tensors.
1038    dtype = dtype or DType(Type.FLOAT32)
1039    self._name = name or self._get_unique_name()
1040    self._assignment = None
1041    self._engine = None
1042    self._sparse_value_location = _SparseValueInfo._UNPACKED
1043    self._dense_storage = None
1044    self._dtype = dtype
1045
1046    if is_dense:
1047      assert (fmt is None)
1048      assert (isinstance(value_or_shape, tuple) or isinstance(
1049          value_or_shape, list)) and _all_instance_of(value_or_shape, int)
1050      self._shape = value_or_shape
1051      self._format = None
1052      return
1053
1054    fmt = fmt or ModeFormat.COMPRESSED
1055    # We currently use _coords and _values to host the sparse tensor value with
1056    # COO format, and _dense_storage to host the dense tensor value. We don't
1057    # support the conversion between the two storages.
1058    self._coords = []
1059    self._values = []
1060    self._stats = _Stats()
1061    if value_or_shape is None or isinstance(value_or_shape, int) or isinstance(
1062        value_or_shape, float):
1063      # Create a scalar tensor and ignore the fmt parameter.
1064      self._shape = []
1065      self._format = _make_format([], [])
1066      if value_or_shape is not None:
1067        self._dense_storage = np.array(value_or_shape, dtype=self._dtype.value)
1068    elif (isinstance(value_or_shape, tuple) or isinstance(
1069        value_or_shape, list)) and _all_instance_of(value_or_shape, int):
1070      # Create a tensor with the specified shape and format.
1071      self._shape = list(value_or_shape)
1072      self._init_format(fmt)
1073    else:
1074      raise ValueError("Invalid first argument. "
1075                       "Must be a tuple or list for a shape or a single value"
1076                       f"if initializing a scalar tensor: {value_or_shape}.")
1077
1078  def _set_packed_sparse_tensor(self, pointer: ctypes.c_void_p) -> None:
1079    """Records the MLIR sparse tensor pointer."""
1080    self._sparse_value_location = _SparseValueInfo._PACKED
1081    self._packed_sparse_value = pointer
1082
1083  def is_unpacked(self) -> bool:
1084    """Returns true if the tensor value is not packed as MLIR sparse tensor."""
1085    return (self._sparse_value_location == _SparseValueInfo._UNPACKED)
1086
1087  def unpack(self) -> None:
1088    """Unpacks the MLIR sparse tensor representation."""
1089    if self.is_dense() or self.is_unpacked():
1090      return
1091
1092    # Use the output MLIR sparse tensor pointer to retrieve the COO-flavored
1093    # values and verify the values.
1094    rank, nse, shape, values, indices = utils.sparse_tensor_to_coo_tensor(
1095        self._packed_sparse_value, self._dtype.value)
1096    assert rank == self.order
1097    assert np.array_equal(self.shape, shape)
1098    assert nse == len(values)
1099    self._coords = indices
1100    self._values = values
1101    self._sparse_value_location = _SparseValueInfo._UNPACKED
1102
1103  def __repr__(self) -> str:
1104    self._sync_value()
1105    self.unpack()
1106    value_str = (f"{repr(self._dense_storage)})" if self.is_dense() else
1107                 f"{repr(self._coords)} {repr(self._values)})")
1108    return (f"Tensor(_name={repr(self._name)} "
1109            f"_dtype={repr(self._dtype)} : ") + value_str
1110
1111  def insert(self, coords: List[int], val: Union[float, int]) -> None:
1112    """Inserts a value to the given coordinate.
1113
1114    Args:
1115      coords: A list of integer coordinates. The length of the list must be the
1116        same as the rank of the tensor.
1117      val: A value being inserted. It is either an integral or a floating point
1118        value. This value will be converted to the data type of the tensor.
1119
1120    Raises:
1121      ValueError: When there is any problem in the parameters.
1122    """
1123    if self.is_dense():
1124      raise ValueError("Insert method is not supported for dense tensors.")
1125    if self._assignment != None or not self.is_unpacked():
1126      raise ValueError(
1127          "Can't use Insert method for a tensor constructed from a file.")
1128    if not isinstance(coords, list):
1129      raise ValueError(f"Non list coordinate detected: {coords}.")
1130    if not _all_instance_of(coords, int):
1131      raise ValueError(f"Non integer coordinate detected: {coords}.")
1132    if (len(coords) != self.order or
1133        any([c < 0 or c >= self._shape[i] for i, c in enumerate(coords)])):
1134      raise ValueError("Invalid coordinate for rank: "
1135                       f"{self.order}, {coords}.")
1136
1137    if not isinstance(val, int) and not isinstance(val, float):
1138      raise ValueError(f"Value is neither int nor float: {val}.")
1139
1140    self._coords.append(tuple(coords))
1141    self._values.append(self._dtype.value(val))
1142
1143  def is_dense(self) -> bool:
1144    """Returns true if the tensor doesn't have sparsity annotation."""
1145    return self.order == 0 or self._format is None
1146
1147  def to_array(self) -> np.ndarray:
1148    """Returns the numpy array for the Tensor.
1149
1150    This is currenly only implemented for dense Tensor.
1151    """
1152    if not self.is_dense():
1153      raise ValueError("Conversion from non-dense Tensor "
1154                       "to numpy array not supported yet.")
1155
1156    self._sync_value()
1157
1158    return self._dense_storage
1159
1160  @staticmethod
1161  def from_array(array: np.ndarray) -> "Tensor":
1162    """Returns a dense tensor with the value copied from the input array.
1163
1164    We currently only support the conversion of float32 and float64 numpy arrays
1165    to Tensor.
1166
1167    Args:
1168      array: The numpy array that provides the data type, shape and value for
1169        the tensor.
1170
1171    Returns:
1172      A Tensor object.
1173
1174    Raises:
1175      ValueError if the data type of the numpy array is not supported.
1176    """
1177    if array.dtype != np.float32 and array.dtype != np.float64:
1178      raise ValueError(f"Expected floating point value type: {array.dtype}.")
1179    tensor = Tensor(
1180        array.shape,
1181        dtype=_nptype_to_taco_type(array.dtype.type),
1182        is_dense=True)
1183    tensor._dense_storage = np.copy(array)
1184    return tensor
1185
1186  @staticmethod
1187  def from_coo(
1188      coordinates: List[Tuple[int, ...]],
1189      values: List[_AnyRuntimeType],
1190      fmt: Format,
1191      dtype: DType,
1192  ) -> "Tensor":
1193    """Converts coordinates and values to a sparse tensor representation.
1194
1195    Args:
1196      coordinates: A list of coordinates with non-zero values.
1197      values: The non-zero values.
1198      fmt: The tensor storage format.
1199      dtype: The tensor element data type.
1200
1201    Returns:
1202      A tensor with the given non-zero values and storage format. The shape of
1203      the tensor has the minimum size for each dimension to make the given
1204      coordinates valid.
1205    """
1206    assert (isinstance(coordinates, List) and
1207            _all_instance_of(coordinates, Tuple))
1208    assert (isinstance(values, List) and _all_instance_of(values, dtype.value))
1209    assert isinstance(fmt, Format)
1210
1211    rank = fmt.rank()
1212    assert all(len(c) == rank and _all_instance_of(c, int) for c in coordinates)
1213
1214    # Find the maximum coordinate value for each dimension.
1215    max_coordinate = list(map(max, zip(*coordinates)))
1216    # The size of each dimension is one more that such a maximum coordinate
1217    # value.
1218    shape = [c + 1 for c in max_coordinate]
1219    tensor = Tensor(shape, fmt, dtype=dtype)
1220    tensor._coords = coordinates
1221    tensor._values = values
1222
1223    return tensor
1224
1225  @staticmethod
1226  def from_file(
1227      filename: str,
1228      fmt: Format,
1229      dtype: DType,
1230  ) -> "Tensor":
1231    """Constructs a sparse tensor using the COO-flavored values from a file.
1232
1233    Args:
1234      filename: A string for the name of the file that contains the sparse
1235        tensor data.
1236      fmt: The tensor storage format.
1237      dtype: The tensor element data type.
1238
1239    Returns:
1240      A tensor with the given non-zero values and storage format. The tensor
1241      value is stored as an MLIR sparse tensor.
1242    """
1243    sparse_tensor, shape = utils.create_sparse_tensor(filename,
1244                                                      fmt.format_pack.formats,
1245                                                      _dtype_to_mlir_str(dtype))
1246    tensor = Tensor(shape.tolist(), fmt, dtype=dtype)
1247    tensor._set_packed_sparse_tensor(sparse_tensor)
1248
1249    return tensor
1250
1251  def to_file(self, filename: str) -> None:
1252    """Output the tensor value to a file.
1253
1254    This method evaluates any pending assignment to the tensor and outputs the
1255    tensor value.
1256
1257    Args:
1258      filename: A string file name.
1259
1260    Raises:
1261       ValueError: If the tensor is dense, or an unpacked sparse tensor.
1262    """
1263    self._sync_value()
1264
1265    if self.is_dense():
1266      raise ValueError("Writing dense tensors without sparsity annotation to "
1267                       "file is not supported.")
1268
1269    if self.is_unpacked():
1270      raise ValueError("Writing unpacked sparse tensors to file is not "
1271                       "supported.")
1272
1273    utils.output_sparse_tensor(self._packed_sparse_value, filename,
1274                               self._format.format_pack.formats,
1275                               _dtype_to_mlir_str(self._dtype))
1276
1277  @property
1278  def dtype(self) -> DType:
1279    """Returns the data type for the Tensor."""
1280    return self._dtype
1281
1282  @property
1283  def format(self) -> Format:
1284    """Returns the storage format for the Tensor."""
1285    return self._format
1286
1287  @property
1288  def name(self) -> str:
1289    """Returns the name for the Tensor."""
1290    return self._name
1291
1292  @property
1293  def order(self) -> int:
1294    """Returns the rank of the Tensor."""
1295    return len(self._shape)
1296
1297  @property
1298  def shape(self) -> List[int]:
1299    """Returns the shape of the Tensor."""
1300    return self._shape
1301
1302  def _verify_and_normalize_indices(self, indices) -> Tuple[IndexVar, ...]:
1303    """Verifies and normalizes the indices to access the tensor.
1304
1305    Args:
1306      indices: The index expression used to access a tensor, which could be any
1307        Python object from user inputs.
1308
1309    Returns:
1310      A tuple of IndexVar.
1311
1312    Raises:
1313      ValueError: If indices is not 0 for scalar tensors, or not an IndexVar or
1314        a tuple of IndexVar for other tensors.
1315    """
1316    if self.order == 0:
1317      if not isinstance(indices, int) or indices != 0:
1318        raise ValueError(f"Expected 0 to index scalar tensors: {indices}")
1319      return ()
1320
1321    if isinstance(indices, IndexVar):
1322      return (indices,)
1323    elif isinstance(indices, tuple) and _all_instance_of(indices, IndexVar):
1324      return indices
1325
1326    raise ValueError(f"Expected IndexVars: {indices}")
1327
1328  def __getitem__(self, key) -> "Access":
1329    """Verifies and processes a tensor access.
1330
1331    In the tensor index notation, a tensor access T[i, j] is represented as
1332    retrieving a value with key (i, j) from the tensor object T in Python. This
1333    routine verifies the key for the tensor access and returns a tensor access
1334    object.
1335
1336    Args:
1337      key: The key used to access the tensor, which could be any Python object
1338        from user inputs.
1339
1340    Returns:
1341      The corresponding tensor access object.
1342
1343    Raises:
1344      ValueError: If key is not an IndexVar or a tuple of IndexVar.
1345    """
1346    indices = self._verify_and_normalize_indices(key)
1347    return Access(self, indices)
1348
1349  def __setitem__(self, key, value) -> None:
1350    """Verifies and processes a tensor assignment.
1351
1352    In the tensor index notation, a tensor assignment "T[i, j] = ..." is
1353    represented as setting a value for a tensor object T via key (i, j) in
1354    Python. This routine verifies the key, evaluates the value, and assigns the
1355    value to the tensor.
1356
1357    We only support assignment of dense tensor currently.
1358
1359    Args:
1360      key: The key used to access the tensor, which could be any Python object
1361        from user inputs.
1362      value: The value assigned to the tensor, which could be any Python object
1363        from user inputs.
1364
1365    Raises:
1366      ValueError: If tensor is not a dense tensor, or the key is not an IndexVar
1367        or a tuple of IndexVar, or the length of the indices is not the same as
1368        the rank of the tensor.
1369    """
1370    indices = self._verify_and_normalize_indices(key)
1371    if len(indices) != self.order:
1372      raise ValueError("Mismatch between indices and tensor rank: "
1373                       f"len({indices}) != {self.order}.")
1374
1375    self._assignment = _Assignment(indices, value)
1376    self._engine = None
1377
1378  def compile(self, force_recompile: bool = False) -> None:
1379    """Compiles the tensor assignment to an execution engine.
1380
1381    Calling compile the second time does not do anything unless
1382    force_recompile is True.
1383
1384    Args:
1385      force_recompile: A boolean value to enable recompilation, such as for the
1386        purpose of timing.
1387
1388    Raises:
1389      ValueError: If the assignment is not proper or not supported.
1390    """
1391    if self._assignment is None or (self._engine is not None and
1392                                    not force_recompile):
1393      return
1394
1395    self._engine = self._assignment.expression.compile(self,
1396                                                       self._assignment.indices)
1397
1398  def compute(self) -> None:
1399    """Executes the engine for the tensor assignment.
1400
1401    Raises:
1402      ValueError: If the assignment hasn't been compiled yet.
1403    """
1404    if self._assignment is None:
1405      return
1406
1407    if self._engine is None:
1408      raise ValueError("Need to invoke compile() before invoking compute().")
1409
1410    input_accesses = self._assignment.expression.get_input_accesses()
1411    # Gather the pointers for the input buffers.
1412    input_pointers = [a.tensor.ctype_pointer() for a in input_accesses]
1413    if self.is_dense():
1414      # The pointer to receive dense output is the first argument to the
1415      # execution engine.
1416      arg_pointers = [self.dense_dst_ctype_pointer()] + input_pointers
1417    else:
1418      # The pointer to receive the sparse tensor output is the last argument
1419      # to the execution engine and is a pointer to pointer of char.
1420      arg_pointers = input_pointers + [
1421          ctypes.pointer(ctypes.pointer(ctypes.c_char(0)))
1422      ]
1423
1424    # Invoke the execution engine to run the module.
1425    self._engine.invoke(_ENTRY_NAME, *arg_pointers)
1426
1427    # Retrieve the result.
1428    if self.is_dense():
1429      result = runtime.ranked_memref_to_numpy(arg_pointers[0][0])
1430      assert isinstance(result, np.ndarray)
1431      self._dense_storage = result
1432    else:
1433      self._set_packed_sparse_tensor(arg_pointers[-1][0])
1434
1435    self._assignment = None
1436    self._engine = None
1437
1438  def evaluate(self) -> None:
1439    """Evaluates the tensor assignment."""
1440    self.compile()
1441    self.compute()
1442
1443  def _sync_value(self) -> None:
1444    """Updates the tensor value by evaluating the pending assignment."""
1445    if self._assignment is not None:
1446      self.evaluate()
1447
1448  def mlir_tensor_type(self) -> ir.RankedTensorType:
1449    """Returns the MLIR type for the tensor."""
1450    mlir_attr = (None if (self._format is None or self.order == 0) else
1451                 self._format.mlir_tensor_attr())
1452    return _mlir_tensor_type(self._dtype, tuple(self._shape), mlir_attr)
1453
1454  def dense_dst_ctype_pointer(self) -> ctypes.pointer:
1455    """Returns the ctypes pointer for the pointer to an MemRefDescriptor.
1456
1457    For a dense tensor output, the MLIR compiler allocates the storage for
1458    the tensor. This routine returns the pointer to an MLIR MemRefDescriptor for
1459    receiving the tensor.
1460    """
1461    assert self.is_dense()
1462    mem_ref_desc = runtime.make_nd_memref_descriptor(
1463        self.order, np.ctypeslib.as_ctypes_type(self.dtype.value))()
1464    return ctypes.pointer(ctypes.pointer(mem_ref_desc))
1465
1466  def ctype_pointer(self) -> ctypes.pointer:
1467    """Returns the ctypes pointer for the pointer to the input tensor."""
1468    if self.is_dense():
1469      if self._dense_storage is None:
1470        self._dense_storage = np.zeros(self._shape, self._dtype.value)
1471      return _ctype_pointer_from_array(self._dense_storage)
1472
1473    if self.is_unpacked():
1474      shape = np.array(self._shape, np.int64)
1475      indices = np.array(self._coords, np.int64)
1476      values = np.array(self._values, self._dtype.value)
1477      perm, sparse = self.format.get_permutation_and_sparsity()
1478      ptr = utils.coo_tensor_to_sparse_tensor(shape, values, indices, perm,
1479                                              sparse)
1480    else:
1481      ptr = self._packed_sparse_value
1482
1483    return ctypes.pointer(ctypes.cast(ptr, ctypes.c_void_p))
1484
1485  def get_scalar_value(self) -> _AnyRuntimeType:
1486    """Returns the value for the scalar tensor.
1487
1488    This method also evaluates the assignment to the tensor.
1489
1490    Raises:
1491      ValueError: If the tensor is not a scalar.
1492    """
1493    if self.order != 0:
1494      raise ValueError(f"Expected a scalar tensor, got: rank={self.order}")
1495
1496    self._sync_value()
1497    return self._dense_storage
1498
1499
1500  def get_coordinates_and_values(
1501      self) -> Tuple[List[Tuple[int, ...]], List[_AnyRuntimeType]]:
1502    """Returns the coordinates and values for the non-zero elements.
1503
1504    This method also evaluates the assignment to the tensor and unpack the
1505    sparse tensor.
1506    """
1507    self._sync_value()
1508
1509    if not self.is_dense():
1510      self.unpack()
1511      return (self._coords, self._values)
1512
1513    if self.order == 0:
1514      return ([], self._dense_storage)
1515
1516    # Coordinates for non-zero elements, grouped by dimensions.
1517    coords_by_dims = self._dense_storage.nonzero()
1518    # Coordinates for non-zero elements, grouped by elements.
1519    coords = np.transpose(coords_by_dims)
1520    values = self._dense_storage[coords_by_dims]
1521    return (coords, values)
1522
1523  def _record_stats(self, structop: "_StructOpInfo"):
1524    """Collects information for temporary tensors."""
1525    # Exclude user specified destination tensors.
1526    if structop.dst_name == self.name:
1527      return
1528
1529    self._stats.add_element(structop)
1530
1531
1532def _emit_operand(op_def: lang.LinalgOpDef, indices: Tuple[IndexVar, ...],
1533                  name: str, kind: lang.OperandKind) -> lang.OperandDef:
1534  """Emits an operand for a tensor access in the current linalg operation.
1535
1536  Args:
1537    op_def: A LinalgOpDef representing the current linalg dialect operation.
1538    indices: A tuple of IndexVar used to access the tensor.
1539    name: A unique string name of the tensor.
1540    kind: An OperandKind for the operand.
1541
1542  Returns:
1543    An OperandDef representing the operand.
1544  """
1545  dim_sym = _mlir_symbols_from_index_vars(indices)
1546  opnd = lang.OperandDef(kind, lang.T, dim_sym)
1547  op_def.add_operand(name, opnd)
1548  return opnd
1549
1550
1551@dataclasses.dataclass(frozen=True)
1552class _DimInfo:
1553  """Information for an operand dimension.
1554
1555  Attributes:
1556    dim: An integer for the size of the dimension.
1557    mode_format: A ModeFormat for the dimension sparsity.
1558  """
1559  dim: int
1560  mode_format: ModeFormat
1561
1562
1563def _get_dummy_dim_info() -> _DimInfo:
1564  """Constructs the _DimInfo for an index used in tensor expressions."""
1565  return _DimInfo(-1, ModeFormat.DENSE)
1566
1567
1568@dataclasses.dataclass()
1569class _ExprInfo:
1570  """Expression information for validation and code generation.
1571
1572  Attributes:
1573    src_indices: A tuple of IndexVar for the indices used by the tensors in the
1574      expression tree.
1575    dim_infos: A tuple of _DimInfo, representing the dimension information
1576      corresponding to the src_indices.
1577    reduce_indices: A set of IndexVar for the indices reduced by the expression.
1578    acc_reduce_indices: An accumulated set of IndexVar for the indices reduced
1579      by the expression and its children.
1580    structop_info: Information to support the code generation for a structured
1581      op in the linalg dialect, if the corresponding expression node is the root
1582      of a subtree for a structured op.
1583    mlir_value: The MLIR value generated for the structured op.
1584  """
1585  src_indices: Tuple[IndexVar, ...]
1586  dim_infos: Tuple[_DimInfo, ...]
1587  reduce_indices: Optional[Set[IndexVar]] = None
1588  acc_reduce_indices: Optional[Set[IndexVar]] = None
1589  structop_info: Optional[_StructOpInfo] = None
1590  mlir_value: Optional[ir.Value] = None
1591
1592  def __post_init__(self) -> None:
1593    """Verifies and fix up attribute values.
1594
1595    Verifies the consistency of the attributes and modifies the default values
1596    to support convenient initializer syntax.
1597    """
1598    assert len(self.src_indices) == len(self.dim_infos)
1599    self.reduce_indices = self.reduce_indices or set()
1600    self.acc_reduce_indices = self.acc_reduce_indices or set()
1601
1602
1603@dataclasses.dataclass(frozen=True)
1604class Access(IndexExpr):
1605  """The tensor access class.
1606
1607  We support the TACO API access class with an alias of this class.
1608
1609  Attributes:
1610    tensor: A Tensor being accessed.
1611    indices: A tuple of IndexVar, representing the indices used to access the
1612      Tensor.
1613  """
1614  tensor: Tensor
1615  indices: Tuple[IndexVar, ...]
1616
1617  def __post_init__(self) -> None:
1618    """Verifies the tensor and indices for a tensor access.
1619
1620    Raises:
1621       ValueError: If indices is not a list of IndexVar or the len of indices
1622       doesn't equal to the rank of the tensor.
1623    """
1624    if (not isinstance(self.indices, tuple) or
1625        not _all_instance_of(self.indices, IndexVar)):
1626      raise ValueError(f"Indices contain non IndexVar: {str(self.indices)}.")
1627    if self.tensor.order != len(self.indices):
1628      raise ValueError("Invalid indices for rank: "
1629                       f"str{self.tensor.order} != len({str(self.indices)}).")
1630
1631  def __repr__(self) -> str:
1632    # The Tensor __repr__ method evaluates the pending assignment to the tensor.
1633    # We want to define the __repr__ method here to avoid such evaluation of the
1634    # tensor assignment.
1635    indices_str = ", ".join(map(lambda i: i.name, self.indices))
1636    return (f"Tensor({self.tensor.name}) " f"Indices({indices_str})")
1637
1638  def _emit_expression(
1639      self,
1640      expr_to_opnd: Dict[IndexExpr, lang.OperandDef],
1641      expr_to_info: _ExprInfoDict,
1642  ) -> lang.ScalarExpression:
1643    """Emits a linalg dialect TensorUse expression for the tensor access."""
1644    assert self in expr_to_opnd
1645    dims = _mlir_dimensions_from_index_vars(self.indices)
1646    return lang.TensorUse(expr_to_opnd[self], dims)
1647
1648  def _visit(self,
1649             func: _ExprVisitor,
1650             args,
1651             *,
1652             leaf_checker: _SubtreeLeafChecker = None) -> None:
1653    if leaf_checker:
1654      assert leaf_checker(self, *args)
1655    func(self, *args)
1656
1657  def dtype(self) -> DType:
1658    return self.tensor.dtype
1659
1660
1661def _gather_input_accesses_index_vars(
1662    expr: IndexExpr,
1663    input_accesses: List[Access],
1664) -> None:
1665  """Collects Access nodes."""
1666  if isinstance(expr, Access) and expr not in input_accesses:
1667    input_accesses.append(expr)
1668
1669
1670def _op_ceil(__a: Any) -> Any:
1671  """A _UnaryOp object for operation ceil."""
1672  pass
1673
1674
1675def _op_floor(__a: Any) -> Any:
1676  """A _UnaryOp object for operation floor."""
1677  pass
1678
1679
1680def _op_unary_to_callable(op: _UnaryOp) -> lang.UnaryFnType:
1681  """Returns the linalg dialect function object for the given operation."""
1682  op_to_callable = {
1683      operator.abs: lang.UnaryFn.abs,
1684      operator.neg: lang.UnaryFn.negf,
1685      _op_ceil: lang.UnaryFn.ceil,
1686      _op_floor: lang.UnaryFn.floor,
1687  }
1688  return op_to_callable[op]
1689
1690
1691@dataclasses.dataclass(frozen=True)
1692class _UnaryExpr(IndexExpr):
1693  """The representation for a Unary operation.
1694
1695  Attributes:
1696  op: A _UnaryOp representing the operation.
1697  a: An IndexExpr representing the operand for the operation.
1698  """
1699  op: _BinaryOp
1700  a: IndexExpr
1701
1702  def __post_init__(self) -> None:
1703    """Verifies that the operand being added is an IndexExpr."""
1704    assert isinstance(self.a, IndexExpr)
1705
1706  def _emit_expression(
1707      self,
1708      expr_to_opnd: Dict[IndexExpr, lang.OperandDef],
1709      expr_to_info: _ExprInfoDict,
1710  ) -> lang.ScalarExpression:
1711    """Emits the expression tree and returns the expression."""
1712    # The current expression node is an internal node of the structured op.
1713    if self not in expr_to_opnd:
1714      a = self.a._emit_expression(expr_to_opnd, expr_to_info)
1715      return _op_unary_to_callable(self.op)(a)
1716
1717    # The current expression is a leaf node of the structured op. That is, it is
1718    # a temporary tensor generated by its child structured op.
1719    op_info = expr_to_info[self].structop_info
1720    assert op_info is not None
1721    dims = _mlir_dimensions_from_index_vars(op_info.dst_indices)
1722    return lang.TensorUse(expr_to_opnd[self], dims)
1723
1724  def _visit(self,
1725             func: _ExprVisitor,
1726             args,
1727             *,
1728             leaf_checker: _SubtreeLeafChecker = None) -> None:
1729    """A post-order visitor."""
1730    if leaf_checker is None or not leaf_checker(self, *args):
1731      self.a._visit(func, args, leaf_checker=leaf_checker)
1732    func(self, *args)
1733
1734  def dtype(self) -> DType:
1735    """Returns the data type of the operation."""
1736    return self.a.dtype()
1737
1738
1739def _op_to_callable(op: _BinaryOp) -> lang.BinaryFnType:
1740  """Returns the linalg dialect function object for the given operation."""
1741  op_to_callable = {
1742      operator.add: lang.BinaryFn.add,
1743      operator.sub: lang.BinaryFn.sub,
1744      operator.mul: lang.BinaryFn.mul,
1745  }
1746  return op_to_callable[op]
1747
1748@dataclasses.dataclass(frozen=True)
1749class _BinaryExpr(IndexExpr):
1750  """The representation for a binary operation.
1751
1752  Attributes:
1753  op: A _BinaryOp representing the binary operation.
1754  a: An IndexExpr representing the first operand of the operation.
1755  b: An IndexExpr representing the second operand of the operation.
1756  """
1757  op: _BinaryOp
1758  a: IndexExpr
1759  b: IndexExpr
1760
1761  def __post_init__(self) -> None:
1762    """Verifies that the operands being added are IndexExpr."""
1763    assert isinstance(self.a, IndexExpr) and isinstance(self.b, IndexExpr)
1764
1765  def _emit_expression(
1766      self,
1767      expr_to_opnd: Dict[IndexExpr, lang.OperandDef],
1768      expr_to_info: _ExprInfoDict,
1769  ) -> lang.ScalarExpression:
1770    """Emits the expression tree and returns the expression."""
1771    # The current expression node is an internal node of the structured op.
1772    if self not in expr_to_opnd:
1773      a = self.a._emit_expression(expr_to_opnd, expr_to_info)
1774      b = self.b._emit_expression(expr_to_opnd, expr_to_info)
1775      return _op_to_callable(self.op)(a, b)
1776
1777    # The current expression is a leaf node of the structured op. That is, it is
1778    # a temporary tensor generated by its child structured op.
1779    op_info = expr_to_info[self].structop_info
1780    assert op_info is not None
1781    dims = _mlir_dimensions_from_index_vars(op_info.dst_indices)
1782    return lang.TensorUse(expr_to_opnd[self], dims)
1783
1784  def _visit(self,
1785             func: _ExprVisitor,
1786             args,
1787             *,
1788             leaf_checker: _SubtreeLeafChecker = None) -> None:
1789    """A post-order visitor."""
1790    if leaf_checker is None or not leaf_checker(self, *args):
1791      self.a._visit(func, args, leaf_checker=leaf_checker)
1792      self.b._visit(func, args, leaf_checker=leaf_checker)
1793    func(self, *args)
1794
1795  def dtype(self) -> DType:
1796    """Returns the data type of the binary operation."""
1797    return self.a.dtype()
1798
1799
1800def _validate_and_collect_dim_info(
1801    index_to_dim_info: Dict[IndexVar, _DimInfo],
1802    indices: Tuple[IndexVar, ...],
1803    dim_infos: Tuple[_DimInfo, ...],
1804    expr: _BinaryExpr,
1805) -> None:
1806  """Validates and collects the dimension information for an index notation.
1807
1808  Validates (indices, dim_infos) against the information collected from other
1809  source operands and is represented by index_to_dim_info. In particular, we
1810  ensure that each IndexVar corresponds to only one dimension size. We also
1811  aggregate the new information represented in (indices, dim_infos) to
1812  index_to_dim_info.
1813
1814  Args:
1815    index_to_dim: A dictionary of (IndexVar, _DimInfo) collected from the
1816      previous operands.
1817    indices: The IndexVars to be validated.
1818    dim_infos: The dimension information for the IndexVars to be validated.
1819    expr: The binary expression where (indices, dim_infos) is used.
1820
1821  Raises:
1822    ValueError if there is any problem in the IndexVars or dimensional values.
1823  """
1824  assert len(indices) == len(dim_infos)
1825  for i, d in zip(indices, dim_infos):
1826    if i not in index_to_dim_info:
1827      index_to_dim_info[i] = d
1828    else:
1829      dim = index_to_dim_info[i].dim
1830      if dim == -1 or d.dim == -1:
1831        dim = dim if dim != -1 else d.dim
1832      elif dim != d.dim:
1833        raise ValueError(f"Inconsistent source dimension for {i}: "
1834                         f"{d.dim} vs {dim}")
1835      mode_format = _mode_format_estimator(expr.op)(
1836          index_to_dim_info[i].mode_format, d.mode_format)
1837      index_to_dim_info[i] = _DimInfo(d.dim, mode_format)
1838
1839
1840def _validate_and_collect_expr_info(
1841    expr: IndexExpr,
1842    expr_to_info: _ExprInfoDict,
1843) -> None:
1844  """Validates dimension information and constructs _ExprInfo.
1845
1846  Validates that dimensional values for the same IndexVar are the same. Collects
1847  a list of IndexVar used by the expression and their corresponding dimensional
1848  values. Constructs an _ExprInfo object to record the information for the
1849  IndexExpr.
1850
1851  This routine is passed to the post-order visitor as an _ExprVisitor object.
1852
1853  Args:
1854    expr: The IndexExpr being validated.
1855    expr_to_info: The dictionary of (IndexExpr, _ExprInfo) for recording the
1856      expression information.
1857
1858  Raises:
1859    ValueError if there is any problem in the IndexVars or dimensional values.
1860  """
1861  # Objects of class Access can be shared by different expressions. Avoid
1862  # processing Access objects multiple times by skipping the processing if expr
1863  # is already in the dictionary.
1864  if expr in expr_to_info:
1865    return
1866
1867  if isinstance(expr, IndexVar):
1868    src_indices = expr,  # A tuple with one element.
1869    dim_infos = _get_dummy_dim_info(),  # A tuple with one element.
1870  elif isinstance(expr, Access):
1871    src_indices = expr.indices
1872    src_dims = tuple(expr.tensor.shape)
1873    if expr.tensor.format is None:
1874      # Treat each dimension of a dense tensor as DENSE for the purpose of
1875      # calculating temporary tensor storage format.
1876      mode_formats = tuple([ModeFormat.DENSE] * len(src_dims))
1877    else:
1878      mode_formats = tuple(expr.tensor.format.format_pack.formats)
1879    assert len(src_dims) == len(mode_formats)
1880    dim_infos = tuple([_DimInfo(d, m) for d, m in zip(src_dims, mode_formats)])
1881  elif isinstance(expr, _UnaryExpr):
1882    a_info = expr_to_info[expr.a]
1883    index_to_dim_info = {
1884        i: d for i, d in zip(a_info.src_indices, a_info.dim_infos)
1885    }
1886    # Here we rely on the fact that dictionaries keep the insertion order for
1887    # keys and values.
1888    src_indices = tuple(index_to_dim_info.keys())
1889    dim_infos = tuple(index_to_dim_info.values())
1890  else:
1891    assert isinstance(expr, _BinaryExpr)
1892    a_info = expr_to_info[expr.a]
1893    index_to_dim_info = {
1894        i: d for i, d in zip(a_info.src_indices, a_info.dim_infos)
1895    }
1896    b_info = expr_to_info[expr.b]
1897    _validate_and_collect_dim_info(index_to_dim_info, b_info.src_indices,
1898                                   b_info.dim_infos, expr)
1899    # Here we rely on the fact that dictionaries keep the insertion order for
1900    # keys and values.
1901    src_indices = tuple(index_to_dim_info.keys())
1902    dim_infos = tuple(index_to_dim_info.values())
1903
1904  expr_to_info[expr] = _ExprInfo(src_indices, dim_infos)
1905
1906
1907def _mark_structured_op_root(
1908    expr: IndexExpr,
1909    reduce_index: IndexVar,
1910    expr_to_info: _ExprInfoDict,
1911) -> None:
1912  """Identifies the root expression for a structured op in the linalg dialect.
1913
1914  An linalg structured op can only perform reduction on the whole expression.
1915  For a TACO tensor algebra expression, the reduction on an IndexVar is done at
1916  the smallest expression that contains all the uses of the IndexVar. If such an
1917  expression is only part of the whole expression, we need to split this
1918  sub-expression tree out from its parent and implement the sub-expression as a
1919  structured op.
1920
1921  This routine identifies the root expression node for performing a reduction on
1922  the given IndexVar. If the reduction of the given IndexVar should be performed
1923  on expression X, then the IndexVar is added to expr_to_info[X].reduce_indices
1924
1925  Args:
1926    expr: The root IndexExpr for the tensor algebra expression.
1927    reduce_index: The IndexVar which we want to find out the proper expression
1928      to perform a reduction.
1929    expr_to_info: The dictionary to look up _ExprInfo for IndexExpr.
1930
1931  Raises:
1932      ValueError: If the expression is not proper or not supported.
1933  """
1934  expr_info = expr_to_info[expr]
1935  if isinstance(expr, Access):
1936    # Handle simple reduction expression in the format of A[i] = B[i, j].
1937    if reduce_index in expr_info.src_indices:
1938      expr_info.reduce_indices.add(reduce_index)
1939    return
1940  elif isinstance(expr, IndexVar):
1941    # A[i] = B[i] + j is not allowed.
1942    raise ValueError(f"IndexVar is not part of the iteration domain: {expr}.")
1943
1944  assert (isinstance(expr, _BinaryExpr))
1945  a_info = expr_to_info[expr.a]
1946  b_info = expr_to_info[expr.b]
1947
1948  if reduce_index in a_info.src_indices and reduce_index in b_info.src_indices:
1949    expr_info.reduce_indices.add(reduce_index)
1950    return
1951
1952  if reduce_index in a_info.src_indices:
1953    _mark_structured_op_root(expr.a, reduce_index, expr_to_info)
1954  elif reduce_index in b_info.src_indices:
1955    _mark_structured_op_root(expr.b, reduce_index, expr_to_info)
1956  else:
1957    assert False, "Unreachable path"
1958
1959
1960def _accumulate_reduce_indices(
1961    expr: IndexExpr,
1962    expr_to_info: _ExprInfoDict,
1963) -> None:
1964  """Propagates reduction indices from child expressions to parent expressions.
1965
1966  This routine is passed to the post-order visitor as an _ExprVisitor object.
1967
1968  Args:
1969    expr: The IndexExpr being visited.
1970    expr_to_info: The dictionary of (IndexExpr, _ExprInfo) for recording the
1971      expression information.
1972  """
1973  assert expr in expr_to_info
1974  expr_info = expr_to_info[expr]
1975
1976  if isinstance(expr, _BinaryExpr):
1977    a_info = expr_to_info[expr.a]
1978    b_info = expr_to_info[expr.b]
1979    expr_info.acc_reduce_indices = (
1980        a_info.acc_reduce_indices | b_info.acc_reduce_indices
1981        | expr_info.reduce_indices)
1982  elif isinstance(expr, _UnaryExpr):
1983    a_info = expr_to_info[expr.a]
1984    expr_info.acc_reduce_indices = (
1985        a_info.acc_reduce_indices | expr_info.reduce_indices)
1986  elif isinstance(expr, IndexVar):
1987    # If an IndexVar is reducing itself, it means the IndexVar is outside the
1988    # iteration domain. This usage is now allowed and we should emit an error
1989    # before reaching here.
1990    assert not expr_info.reduce_indices
1991  else:
1992    assert isinstance(expr, Access)
1993    # Handle simple reduction expression in the format of A[i] = B[i, j].
1994    expr_info.acc_reduce_indices = expr_info.reduce_indices
1995
1996
1997
1998def _gather_structured_op(
1999    expr: IndexExpr,
2000    expr_to_info: _ExprInfoDict,
2001    structop_roots: List[IndexExpr],
2002) -> None:
2003  """Adds structured op root expression information to structop_roots.
2004
2005  This routine is passed to the post-order visitor as an _ExprVisitor object.
2006
2007  Args:
2008    expr: The IndexExpr being visited.
2009    expr_to_info: The dictionary to look up _ExprInfo for IndexExpr.
2010    structop_roots: The resulting list of IndexExpr that are the roots for
2011      linalg structured ops.
2012  """
2013  if not expr_to_info[expr].reduce_indices:
2014    return
2015
2016  # If the expression is the root for reducing some indices, collect the indices
2017  # and dimensions for the reduction result.
2018  dst_indices = []
2019  dst_dims = []
2020  mode_fmts = []
2021  for i, d in zip(expr_to_info[expr].src_indices, expr_to_info[expr].dim_infos):
2022    if i not in expr_to_info[expr].acc_reduce_indices:
2023      dst_indices.append(i)
2024      dst_dims.append(d.dim)
2025      mode_fmts.append(d.mode_format)
2026
2027  # Add the information to the dictionary.
2028  op_info = _StructOpInfo(
2029      tuple(dst_indices),
2030      tuple(dst_dims),
2031      expr.dtype(),
2032      f"temp{len(structop_roots)}",
2033      _make_format(mode_fmts),
2034  )
2035  expr_to_info[expr].structop_info = op_info
2036
2037  # Add the expression to the list of structured op roots.
2038  structop_roots.append(expr)
2039
2040
2041def _is_structured_op_leaf(
2042    expr: IndexExpr,
2043    root: IndexExpr,
2044    expr_to_info: _ExprInfoDict,
2045    *unused_args,
2046) -> bool:
2047  """Returns true iff the expression is a leaf node for a structured op.
2048
2049  The root of a structured op is a leaf of its parent structured op that uses
2050  its result. An expression node is a leaf node for the current structured op if
2051  it is an Access node or the root for a structured op that is not the current
2052  structured op.
2053
2054  This routine is passed to the post-order visitor as a _SubtreeLeafChecker
2055  object. Because the post-order visitor pass the same parameters to both
2056  _SubtreeLeafChecker and _ExprVisitor, this routine may received unused
2057  parameters.
2058
2059  Args:
2060    expr: The IndexExpr being visited.
2061    root: The root of the current structured op.
2062    expr_to_info: The dictionary to look up _ExprInfo for IndexExpr.
2063
2064  Returns:
2065    True if the current IndexExpr is a leaf for the current structured op.
2066  """
2067  return (expr != root and
2068          expr_to_info[expr].structop_info is not None) or isinstance(
2069              expr, Access) or isinstance(expr, IndexVar)
2070
2071
2072def _gather_structured_op_input(
2073    expr: IndexExpr,
2074    root: IndexExpr,
2075    expr_to_info: _ExprInfoDict,
2076    structop_inputs: List[IndexExpr],
2077) -> None:
2078  """Adds the IndexExpr to structop_inputs if it is an input.
2079
2080  If the current IndexExpr is an input for the current structured op, adds it to
2081  structop_inputs. The current IndexExpr is an input if it is an Access node or
2082  if it is the root for a structured op that is not the current structured op.
2083
2084  This routine is passed to the post-order visitor as an _ExprVisitor object.
2085
2086  Args:
2087    expr: The IndexExpr being visited.
2088    root: The root of the current structured op.
2089    expr_to_info: The dictionary to look up _ExprInfo for IndexExpr.
2090    structop_inputs: The resulting list of IndexExpr that provide input to the
2091      current structured op.
2092  """
2093  if ((expr != root or isinstance(expr, Access)) and
2094      expr not in structop_inputs) and (isinstance(expr, Access) or
2095                                        (expr in expr_to_info and
2096                                         expr_to_info[expr].structop_info)):
2097    structop_inputs.append(expr)
2098
2099
2100def _emit_structured_op_input(
2101    expr: IndexExpr,
2102    expr_to_info: _ExprInfoDict,
2103    op_def: lang.LinalgOpDef,
2104) -> lang.OperandDef:
2105  """Emits OperandDef in the linalg dialect for the input IndexExpr.
2106
2107  Args:
2108    expr: The input IndexExpr for the current structured op.
2109    expr_to_info: The dictionary to look up _ExprInfo for IndexExpr.
2110    op_def: The linalg operation for the current structured op.
2111
2112  Returns:
2113    An OperandDef in the linalg dialect for the input IndexExpr.
2114  """
2115  op_info = expr_to_info[expr].structop_info
2116  if op_info and not isinstance(expr, Access):
2117    # The input is a temporary tensor produced by another structured op.
2118    indices = op_info.dst_indices
2119    name = op_info.dst_name
2120  else:
2121    # The input is a user provided tensor.
2122    assert isinstance(expr, Access)
2123    indices = expr.indices
2124    name = expr.tensor.name
2125
2126  dim_sym = _mlir_symbols_from_index_vars(indices)
2127  opnd = lang.OperandDef(lang.OperandKind.INPUT_TENSOR, lang.T, dim_sym)
2128  op_def.add_operand(name, opnd)
2129  return opnd
2130
2131
2132def _check_and_build_unary(a: Access, op: _UnaryOp) -> "_UnaryExpr":
2133  """Build a unary operation ceil.
2134
2135    Args:
2136      a: The operand, which could be any Python object from user inputs.
2137      op: An _UnaryOp object representing the operation.
2138
2139    Returns:
2140      A _UnaryExpr object representing the operation.
2141
2142    Raises:
2143      ValueError: If a is not an IndexExpr.
2144    """
2145  if not isinstance(a, Access):
2146    raise ValueError(f"Expected an Access Operand: {a}")
2147  return a._build_unary_expr(op)
2148
2149
2150def ceil(a: Access) -> "_UnaryExpr":
2151  """Defines the operation ceil.
2152
2153    Args:
2154      a: The operand, which could be any Python object from user inputs.
2155
2156    Returns:
2157      A _UnaryExpr object representing the operation.
2158
2159    Raises:
2160      ValueError: If a is not an IndexExpr.
2161    """
2162  return _check_and_build_unary(a, _op_ceil)
2163
2164
2165def floor(a: Access) -> "_UnaryExpr":
2166  """Defines the operation floor.
2167
2168    Args:
2169      a: The operand, which could be any Python object from user inputs.
2170
2171    Returns:
2172      A _UnaryExpr object representing the operation.
2173
2174    Raises:
2175      ValueError: If a is not an IndexExpr.
2176    """
2177  return _check_and_build_unary(a, _op_floor)
2178