code: def make_loop_call(
      typingctx,
      context: numba.core.base.BaseContext,
      builder: ir.IRBuilder,
      scalar_func: Any,
      scalar_signature: types.FunctionType,
      iter_shape: tuple[ir.Instruction, ...],
      inputs: tuple[ir.Instruction, ...],
      outputs: tuple[ir.Instruction, ...],
      input_bc: tuple[tuple[bool, ...], ...],
      output_bc: tuple[tuple[bool, ...], ...],
      input_types: tuple[Any, ...],
      output_types: tuple[Any, ...],
  ):
      safe = (False, False)
      n_outputs = len(outputs)
  
      # context.printf(builder, "iter shape: " + ', '.join(["%i"] * len(iter_shape)) + "\n", *iter_shape)
  
      # Extract shape and stride information from the array.
      # For later use in the loop body to do the indexing
      def extract_array(aryty, obj):
          shape = cgutils.unpack_tuple(builder, obj.shape)
          strides = cgutils.unpack_tuple(builder, obj.strides)
          data = obj.data
          layout = aryty.layout
          return (data, shape, strides, layout)
  
      # TODO I think this is better than the noalias attribute
      # for the input, but self_ref isn't supported in a released
      # llvmlite version yet
      # mod = builder.module
      # domain = mod.add_metadata([], self_ref=True)
      # input_scope = mod.add_metadata([domain], self_ref=True)
      # output_scope = mod.add_metadata([domain], self_ref=True)
      # input_scope_set = mod.add_metadata([input_scope, output_scope])
      # output_scope_set = mod.add_metadata([input_scope, output_scope])
  
      inputs = tuple(
          extract_array(aryty, ary)
          for aryty, ary in zip(input_types, inputs)
      )
  
      outputs = tuple(
          extract_array(aryty, ary)
          for aryty, ary in zip(output_types, outputs)
      )
  
      zero = ir.Constant(ir.IntType(64), 0)
  
      # Setup loops and initialize accumulators for outputs
      # This part corresponds to opening the loops
      loop_stack = []
      loops = []
      output_accumulator: list[tuple[Any | None, int | None]] = [
          (None, None)
      ] * n_outputs
      for dim, length in enumerate(iter_shape):
          # Find outputs that only have accumulations left
          for output in range(n_outputs):
              if output_accumulator[output][0] is not None:
                  continue
              if all(output_bc[output][dim:]):
                  value = outputs[output][0].type.pointee(0)
                  accu = cgutils.alloca_once_value(builder, value)
                  output_accumulator[output] = (accu, dim)
  
          loop = cgutils.for_range(builder, length)
          loop_stack.append(loop)
          loops.append(loop.__enter__())
  
      # Code in the inner most loop...
      idxs = [loopval.index for loopval in loops]
  
      # Load values from input arrays
      input_vals = []
      for array_info, bc in zip(inputs, input_bc):
          idxs_bc = [zero if bc else idx for idx, bc in zip(idxs, bc)]
          ptr = cgutils.get_item_pointer2(
              context, builder, *array_info, idxs_bc, *safe
          )
          val = builder.load(ptr)
          # val.set_metadata("alias.scope", input_scope_set)
          # val.set_metadata("noalias", output_scope_set)
          input_vals.append(val)
  
      inner_codegen = context.get_function(
          scalar_func, scalar_signature
      )
  
      if isinstance(
          scalar_signature.args[0],
          (types.StarArgTuple, types.StarArgUniTuple),
      ):
          input_vals = [
              context.make_tuple(
                  builder, scalar_signature.args[0], input_vals
              )
          ]
      output_values = inner_codegen(builder, input_vals)
  
      if isinstance(
          scalar_signature.return_type, (types.Tuple, types.UniTuple)
      ):
          output_values = cgutils.unpack_tuple(builder, output_values)
          func_output_types = scalar_signature.return_type.types
      else:
          output_values = [output_values]
          func_output_types = [scalar_signature.return_type]
  
      # Update output value or accumulators respectively
      for i, ((accu, _), value) in enumerate(
          zip(output_accumulator, output_values)
      ):
          if accu is not None:
              load = builder.load(accu)
              # load.set_metadata("alias.scope", output_scope_set)
              # load.set_metadata("noalias", input_scope_set)
              new_value = builder.fadd(load, value)
              builder.store(new_value, accu)
              # TODO belongs to noalias scope
              # store.set_metadata("alias.scope", output_scope_set)
              # store.set_metadata("noalias", input_scope_set)
          else:
              idxs_bc = [
                  zero if bc else idx
                  for idx, bc in zip(idxs, output_bc[i])
              ]
              ptr = cgutils.get_item_pointer2(
                  context, builder, *outputs[i], idxs_bc
              )
              # store = builder.store(value, ptr)
              value = context.cast(
                  builder,
                  value,
                  func_output_types[i],
                  output_types[i].dtype,
              )
              arrayobj.store_item(
                  context, builder, output_types[i], value, ptr
              )
              # store.set_metadata("alias.scope", output_scope_set)
              # store.set_metadata("noalias", input_scope_set)
  
      # Close the loops and write accumulator values to the output arrays
      for depth, loop in enumerate(loop_stack[::-1]):
          for output, (accu, accu_depth) in enumerate(
              output_accumulator
          ):
              if accu_depth == depth:
                  idxs_bc = [
                      zero if bc else idx
                      for idx, bc in zip(idxs, output_bc[output])
                  ]
                  ptr = cgutils.get_item_pointer2(
                      context, builder, *outputs[output], idxs_bc
                  )
                  load = builder.load(accu)
                  # load.set_metadata("alias.scope", output_scope_set)
                  # load.set_metadata("noalias", input_scope_set)
                  # store = builder.store(load, ptr)
                  load = context.cast(
                      builder,
                      load,
                      func_output_types[output],
                      output_types[output].dtype,
                  )
                  arrayobj.store_item(
                      context, builder, output_types[output], load, ptr
                  )
                  # store.set_metadata("alias.scope", output_scope_set)
                  # store.set_metadata("noalias", input_scope_set)
          loop.__exit__(None, None, None)
  
      return
  
file_path: pytensor/pytensor/link/numba/dispatch/elemwise_codegen.py
imports: from __future__ import annotations
  from typing import Any
  import numba
  import numpy
  from llvmlite import ir
  from numba import types
  from numba.core import cgutils
  from numba.core.base import BaseContext
  from numba.np import arrayobj
label_desc: Use image node_id 3 for calling a global function with example usage: make_loop_call(typingctx, context, builder, scalar_func, scalar_signature, iter_shape, inputs, outputs, input_bc, output_bc, input_types, output_types) and returns: data,  shape,  strides,  layout