class_docstr: Scalar Op that encapsulates a scalar loop operation.
  
  This Op can be used for the gradient of other Scalar Ops.
  It is much more restricted than `Scan` in that the entire inner graph
  must be composed of Scalar operations, and all inputs and outputs must be ScalarVariables.
  
  The pseudocode of the computation performed by this Op looks like the following:
  
  ```python
  def scalar_for_loop(fn, n_steps, init, update, constant):
      for i in range(n_steps):
          state = fn(*state, *constant)
      return state
  ```
  
  When an until condition is present it behaves like this:
  
  ```python
  def scalar_while_loop(fn, n_steps, init, update, constant):
      # If n_steps <= 0, we skip the loop altogether.
      # This does not count as a "failure"
      done = True
  
      for i in range(n_steps):
          *state, done = fn(*state, *constant)
          if done:
              break
  
      return *state, done
  ```
class_docstr_tok: ["Scalar","Op","that","encapsulates","a","scalar","loop","operation",".","This","Op","can","be","used","for","the","gradient","of","other","Scalar","Ops",".","It","is","much","more","restricted","than","`","Scan","`","in","that","the","entire","inner","graph","must","be","composed","of","Scalar","operations",",","and","all","inputs","and","outputs","must","be","ScalarVariables",".","The","pseudocode","of","the","computation","performed","by","this","Op","looks","like","the","following",":","``","`","python","def","scalar_for_loop","(","fn",",","n_steps",",","init",",","update",",","constant",")",":","for","i","in","range","(","n_steps",")",":","state","=","fn","(","*","state",",","*","constant",")","return","state","``","`","When","an","until","condition","is","present","it","behaves","like","this",":","``","`","python","def","scalar_while_loop","(","fn",",","n_steps",",","init",",","update",",","constant",")",":","#","If","n_steps","<","=","0",",","we","skip","the","loop","altogether",".","#","This","does","not","count","as","a","``","failure","''","done","=","True","for","i","in","range","(","n_steps",")",":","*","state",",","done","=","fn","(","*","state",",","*","constant",")","if","done",":","break","return","*","state",",","done","``","`"]
code: def c_code_template(self):
      from pytensor.link.c.interface import CLinkerType
  
      if hasattr(self, "_c_code"):
          return self._c_code
  
      fgraph = self.fgraph
  
      # The first input is `n_steps` so we skip it in the mapping dictionary
      n_update = len(self.outputs) - (1 if self.is_while else 0)
      carry_subd = {
          c: f"%(i{int(i)})s"
          for i, c in enumerate(fgraph.inputs[:n_update], start=1)
      }
      constant_subd = {
          c: f"%(i{int(i)})s"
          for i, c in enumerate(
              fgraph.inputs[n_update:], start=n_update + 1
          )
      }
      update_subd = {
          u: f"%(o{int(i)})s"
          for i, u in enumerate(fgraph.outputs[:n_update])
      }
      until_subd = {u: "until" for u in fgraph.outputs[n_update:]}
      subd = {
          **carry_subd,
          **constant_subd,
          **update_subd,
          **until_subd,
      }
  
      for var in fgraph.variables:
          if var.owner is None:
              if var not in self.fgraph.inputs:
                  # This is an orphan
                  if isinstance(var, Constant) and isinstance(
                      var.type, CLinkerType
                  ):
                      subd[var] = var.type.c_literal(var.data)
                  else:
                      raise ValueError(
                          "All orphans in the fgraph to ScalarLoop must"
                          " be Constant, CLinkerType instances."
                      )
          elif any(
              i.dtype == "float16" for i in var.owner.inputs
          ) or any(o.dtype == "float16" for o in var.owner.outputs):
              # flag for elemwise ops to check.
              self.inner_float16 = True
  
      _c_code = "{\n"
      if self.is_while:
          _c_code += "bool until = 1;\n\n"
  
      # Copy carried inputs
      for i, (var, name) in enumerate(carry_subd.items()):
          copy_var_name = f"{name}_copy{i}"
          _c_code += (
              f"{var.type.dtype_specs()[1]} {copy_var_name} = {name};\n"
          )
          carry_subd[var] = copy_var_name
          subd[var] = copy_var_name
  
      # _c_code += 'printf("inputs=[");'
      # for i in range(1, len(fgraph.inputs)):
      #     _c_code += f'printf("%%.16g, ", %(i{i})s);'
      # _c_code += 'printf("]\\n");\n'
  
      _c_code += (
          "\nfor(%(n_steps_dtype)s i = 0; i < %(n_steps)s; i++){\n"
      )
  
      self.nodenames = [
          f"%(nodename)s_subnode{int(j)}"
          for j, n in enumerate(fgraph.toposort())
      ]
  
      i = 0
      for j, node in enumerate(fgraph.toposort()):
          for output in node.outputs:
              if output not in subd:
                  i += 1
                  name = f"V%(id)s_tmp{int(i)}"
                  subd[output] = name
                  _c_code += f"{output.type.dtype_specs()[1]} {name};\n"
          s = node.op.c_code(
              node,
              self.nodenames[j],
              # Any node that depended on `init` will depend on `update` instead
              # The initial value of `update` was set to `init` before the loop
              [subd[input] for input in node.inputs],
              [subd[output] for output in node.outputs],
              dict(fail="%(fail)s", id=f"%(id)s_{int(j)}"),
          )
          _c_code += s
          _c_code += "\n"
  
      # Set the carry variables to the output variables
      _c_code += "\n"
      for init, update in zip(
          carry_subd.values(), update_subd.values()
      ):
          _c_code += f"{init} = {update};\n"
  
      # _c_code += 'printf("%%ld\\n", i);\n'
      # for carry in range(1, 10):
      #     _c_code += f'printf("\\t %%.g\\n", i, %(i{carry})s_copy{carry-1});\n'
  
      if self.is_while:
          _c_code += "\nif(until){break;}\n"
  
      # End of the loop
      _c_code += "}\n"
  
      # Output until flag
      if self.is_while:
          _c_code += f"%(o{len(fgraph.outputs)-1})s = until;\n"
  
      _c_code += "}\n"
  
      self._c_code = _c_code
  
      return self._c_code
  
imports: from collections.abc import Sequence
  from itertools import chain
  from typing import Optional
  from pytensor.compile import rebuild_collect_shared
  from pytensor.graph.basic import Constant, Variable, clone
  from pytensor.graph.fg import FunctionGraph
  from pytensor.scalar.basic import ScalarInnerGraphOp, as_scalar
label_desc: Use image node_id 10 for calling the ScalarLoop obj's underlying member method code with example usage: obj.c_code_template() and returns: self, self