text: #!/usr/bin/env python
  
  # Copyright 2019 Stanford University
  #
  # Licensed under the Apache License, Version 2.0 (the "License");
  # you may not use this file except in compliance with the License.
  # You may obtain a copy of the License at
  #
  #     http://www.apache.org/licenses/LICENSE-2.0
  #
  # Unless required by applicable law or agreed to in writing, software
  # distributed under the License is distributed on an "AS IS" BASIS,
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  # See the License for the specific language governing permissions and
  # limitations under the License.
  #
  
  from __future__ import absolute_import, division, print_function, unicode_literals
  
  import cffi
  try:
      import cPickle as pickle
  except ImportError:
      import pickle
  import collections
  import itertools
  import math
  import numpy
  import os
  import re
  import subprocess
  import sys
  import threading
  import weakref
  
  # Python 3.x compatibility:
  try:
      long # Python 2
  except NameError:
      long = int  # Python 3
  
  try:
      basestring # Python 2
  except NameError:
      basestring = str # Python 3
  
  try:
      xrange # Python 2
  except NameError:
      xrange = range # Python 3
  
  try:
      imap = itertools.imap # Python 2
  except:
      imap = map # Python 3
  
  try:
      zip_longest = itertools.izip_longest # Python 2
  except:
      zip_longest = itertools.zip_longest # Python 3
  
  from io import StringIO
  
  _pickle_version = pickle.HIGHEST_PROTOCOL # Use latest Pickle protocol
  
  def find_legion_header():
      def try_prefix(prefix_dir):
          legion_h_path = os.path.join(prefix_dir, 'legion.h')
          if os.path.exists(legion_h_path):
              return prefix_dir, legion_h_path
  
      # For in-source builds, find the header relative to the bindings
      root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
      runtime_dir = os.path.join(root_dir, 'runtime')
      result = try_prefix(runtime_dir)
      if result:
          return result
  
      # If this was installed to a non-standard prefix, we might be able
      # to guess from the directory structures
      if os.path.basename(root_dir) == 'lib':
          include_dir = os.path.join(os.path.dirname(root_dir), 'include')
          result = try_prefix(include_dir)
          if result:
              return result
  
      # Otherwise we have to hope that Legion is installed in a standard location
      result = try_prefix('/usr/include')
      if result:
          return result
  
      result = try_prefix('/usr/local/include')
      if result:
          return result
  
      raise Exception('Unable to locate legion.h header file')
  
  try:
      with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'cached_legion.h'), 'r') as f:
          header = f.read()
  except IOError as e:
      print('Unable to find cached_legion.h, falling back to reading legion.h')
      prefix_dir, legion_h_path = find_legion_header()
  
      # If we're running from a local Python build, we need to include
      # the path where legion.py is located in order to find
      # legion_defines.h.
      legion_py_dir = os.path.dirname(os.path.realpath(__file__))
  
      # If we're running inside of Regent, try to parse the include path
      # since we'll need it to locate legion_defines.h.
      include_path = os.environ.get('INCLUDE_PATH', '').split(';')
      include_path = [x for y in include_path for x in ['-I', y]]
  
      header = subprocess.check_output(['gcc', '-I', prefix_dir, '-I', legion_py_dir] + include_path + ['-DLEGION_USE_PYTHON_CFFI', '-E', '-P', legion_h_path]).decode('utf-8')
  
  ffi = cffi.FFI()
  ffi.cdef(header)
  c = ffi.dlopen(None)
  
  _max_dim = None
  for dim in range(1, 9):
      try:
          getattr(c, 'legion_domain_get_rect_{}d'.format(dim))
      except AttributeError:
          break
      _max_dim = dim
  assert _max_dim is not None, 'Unable to detect LEGION_MAX_DIM'
  
  # Can't seem to pull this out of the header, so reproduce it here.
  AUTO_GENERATE_ID = -1
  
  # Note: don't use __file__ here, it may return either .py or .pyc and cause
  # non-deterministic failures.
  library_name = "legion.py"
  max_legion_python_tasks = 1000000
  next_legion_task_id = c.legion_runtime_generate_library_task_ids(
                          c.legion_runtime_get_runtime(),
                          library_name.encode('utf-8'),
                          max_legion_python_tasks)
  max_legion_task_id = next_legion_task_id + max_legion_python_tasks
  
  # Returns true if this module is running inside of a Legion
  # executable. If false, then other Legion functionality should not be
  # expected to work.
  def inside_legion_executable():
      try:
          c.legion_get_current_time_in_micros()
      except AttributeError:
          return False
      else:
          return True
  
  def input_args(filter_runtime_options=False):
      raw_args = c.legion_runtime_get_input_args()
  
      args = []
      for i in range(raw_args.argc):
          args.append(ffi.string(raw_args.argv[i]).decode('utf-8'))
  
      if filter_runtime_options:
          i = 1 # Skip program name
  
          prefixes = ['-lg:', '-hl:', '-realm:', '-ll:', '-cuda:', '-numa:',
                      '-dm:', '-bishop:']
          while i < len(args):
              match = False
              for prefix in prefixes:
                  if args[i].startswith(prefix):
                      match = True
                      break
              if args[i] == '-level':
                  match = True
              if args[i] == '-logfile':
                  match = True
              if match:
                  args.pop(i)
                  args.pop(i) # Assume that every option has an argument
                  continue
              i += 1
      return args
  
  def execute_as_script():
      args = input_args(True)
      if len(args) < 1:
          return False, False # no idea what's going on here, just return
      if os.path.basename(args[0]) != 'legion_python':
          return False, False # not in legion_python
      if len(args) < 2 or args[1].startswith('-'):
          return True, False # argument is a flag
      # If it has an extension, we're going to guess that it was
      # intended to be a script.
      return True, len(os.path.splitext(args[1])[1]) > 1
  
  is_legion_python, is_script = execute_as_script()
  
  # The Legion context is stored in thread-local storage. This assumes
  # that the Python processor maintains the invariant that every task
  # corresponds to one and only one thread.
  _my = threading.local()
  
  global_task_registration_barrier = None
  
  class Context(object):
      __slots__ = ['context_root', 'context', 'runtime_root', 'runtime',
                   'task_root', 'task', 'regions',
                   'owned_objects', 'current_launch', 'next_trace_id']
      def __init__(self, context_root, runtime_root, task_root, regions):
          self.context_root = context_root
          self.context = self.context_root[0]
          self.runtime_root = runtime_root
          self.runtime = self.runtime_root[0]
          self.task_root = task_root
          self.task = self.task_root[0]
          self.regions = regions
          self.owned_objects = []
          self.current_launch = None
          self.next_trace_id = 0
      def track_object(self, obj):
          self.owned_objects.append(weakref.ref(obj))
      def begin_launch(self, launch):
          assert self.current_launch == None
          self.current_launch = launch
      def end_launch(self, launch):
          assert self.current_launch == launch
          self.current_launch = None
  
  # Hack: Can't pickle static methods.
  def _DomainPoint_unpickle(values):
      return DomainPoint(values)
  
  class DomainPoint(object):
      __slots__ = [
          'handle',
          '_point', # cached properties
      ]
      def __init__(self, values, **kwargs):
          def parse_kwargs(_handle=None):
              return _handle
          handle = parse_kwargs(**kwargs)
  
          if values is not None:
              assert handle is None
              try:
                  len(values)
              except TypeError:
                  values = [values]
              assert 1 <= len(values) <= _max_dim
              self.handle = ffi.new('legion_domain_point_t *')
              self.handle[0].dim = len(values)
              for i, value in enumerate(values):
                  self.handle[0].point_data[i] = value
          else:
              # Important: Copy handle. Do NOT assume ownership.
              assert handle is not None
              self.handle = ffi.new('legion_domain_point_t *', handle)
  
          self._point = None
  
      def __reduce__(self):
          return (_DomainPoint_unpickle,
                  ([self.handle[0].point_data[i] for i in xrange(self.dim)],))
  
      def __int__(self):
          assert self.dim == 1
          return self.handle[0].point_data[0]
  
      def __index__(self):
          return self.__int__()
  
      def __getitem__(self, i):
          assert 0 <= i < self.dim
          return self.handle[0].point_data[i]
  
      def __eq__(self, other):
          if not isinstance(other, DomainPoint):
              return NotImplemented
          return numpy.array_equal(self.point, other.point)
  
      def __str__(self):
          if self.dim == 1:
              return str(int(self))
          return str(self.point)
  
      def __repr__(self):
          return 'DomainPoint({})'.format(self.point)
  
      @property
      def dim(self):
          return self.handle[0].dim
  
      @property
      def point(self):
          if self._point is None:
              self._point = self.asarray()
          return self._point
  
      @staticmethod
      def coerce(value):
          if not isinstance(value, DomainPoint):
              return DomainPoint(value)
          return value
  
      def raw_value(self):
          return self.handle[0]
  
      def asarray(self):
          return numpy.frombuffer(
              ffi.buffer(self.handle[0].point_data),
              count=self.dim,
              dtype=numpy.int64)
  
  class Domain(object):
      __slots__ = [
          'handle',
          '_bounds', # cached properties
      ]
      def __init__(self, extent, start=None, **kwargs):
          def parse_kwargs(_handle=None):
              return _handle
          handle = parse_kwargs(**kwargs)
  
          if extent is not None:
              assert handle is None
              try:
                  len(extent)
              except TypeError:
                  extent = [extent]
              if start is not None:
                  try:
                      len(start)
                  except TypeError:
                      start = [start]
                  assert len(start) == len(extent)
              else:
                  start = [0 for _ in extent]
              assert 1 <= len(extent) <= _max_dim
              rect = ffi.new('legion_rect_{}d_t *'.format(len(extent)))
              for i in xrange(len(extent)):
                  rect[0].lo.x[i] = start[i]
                  rect[0].hi.x[i] = start[i] + extent[i] - 1
              handle = getattr(c, 'legion_domain_from_rect_{}d'.format(len(extent)))(rect[0])
  
          # Important: Copy handle. Do NOT assume ownership.
          assert handle is not None
          self.handle = ffi.new('legion_domain_t *', handle)
          self._bounds = None
  
      @property
      def dim(self):
          return self.handle[0].dim
  
      @property
      def volume(self):
          return c.legion_domain_get_volume(self.handle[0])
  
      @property
      def bounds(self):
          if self._bounds is None:
              self._bounds = self.asarray()
          return self._bounds
  
      @property
      def extent(self):
          bounds = self.bounds
          return bounds[1] - bounds[0] + 1
  
      @property
      def start(self):
          return self.bounds[0]
  
      @staticmethod
      def coerce(value):
          if not isinstance(value, Domain):
              return Domain(value)
          return value
  
      def __iter__(self):
          return imap(
              DomainPoint,
              itertools.product(
                  *[xrange(
                      self.handle[0].rect_data[i],
                      self.handle[0].rect_data[i+self.dim] + 1)
                    for i in xrange(self.dim)]))
  
      def raw_value(self):
          return self.handle[0]
  
      def asarray(self):
          return numpy.frombuffer(
              ffi.buffer(self.handle[0].rect_data),
              count=self.dim * 2,
              dtype=numpy.int64
          ).reshape((2, self.dim))
  
  class DomainTransform(object):
      __slots__ = ['handle']
      def __init__(self, matrix, **kwargs):
          def parse_kwargs(_handle=None):
              return _handle
          handle = parse_kwargs(**kwargs)
  
          if matrix is not None:
              assert handle is None
              matrix = numpy.asarray(matrix, dtype=numpy.int64)
              transform = ffi.new('legion_transform_{}x{}_t *'.format(*matrix.shape))
              ffi.buffer(transform[0].trans)[:] = matrix
              handle = getattr(c, 'legion_domain_transform_from_{}x{}'.format(*matrix.shape))(transform[0])
  
          # Important: Copy handle. Do NOT assume ownership.
          assert handle is not None
          self.handle = ffi.new('legion_domain_transform_t *', handle)
  
      @staticmethod
      def coerce(value):
          if not isinstance(value, DomainTransform):
              return DomainTransform(value)
          return value
  
      def raw_value(self):
          return self.handle[0]
  
  class Future(object):
      __slots__ = ['handle', 'value_type', 'argument_number']
      def __init__(self, value, value_type=None, argument_number=None):
          if value is None:
              self.handle = None
          elif isinstance(value, Future):
              value.resolve_handle()
              self.handle = c.legion_future_copy(value.handle)
              if value_type is None:
                  value_type = value.value_type
          elif value_type is not None:
              if value_type.size > 0:
                  value_ptr = ffi.new(ffi.getctype(value_type.cffi_type, '*'), value)
              else:
                  value_ptr = ffi.NULL
              value_size = value_type.size
              self.handle = c.legion_future_from_untyped_pointer(_my.ctx.runtime, value_ptr, value_size)
          else:
              value_str = pickle.dumps(value, protocol=_pickle_version)
              value_size = len(value_str)
              value_ptr = ffi.new('char[]', value_size)
              ffi.buffer(value_ptr, value_size)[:] = value_str
              self.handle = c.legion_future_from_untyped_pointer(_my.ctx.runtime, value_ptr, value_size)
  
          self.value_type = value_type
          self.argument_number = argument_number
  
      @staticmethod
      def from_cdata(value, *args, **kwargs):
          result = Future(None, *args, **kwargs)
          result.handle = c.legion_future_copy(value)
          return result
  
      @staticmethod
      def from_buffer(value, *args, **kwargs):
          result = Future(None, *args, **kwargs)
          result.handle = c.legion_future_from_untyped_pointer(_my.ctx.runtime, ffi.from_buffer(value), len(value))
          return result
  
      def __del__(self):
          if self.handle is not None:
              c.legion_future_destroy(self.handle)
  
      def __reduce__(self):
          if self.argument_number is None:
              raise Exception('Cannot pickle a Future except when used as a task argument')
          return (Future, (None, self.value_type, self.argument_number))
  
      def resolve_handle(self):
          if self.handle is None and self.argument_number is not None:
              self.handle = c.legion_future_copy(
                  c.legion_task_get_future(_my.ctx.task, self.argument_number))
  
      def get(self):
          self.resolve_handle()
  
          if self.handle is None:
              return
          if self.value_type is None:
              value_ptr = c.legion_future_get_untyped_pointer(self.handle)
              value_size = c.legion_future_get_untyped_size(self.handle)
              assert value_size > 0
              value_str = ffi.unpack(ffi.cast('char *', value_ptr), value_size)
              value = pickle.loads(value_str)
              return value
          elif self.value_type.size == 0:
              c.legion_future_get_void_result(self.handle)
          else:
              expected_size = ffi.sizeof(self.value_type.cffi_type)
  
              value_ptr = c.legion_future_get_untyped_pointer(self.handle)
              value_size = c.legion_future_get_untyped_size(self.handle)
              assert value_size == expected_size
              value = ffi.cast(ffi.getctype(self.value_type.cffi_type, '*'), value_ptr)[0]
              # Hack: Use closure to keep self alive as long as the value is live.
              if isinstance(value, ffi.CData):
                  return ffi.gc(value, lambda x: self)
              return value
  
      def get_buffer(self):
          self.resolve_handle()
  
          if self.handle is None:
              return
          value_ptr = c.legion_future_get_untyped_pointer(self.handle)
          value_size = c.legion_future_get_untyped_size(self.handle)
          return ffi.buffer(value_ptr, value_size)
  
  class FutureMap(object):
      __slots__ = ['handle', 'value_type']
      def __init__(self, handle, value_type=None):
          self.handle = c.legion_future_map_copy(handle)
          self.value_type = value_type
  
      def __del__(self):
          c.legion_future_map_destroy(self.handle)
  
      def __getitem__(self, point):
          point = DomainPoint.coerce(point)
          return Future.from_cdata(
              c.legion_future_map_get_future(self.handle, point.raw_value()),
              value_type=self.value_type)
  
      def wait_all_results(self):
          c.legion_future_map_wait_all_results(self.handle)
  
  _type_cache = {}
  
  class Type(object):
      __slots__ = ['numpy_type', 'cffi_type', 'size']
  
      def __new__(cls, numpy_type, cffi_type):
          if cffi_type in _type_cache:
              return _type_cache[cffi_type]
          obj = super(Type, cls).__new__(cls)
          _type_cache[cffi_type] = obj
          return obj
  
      def __init__(self, numpy_type, cffi_type):
          assert (numpy_type is None) == (cffi_type is None)
          self.numpy_type = numpy_type
          self.cffi_type = cffi_type
          self.size = ffi.sizeof(cffi_type) if cffi_type is not None else 0
  
      def __reduce__(self):
          return (Type, (self.numpy_type, self.cffi_type))
  
  # Pre-defined Types
  void = Type(None, None)
  bool_ = Type(numpy.bool_, 'bool')
  complex64 = Type(numpy.complex64, 'float _Complex')
  complex128 = Type(numpy.complex128, 'double _Complex')
  float32 = Type(numpy.float32, 'float')
  float64 = Type(numpy.float64, 'double')
  int8 = Type(numpy.int8, 'int8_t')
  int16 = Type(numpy.int16, 'int16_t')
  int32 = Type(numpy.int32, 'int32_t')
  int64 = Type(numpy.int64, 'int64_t')
  uint8 = Type(numpy.uint8, 'uint8_t')
  uint16 = Type(numpy.uint16, 'uint16_t')
  uint32 = Type(numpy.uint32, 'uint32_t')
  uint64 = Type(numpy.uint64, 'uint64_t')
  
  _rect_types = []
  for dim in xrange(1, _max_dim + 1):
      globals()["int{}d".format(dim)] = Type(
          numpy.dtype([('x', numpy.int64, (dim,))], align=True),
          'legion_point_{}d_t'.format(dim))
      rtype = Type(
          numpy.dtype([('lo', numpy.int64, (dim,)), ('hi', numpy.int64, (dim,))], align=True),
          'legion_rect_{}d_t'.format(dim))
      globals()["rect{}d".format(dim)] = rtype
      _rect_types.append(rtype)
  _rect_types = frozenset(_rect_types)
  
  def is_rect_type(t):
      return t in _rect_types
  
  _redop_ids = {}
  def _fill_redop_ids():
      operators = ['+', '-', '*', '/', 'max', 'min']
      types = [bool_, int16, int32, int64, uint16, uint32, uint64, None, float32, float64, None, complex64, complex128]
      next_id = 1048576
      for operator in operators:
          _redop_ids[operator] = {}
          for type in types:
              if type is not None:
                  _redop_ids[operator][type] = next_id
              next_id += 1
  _fill_redop_ids()
  
  class Privilege(object):
      __slots__ = ['read', 'write', 'discard', 'reduce', 'fields']
  
      def __init__(self, read=False, write=False, discard=False, reduce=False, fields=None):
          self.read = read
          self.write = write
          self.discard = discard
          self.reduce = reduce
          self.fields = fields
  
          if self.fields is not None:
              assert len(self.fields) > 0
  
          if self.discard:
              assert self.write
  
      def _fields(self):
          return (self.read, self.write, self.discard, self.reduce, self.fields)
  
      def __eq__(self, other):
          if not isinstance(other, Privilege):
              return NotImplemented
          return self._fields() == other._fields()
  
      def __ne__(self, other):
          return not (self == other)
  
      def __hash__(self):
          return hash(self._fields())
  
      def __call__(self, *fields):
          assert self.fields is None
          return Privilege(self.read, self.write, self.discard, self.reduce, fields)
  
      def __add__(self, other):
          return PrivilegeComposite([self, other])
  
      def __repr__(self):
          return str(self)
  
      def __str__(self):
          if self.discard:
              priv = 'WD'
          elif self.write:
              priv = 'RW'
          elif self.read:
              priv = 'R'
          elif self.reduce:
              priv = 'Reduce(%s)' % self.reduce
          else:
              priv = 'N'
          if self.fields is not None:
              return '%s(%s)' % (priv, ', '.join(self.fields))
          return priv
  
      def _legion_privilege(self):
          bits = 0
          if self.discard:
              bits |= 2 # WRITE_DISCARD
          elif self.reduce:
              assert False
          else:
              if self.write: bits = 7 # READ_WRITE
              elif self.read: bits = 1 # READ_ONLY
          return bits
  
      def _legion_grouped_privileges(self, fspace):
          if self.fields:
              if not set(self.fields) <= set(fspace.keys()):
                  raise Exception(
                      'Privilege fields ({}) are not a subset of fspace fields ({})'.format(
                          ' '.join(self.fields), ' '.join(fspace.keys())))
          fields = fspace.keys() if self.fields is None else self.fields
          if self.reduce:
              return [
                  (self, None, self._legion_redop_id(fspace.field_types[field_name]), (field_name,))
                  for field_name in fields]
          else:
              return [(self, self._legion_privilege(), None, fields if self.read or self.write or self.reduce else [])]
  
      def _legion_redop_id(self, field_type):
          return _redop_ids[self.reduce][field_type]
  
  class PrivilegeComposite(object):
      __slots__ = ['privileges']
  
      def __init__(self, privileges):
          self.privileges = self.normalize(privileges)
  
      @staticmethod
      def normalize(privileges):
          fields = collections.OrderedDict()
          read_set = set()
          write_set = set()
          discard_set = set()
          reduce_sets = collections.OrderedDict()
  
          for privilege in privileges:
              privilege_fields = privilege.fields if privilege.fields is not None else [None]
              fields.update([(x, True) for x in privilege_fields])
              if privilege.read:
                  read_set.update(privilege_fields)
              if privilege.write:
                  write_set.update(privilege_fields)
              if privilege.discard:
                  discard_set.update(privilege_fields)
              if privilege.reduce:
                  if privilege.reduce not in reduce_sets:
                      reduce_sets[privilege.reduce] = set()
                  reduce_sets[privilege.reduce].update(privilege_fields)
  
          # Reductions combine with read/reduce privileges to upgrade to read-write.
          for op, reduce_set in reduce_sets.items():
              write_set.update(reduce_set & read_set)
              if None in read_set:
                  write_set.update(reduce_set)
              for op2, reduce_set2 in reduce_sets.items():
                  if op != op2:
                      write_set.update(reduce_set & reduce_set2)
                      if None in reduce_set2:
                          write_set.update(reduce_set)
  
          # Read/write/discard shadow reduction privileges.
          if None in read_set or None in write_set or None in discard_set:
              reduce_sets = collections.OrderedDict()
          else:
              for reduce_set in reduce_sets.values():
                  reduce_set.difference_update(read_set, write_set, discard_set)
  
          # Discard shadows read/write.
          if None in discard_set:
              read_set = set()
              write_set = set()
              discard_set = set([None])
          else:
              read_set -= discard_set
              write_set -= discard_set
  
          # Write shadows read.
          if None in write_set:
              read_set = set()
              write_set = set([None])
          else:
              read_set -= write_set
  
          def filter_set(ctor, field_set):
              if None in field_set:
                  return ctor
              return ctor(*filter(lambda x: x in field_set, fields.keys()))
  
          return tuple(
              ([filter_set(R, read_set)] if len(read_set) > 0 else []) +
              ([filter_set(RW, write_set)] if len(write_set) > 0 else []) +
              ([filter_set(WD, discard_set)] if len(discard_set) > 0 else []) +
              [filter_set(Reduce(op), reduce_set) for op, reduce_set in reduce_sets.items()])
  
      def __eq__(self, other):
          if len(self.privileges) == 1:
              return other == self.privileges[0]
  
          if not isinstance(other, PrivilegeComposite):
              return NotImplemented
          return self.privileges == other.privileges
  
      def __ne__(self, other):
          return not (self == other)
  
      def __hash__(self):
          return hash(self.privileges)
  
      def __add__(self, other):
          return PrivilegeComposite(self.privileges + (other,))
  
      def __repr__(self):
          return str(self)
  
      def __str__(self):
          return ' + '.join(map(str, self.privileges))
  
      def _legion_grouped_privileges(self, fspace):
          return [x for privilege in self.privileges for x in privilege._legion_grouped_privileges(fspace)]
  
  # Pre-defined Privileges
  N = Privilege()
  R = Privilege(read=True)
  RO = Privilege(read=True)
  RW = Privilege(read=True, write=True)
  WD = Privilege(write=True, discard=True)
  
  def Reduce(operator, *fields):
      return Privilege(reduce=operator, fields=fields if len(fields) > 0 else None)
  
  class Disjointness(object):
      __slots__ = ['kind', 'value']
  
      def __init__(self, kind, value):
          self.kind = kind
          self.value = value
  
      def __eq__(self, other):
          return isinstance(other, Disjointness) and self.value == other.value
  
      def __cmp__(self, other):
          assert isinstance(other, Disjointness)
          return self.value.__cmp__(other.value)
  
      def __hash__(self):
          return hash(self.value)
  
      def __str__(self):
          return self.kind
  
  disjoint = Disjointness('disjoint', 0)
  aliased = Disjointness('aliased', 1)
  compute = Disjointness('compute', 2)
  disjoint_complete = Disjointness('disjoint_complete', 3)
  aliased_complete = Disjointness('aliased_complete', 4)
  compute_complete = Disjointness('compute_complete', 5)
  disjoint_incomplete = Disjointness('disjoint_incomplete', 6)
  aliased_incomplete = Disjointness('aliased_incomplete', 7)
  compute_incomplete = Disjointness('compute_incomplete', 8)
  
  # Hack: Can't pickle static methods.
  def _Ispace_unpickle(ispace_tid, ispace_id, ispace_type_tag, owned):
      handle = ffi.new('legion_index_space_t *')
      handle[0].tid = ispace_tid
      handle[0].id = ispace_id
      handle[0].type_tag = ispace_type_tag
      return Ispace(None, _handle=handle[0], _owned=owned)
  
  class Ispace(object):
      __slots__ = [
          'handle', 'owned', 'escaped',
          '_domain', # cached properties
          '__weakref__', # allow weak references
      ]
  
      def __init__(self, extent, start=None, name=None, **kwargs):
          def parse_kwargs(_handle=None, _owned=False):
              return _handle, _owned
          handle, owned = parse_kwargs(**kwargs)
  
          if extent is not None:
              assert handle is None
              domain = Domain(extent, start=start).raw_value()
              handle = c.legion_index_space_create_domain(_my.ctx.runtime, _my.ctx.context, domain)
              if name is not None:
                  c.legion_index_space_attach_name(_my.ctx.runtime, handle, name.encode('utf-8'), False)
              owned = True
  
          # Important: Copy handle. Do NOT assume ownership.
          assert handle is not None
          self.handle = ffi.new('legion_index_space_t *', handle)
          self.owned = owned
          self.escaped = False
          self._domain = None
  
          if self.owned:
              _my.ctx.track_object(self)
  
      def __del__(self):
          if self.owned and not self.escaped:
              self.destroy()
  
      def __reduce__(self):
          return (_Ispace_unpickle,
                  (self.handle[0].tid,
                   self.handle[0].id,
                   self.handle[0].type_tag,
                   self.owned and self.escaped))
  
      def __iter__(self):
          return self.domain.__iter__()
  
      @property
      def domain(self):
          if self._domain is None:
              self._domain = Domain(None, _handle=c.legion_index_space_get_domain(_my.ctx.runtime, self.handle[0]))
          return self._domain
  
      @property
      def dim(self):
          return self.domain.dim
  
      @property
      def volume(self):
          return self.domain.volume
  
      @property
      def bounds(self):
          return self.domain.bounds
  
      @staticmethod
      def coerce(value):
          if not isinstance(value, Ispace):
              return Ispace(value)
          return value
  
      def destroy(self):
          assert self.owned and not self.escaped
  
          # This is not something you want to have happen in a
          # destructor, since fspaces may outlive the lifetime of the handle.
          c.legion_index_space_destroy(
              _my.ctx.runtime, _my.ctx.context, self.handle[0])
          # Clear out references. Technically unnecessary but avoids abuse.
          del self.handle
  
      def raw_value(self):
          return self.handle[0]
  
  # Hack: Can't pickle static methods.
  def _Fspace_unpickle(fspace_id, field_ids, field_types, owned):
      handle = ffi.new('legion_field_space_t *')
      handle[0].id = fspace_id
      return Fspace(None, _handle=handle[0], _field_ids=field_ids, _field_types=field_types, _owned=owned)
  
  class Fspace(object):
      __slots__ = [
          'handle', 'field_ids', 'field_types',
          'owned', 'escaped',
          '__weakref__', # allow weak references
      ]
  
      def __init__(self, fields, name=None, **kwargs):
          def parse_kwargs(_handle=None, _field_ids=None, _field_types=None, _owned=False):
              return _handle, _field_ids, _field_types, _owned
          handle, field_ids, field_types, owned = parse_kwargs(**kwargs)
  
          if fields is not None:
              assert handle is None and field_ids is None and field_types is None
              handle = c.legion_field_space_create(_my.ctx.runtime, _my.ctx.context)
              if name is not None:
                  c.legion_field_space_attach_name(_my.ctx.runtime, handle, name.encode('utf-8'), False)
              alloc = c.legion_field_allocator_create(
                  _my.ctx.runtime, _my.ctx.context, handle)
              field_ids = collections.OrderedDict()
              field_types = collections.OrderedDict()
              for field_name, field_entry in fields.items():
                  try:
                      field_type, field_id = field_entry
                  except TypeError:
                      field_type = field_entry
                      field_id = ffi.cast('legion_field_id_t', AUTO_GENERATE_ID)
                  field_id = c.legion_field_allocator_allocate_field(
                      alloc, field_type.size, field_id)
                  c.legion_field_id_attach_name(
                      _my.ctx.runtime, handle, field_id, field_name.encode('utf-8'), False)
                  field_ids[field_name] = field_id
                  field_types[field_name] = field_type
              c.legion_field_allocator_destroy(alloc)
              owned = True
  
          # Important: Copy handle. Do NOT assume ownership.
          assert handle is not None and field_ids is not None and field_types is not None
          self.handle = ffi.new('legion_field_space_t *', handle)
          self.field_ids = field_ids
          self.field_types = field_types
          self.owned = owned
          self.escaped = False
  
          if owned:
              _my.ctx.track_object(self)
  
      def __del__(self):
          if self.owned and not self.escaped:
              self.destroy()
  
      def __reduce__(self):
          return (_Fspace_unpickle,
                  (self.handle[0].id,
                   self.field_ids,
                   self.field_types,
                   self.owned and self.escaped))
  
      @staticmethod
      def coerce(value):
          if not isinstance(value, Fspace):
              return Fspace(value)
          return value
  
      def destroy(self):
          assert self.owned and not self.escaped
  
          # This is not something you want to have happen in a
          # destructor, since fspaces may outlive the lifetime of the handle.
          c.legion_field_space_destroy(
              _my.ctx.runtime, _my.ctx.context, self.handle[0])
          # Clear out references. Technically unnecessary but avoids abuse.
          del self.handle
          del self.field_ids
          del self.field_types
  
      def raw_value(self):
          return self.handle[0]
  
      def keys(self):
          return self.field_ids.keys()
  
  # Hack: Can't pickle static methods.
  def _Region_unpickle(ispace, fspace, tree_id, owned):
      handle = ffi.new('legion_logical_region_t *')
      handle[0].tree_id = tree_id
      handle[0].index_space = ispace.handle[0]
      handle[0].field_space = fspace.handle[0]
  
      return Region(ispace, fspace, _handle=handle[0], _owned=owned)
  
  class Region(object):
      __slots__ = [
          'handle', 'ispace', 'fspace', 'parent',
          'instances', 'privileges', 'instance_wrappers',
          'owned', 'escaped',
          '__weakref__', # allow weak references
      ]
  
      # Make this speak the Type interface
      numpy_type = None
      cffi_type = 'legion_logical_region_t'
      size = ffi.sizeof(cffi_type)
  
      def __init__(self, ispace, fspace, name=None, **kwargs):
          def parse_kwargs(_handle=None, _parent=None, _owned=False):
              return _handle, _parent, _owned
          handle, parent, owned = parse_kwargs(**kwargs)
  
          if handle is None:
              assert parent is None
              ispace = Ispace.coerce(ispace)
              fspace = Fspace.coerce(fspace)
              handle = c.legion_logical_region_create(
                  _my.ctx.runtime, _my.ctx.context, ispace.raw_value(), fspace.raw_value(), False)
              if name is not None:
                  c.legion_logical_region_attach_name(_my.ctx.runtime, handle, name.encode('utf-8'), False)
              owned = True
  
          # Important: Copy handle. Do NOT assume ownership.
          assert handle is not None
          self.handle = ffi.new('legion_logical_region_t *', handle)
          self.ispace = ispace
          self.fspace = fspace
          self.parent = parent
          self.owned = owned
          self.escaped = False
          self.instances = {}
          self.privileges = {}
          self.instance_wrappers = {}
  
          if owned:
              _my.ctx.track_object(self)
              for field_name in fspace.field_ids.keys():
                  self._set_privilege(field_name, RW)
  
      def __del__(self):
          if self.owned and not self.escaped:
              self.destroy()
  
      def __reduce__(self):
          return (_Region_unpickle,
                  (self.ispace,
                   self.fspace,
                   self.handle[0].tree_id,
                   self.owned and self.escaped))
  
      def destroy(self):
          assert self.owned and not self.escaped
  
          # This is not something you want to have happen in a
          # destructor, since regions may outlive the lifetime of the handle.
          c.legion_logical_region_destroy(
              _my.ctx.runtime, _my.ctx.context, self.handle[0])
          # Clear out references. Technically unnecessary but avoids abuse.
          del self.parent
          del self.instance_wrappers
          del self.instances
          del self.handle
          del self.ispace
          del self.fspace
  
      def raw_value(self):
          return self.handle[0]
  
      def keys(self):
          return self.fspace.keys()
  
      def values(self):
          for key in self.keys():
              if key in self.privileges and self.privileges[key] is not None:
                  yield getattr(self, key)
  
      def items(self):
          for key in self.keys():
              if key in self.privileges and self.privileges[key] is not None:
                  yield key, getattr(self, key)
  
      def _set_privilege(self, field_name, privilege):
          assert self.parent is None # not supported on subregions
          assert field_name not in self.privileges
          self.privileges[field_name] = privilege
  
      def _set_instance(self, field_name, instance, privilege=None):
          assert self.parent is None # not supported on subregions
          assert field_name not in self.instances
          self.instances[field_name] = instance
          if privilege is not None:
              self._set_privilege(field_name, privilege)
  
      def _map_inline(self):
          assert self.parent is None # FIXME: support inline mapping subregions
  
          fields_by_privilege = collections.defaultdict(set)
          for field_name, privilege in self.privileges.items():
              fields_by_privilege[privilege].add(field_name)
          for privilege, field_names  in fields_by_privilege.items():
              launcher = c.legion_inline_launcher_create_logical_region(
                  self.handle[0],
                  privilege._legion_privilege(), 0, # EXCLUSIVE
                  self.handle[0],
                  0, False, 0, 0)
              for field_name in field_names:
                  c.legion_inline_launcher_add_field(
                      launcher, self.fspace.field_ids[field_name], True)
              instance = c.legion_inline_launcher_execute(
                  _my.ctx.runtime, _my.ctx.context, launcher)
              for field_name in field_names:
                  self._set_instance(field_name, instance)
  
      def __getattr__(self, field_name):
          if field_name in self.fspace.field_ids:
              if field_name not in self.instances:
                  if self.privileges[field_name] is None:
                      raise Exception('Invalid attempt to access field "%s" without privileges' % field_name)
                  self._map_inline()
              if field_name not in self.instance_wrappers:
                  self.instance_wrappers[field_name] = RegionField(
                      self, field_name)
              return self.instance_wrappers[field_name]
          else:
              raise AttributeError()
  
  class RegionField(numpy.ndarray):
      # NumPy requires us to implement __new__ for subclasses of ndarray:
      # https://docs.scipy.org/doc/numpy/user/basics.subclassing.html
      def __new__(cls, region, field_name):
          accessor = RegionField._get_accessor(region, field_name)
          initializer = RegionField._get_array_initializer(region, field_name, accessor)
          if initializer is None:
              obj = numpy.empty(tuple(0 for i in xrange(region.ispace.dim))).view(
                  dtype=region.fspace.field_types[field_name].numpy_type,
                  type=cls)
          else:
              obj = numpy.asarray(initializer).view(
                  dtype=region.fspace.field_types[field_name].numpy_type,
                  type=cls)
  
          obj.accessor = accessor
          return obj
  
      @staticmethod
      def _get_accessor(region, field_name):
          # Note: the accessor needs to be kept alive, to make sure to
          # save the result of this function in an instance variable.
          instance = region.instances[field_name]
          dim = region.ispace.dim
          get_accessor = getattr(c, 'legion_physical_region_get_field_accessor_array_{}d'.format(dim))
          return get_accessor(instance, region.fspace.field_ids[field_name])
  
      @staticmethod
      def _get_base_and_stride(region, field_name, accessor):
          domain = region.ispace.domain
          dim = domain.dim
          if domain.volume < 1:
              return None, None, None
  
          rect = getattr(c, 'legion_domain_get_rect_{}d'.format(dim))(domain.raw_value())
          subrect = ffi.new('legion_rect_{}d_t *'.format(dim))
          offsets = ffi.new('legion_byte_offset_t[]', dim)
  
          base_ptr = getattr(c, 'legion_accessor_array_{}d_raw_rect_ptr'.format(dim))(
              accessor, rect, subrect, offsets)
          assert base_ptr
          for i in xrange(dim):
              assert subrect[0].lo.x[i] == rect.lo.x[i]
              assert subrect[0].hi.x[i] == rect.hi.x[i]
          assert offsets[0].offset == region.fspace.field_types[field_name].size
  
          shape = tuple(rect.hi.x[i] - rect.lo.x[i] + 1 for i in xrange(dim))
          strides = tuple(offsets[i].offset for i in xrange(dim))
  
          return base_ptr, shape, strides
  
      @staticmethod
      def _get_array_initializer(region, field_name, accessor):
          base_ptr, shape, strides = RegionField._get_base_and_stride(
              region, field_name, accessor)
          if base_ptr is None:
              return None
  
          field_type = region.fspace.field_types[field_name]
  
          # Numpy doesn't know about CFFI pointers, so we have to cast
          # this to a Python long before we can hand it off to Numpy.
          base_ptr = long(ffi.cast("size_t", base_ptr))
  
          return _RegionNdarray(shape, field_type, base_ptr, strides, False)
  
  # This is a dummy object that is only used as an initializer for the
  # RegionField object above. It is thrown away as soon as the
  # RegionField is constructed.
  class _RegionNdarray(object):
      __slots__ = ['__array_interface__']
      def __init__(self, shape, field_type, base_ptr, strides, read_only):
          # See: https://docs.scipy.org/doc/numpy/reference/arrays.interface.html
          self.__array_interface__ = {
              'version': 3,
              'shape': shape,
              'typestr': numpy.dtype(field_type.numpy_type).str,
              'data': (base_ptr, read_only),
              'strides': strides,
          }
  
  def fill(region, field_names, value):
      assert(isinstance(region, Region))
      if isinstance(field_names, basestring):
          field_names = [field_names]
  
      for field_name in field_names:
          field_id = region.fspace.field_ids[field_name]
          field_type = region.fspace.field_types[field_name]
          raw_value = ffi.new('{} *'.format(field_type.cffi_type), value)
          c.legion_runtime_fill_field(
              _my.ctx.runtime, _my.ctx.context,
              region.raw_value(), region.parent.raw_value() if region.parent is not None else region.raw_value(),
              field_id, raw_value, field_type.size,
              c.legion_predicate_true())
  
  def copy(src_region, src_field_names, dst_region, dst_field_names, redop=None):
      assert(isinstance(src_region, Region))
      assert(isinstance(dst_region, Region))
  
      if isinstance(src_field_names, basestring):
          src_field_names = [src_field_names]
      if isinstance(dst_field_names, basestring):
          dst_field_names = [dst_field_names]
  
      launcher = c.legion_copy_launcher_create(c.legion_predicate_true(), 0, 0)
  
      if redop is None:
          src_groups = [src_field_names]
          dst_groups = [dst_field_names]
          add_dst_requirement = c.legion_copy_launcher_add_dst_region_requirement_logical_region
      else:
          src_groups = zip(src_field_names)
          dst_groups = zip(dst_field_names)
          add_dst_requirement = c.legion_copy_launcher_add_dst_region_requirement_logical_region_reduction
  
      for idx, group in enumerate(src_groups):
          c.legion_copy_launcher_add_src_region_requirement_logical_region(
              launcher,
              src_region.raw_value(),
              R._legion_privilege(), 0, # EXCLUSIVE
              src_region.parent.raw_value() if src_region.parent is not None else src_region.raw_value(),
              0, False)
          for src_field_name in group:
              src_field_id = src_region.fspace.field_ids[src_field_name]
              c.legion_copy_launcher_add_src_field(launcher, idx, src_field_id, True)
  
      for idx, group in enumerate(dst_groups):
          if redop is None:
              dst_privilege = RW._legion_privilege()
          else:
              dst_field_type = dst_region.fspace.field_types[group[0]]
              dst_privilege = Reduce(redop, [group[0]])._legion_redop_id(dst_field_type)
          add_dst_requirement(
              launcher,
              dst_region.raw_value(),
              dst_privilege, 0, # EXCLUSIVE
              dst_region.parent.raw_value() if dst_region.parent is not None else dst_region.raw_value(),
              0, False)
          for dst_field_name in group:
              dst_field_id = dst_region.fspace.field_ids[dst_field_name]
              c.legion_copy_launcher_add_dst_field(launcher, idx, dst_field_id, True)
  
      c.legion_copy_launcher_execute(_my.ctx.runtime, _my.ctx.context, launcher)
  
      c.legion_copy_launcher_destroy(launcher)
  
  # Hack: Can't pickle static methods.
  def _Ipartition_unpickle(tid, id, type_tag, parent, color_space):
      handle = ffi.new('legion_index_partition_t *')
      handle[0].tid = tid
      handle[0].id = id
      handle[0].type_tag = type_tag
  
      return Ipartition(handle[0], parent, color_space)
  
  class Ipartition(object):
      __slots__ = ['handle', 'parent', 'color_space']
  
      # Make this speak the Type interface
      numpy_type = None
      cffi_type = 'legion_index_partition_t'
      size = ffi.sizeof(cffi_type)
  
      def __init__(self, handle, parent, color_space):
          # Important: Copy handle. Do NOT assume ownership.
          self.handle = ffi.new('legion_index_partition_t *', handle)
          self.parent = parent
          self.color_space = color_space
  
      def __reduce__(self):
          return (_Ipartition_unpickle,
                  (self.handle[0].tid, self.handle[0].id, self.handle[0].type_tag, self.parent, self.color_space))
  
      def __getitem__(self, point):
          if isinstance(point, SymbolicExpr):
              return SymbolicIndexAccess(self, point)
          point = DomainPoint.coerce(point)
          subspace = c.legion_index_partition_get_index_subspace_domain_point(
              _my.ctx.runtime, self.handle[0], point.raw_value())
          return Ispace(None, _handle=subspace)
  
      def __iter__(self):
          for point in self.color_space:
              yield self[point]
  
      @staticmethod
      def equal(ispace, color_space, granularity=1, color=AUTO_GENERATE_ID):
          assert isinstance(ispace, Ispace)
          color_space = Ispace.coerce(color_space)
          handle = c.legion_index_partition_create_equal(
              _my.ctx.runtime, _my.ctx.context,
              ispace.raw_value(), color_space.raw_value(), granularity, color)
          return Ipartition(handle, ispace, color_space)
  
      @staticmethod
      def by_field(region, field, color_space, color=AUTO_GENERATE_ID):
          assert isinstance(region, Region)
          color_space = Ispace.coerce(color_space)
          handle = c.legion_index_partition_create_by_field(
              _my.ctx.runtime, _my.ctx.context,
              region.raw_value(),
              region.parent.raw_value() if region.parent is not None else region.raw_value(),
              region.fspace.field_ids[field],
              color_space.raw_value(), color)
          return Ipartition(handle, region.ispace, color_space)
  
      @staticmethod
      def image(ispace, projection, field, color_space,
                          part_kind=compute, color=AUTO_GENERATE_ID):
          assert isinstance(ispace, Ispace)
          assert isinstance(projection, Partition)
          assert isinstance(part_kind, Disjointness)
          color_space = Ispace.coerce(color_space)
          parent = projection.parent
          if is_rect_type(parent.fspace.field_types[field]):
              create_by_image = c.legion_index_partition_create_by_image_range
          else:
              create_by_image = c.legion_index_partition_create_by_image
          handle = create_by_image(
              _my.ctx.runtime, _my.ctx.context,
              ispace.raw_value(), projection.raw_value(),
              parent.parent.raw_value() if parent.parent is not None else parent.raw_value(),
              parent.fspace.field_ids[field],
              color_space.raw_value(), part_kind.value, color)
          return Ipartition(handle, parent.ispace, color_space)
  
      @staticmethod
      def preimage(projection, region, field, color_space,
                             part_kind=compute, color=AUTO_GENERATE_ID):
          assert isinstance(projection, Ipartition)
          assert isinstance(region, Region)
          assert isinstance(part_kind, Disjointness)
          color_space = Ispace.coerce(color_space)
          if is_rect_type(region.fspace.field_types[field]):
              create_by_preimage = c.legion_index_partition_create_by_preimage_range
          else:
              create_by_preimage = c.legion_index_partition_create_by_preimage
          handle = create_by_preimage(
              _my.ctx.runtime, _my.ctx.context,
              projection.raw_value(), region.raw_value(),
              region.parent.raw_value() if region.parent is not None else region.raw_value(),
              region.fspace.field_ids[field],
              color_space.raw_value(), part_kind.value, color)
          return Ipartition(handle, region.ispace, color_space)
  
      @staticmethod
      def restrict(ispace, color_space, transform, extent,
                                part_kind=compute, color=AUTO_GENERATE_ID):
          assert isinstance(ispace, Ispace)
          assert isinstance(part_kind, Disjointness)
          color_space = Ispace.coerce(color_space)
          transform = DomainTransform.coerce(transform)
          extent = Domain.coerce(extent)
          handle = c.legion_index_partition_create_by_restriction(
              _my.ctx.runtime, _my.ctx.context,
              ispace.raw_value(), color_space.raw_value(), transform.raw_value(), extent.raw_value(), part_kind.value, color)
          return Ipartition(handle, ispace, color_space)
  
      @staticmethod
      def pending(ispace, color_space,
                         part_kind=compute, color=AUTO_GENERATE_ID):
          assert isinstance(ispace, Ispace)
          assert isinstance(part_kind, Disjointness)
          color_space = Ispace.coerce(color_space)
          handle = c.legion_index_partition_create_pending_partition(
              _my.ctx.runtime, _my.ctx.context,
              ispace.raw_value(), color_space.raw_value(), part_kind.value, color)
          return Ipartition(handle, ispace, color_space)
  
      # The following methods are for pending partitions only:
      def union(self, color, ispaces):
          color = DomainPoint.coerce(color)
  
          handles = ffi.new('legion_index_space_t[]', [ispace.raw_value() for ispace in ispaces])
          c.legion_index_partition_create_index_space_union_spaces(
              _my.ctx.runtime, _my.ctx.context,
              self.handle[0], color.raw_value(), handles, len(ispaces))
  
      def destroy(self):
          # This is not something you want to have happen in a
          # destructor, since partitions may outlive the lifetime of the handle.
          c.legion_index_partition_destroy(
              _my.ctx.runtime, _my.ctx.context, self.handle[0])
          # Clear out references. Technically unnecessary but avoids abuse.
          del self.handle
          del self.parent
          del self.color_space
  
      def raw_value(self):
          return self.handle[0]
  
  # Hack: Can't pickle static methods.
  def _Partition_unpickle(parent, ipartition):
      handle = ffi.new('legion_logical_partition_t *')
      handle[0].tree_id = parent.raw_value().tree_id
      handle[0].index_partition = ipartition.raw_value()
      handle[0].field_space = parent.fspace.raw_value()
  
      return Partition(parent, ipartition, _handle=handle[0])
  
  class Partition(object):
      __slots__ = ['handle', 'parent', 'ipartition']
  
      # Make this speak the Type interface
      numpy_type = None
      cffi_type = 'legion_logical_partition_t'
      size = ffi.sizeof(cffi_type)
  
      def __init__(self, parent, ipartition, **kwargs):
          def parse_kwargs(_handle=None):
              return _handle
          handle = parse_kwargs(**kwargs)
  
          if handle is None:
              assert isinstance(parent, Region)
              assert isinstance(ipartition, Ipartition)
              handle = c.legion_logical_partition_create(
                  _my.ctx.runtime, _my.ctx.context, parent.raw_value(), ipartition.raw_value())
  
          # Important: Copy handle. Do NOT assume ownership.
          assert handle is not None
          self.handle = ffi.new('legion_logical_partition_t *', handle)
          self.parent = parent
          self.ipartition = ipartition
  
      def __reduce__(self):
          return (_Partition_unpickle,
                  (self.parent,
                   self.ipartition))
  
      def __getitem__(self, point):
          if isinstance(point, SymbolicExpr):
              return SymbolicIndexAccess(self, point)
          point = DomainPoint.coerce(point)
          subspace = self.ipartition[point]
          subregion = c.legion_logical_partition_get_logical_subregion_by_color_domain_point(
              _my.ctx.runtime, self.handle[0], point.raw_value())
          return Region(subspace, self.parent.fspace, _handle=subregion,
                        _parent=self.parent.parent if self.parent.parent is not None else self.parent)
  
      def __iter__(self):
          for point in self.color_space:
              yield self[point]
  
      @property
      def color_space(self):
          return self.ipartition.color_space
  
      @staticmethod
      def equal(region, color_space, granularity=1, color=AUTO_GENERATE_ID):
          assert isinstance(region, Region)
          ipartition = Ipartition.equal(region.ispace, color_space, granularity, color)
          return Partition(region, ipartition)
  
      @staticmethod
      def by_field(region, field, color_space, color=AUTO_GENERATE_ID):
          assert isinstance(region, Region)
          ipartition = Ipartition.by_field(
              region, field, color_space, color)
          return Partition(region, ipartition)
  
      @staticmethod
      def image(region, projection, field, color_space,
                          part_kind=compute, color=AUTO_GENERATE_ID):
          assert isinstance(region, Region)
          ipartition = Ipartition.image(
              region.ispace, projection, field, color_space, part_kind, color)
          return Partition(region, ipartition)
  
      @staticmethod
      def preimage(projection, region, field, color_space,
                             part_kind=compute, color=AUTO_GENERATE_ID):
          assert isinstance(projection, Partition)
          ipartition = Ipartition.preimage(
              projection.ipartition, region, field, color_space, part_kind, color)
          return Partition(region, ipartition)
  
      @staticmethod
      def restrict(region, color_space, transform, extent,
                                part_kind=compute, color=AUTO_GENERATE_ID):
          assert isinstance(region, Region)
          ipartition = Ipartition.restrict(
              region.ispace, color_space, transform, extent, part_kind, color)
          return Partition(region, ipartition)
  
      @staticmethod
      def pending(region, color_space, part_kind=compute, color=AUTO_GENERATE_ID):
          assert isinstance(region, Region)
          ipartition = Ipartition.pending(
              region.ispace, color_space, part_kind, color)
          return Partition(region, ipartition)
  
      def union(self, color, regions):
          ispaces = [region.ispace for region in regions]
          self.ipartition.union(color, ispaces)
  
      def destroy(self):
          # This is not something you want to have happen in a
          # destructor, since partitions may outlive the lifetime of the handle.
          c.legion_logical_partition_destroy(
              _my.ctx.runtime, _my.ctx.context, self.handle[0])
          # Clear out references. Technically unnecessary but avoids abuse.
          del self.handle
          del self.parent
          del self.ipartition
  
      def raw_value(self):
          return self.handle[0]
  
  def define_regent_argument_struct(task_id, argument_types, privileges, return_type, arguments):
      if argument_types is None:
          raise Exception('Arguments must be typed in extern Regent tasks')
  
      struct_name = 'task_args_%s' % task_id
  
      n_fields = int(math.ceil(len(argument_types)/64.))
  
      fields = ['uint64_t %s[%s];' % ('__map', n_fields)]
      for i, arg_type in enumerate(argument_types):
          arg_name = '__arg_%s' % i
          fields.append('%s %s;' % (arg_type.cffi_type, arg_name))
      for i, arg in enumerate(arguments):
          if isinstance(arg, Region):
              fields.append('legion_field_id_t __arg_%s_fields[%s];' % (i, len(arg.fspace.field_types)))
  
      struct = 'typedef struct %s { %s } %s;' % (struct_name, ' '.join(fields), struct_name)
      ffi.cdef(struct)
  
      return struct_name
  
  class ExternTask(object):
      __slots__ = ['argument_types', 'privileges', 'return_type',
                   'calling_convention', 'task_id', '_argument_struct']
  
      def __init__(self, task_id, argument_types=None, privileges=None,
                   return_type=void, calling_convention=None):
          self.argument_types = argument_types
          self.privileges = privileges
          self.return_type = return_type
          self.calling_convention = calling_convention
          assert isinstance(task_id, int)
          self.task_id = task_id
          self._argument_struct = None
  
      def argument_struct(self, args):
          if self.calling_convention == 'regent' and self._argument_struct is None:
              self._argument_struct = define_regent_argument_struct(
                  self.task_id, self.argument_types, self.privileges, self.return_type, args)
          return self._argument_struct
  
      def __call__(self, *args):
          return self.spawn_task(*args)
  
      def spawn_task(self, *args, **kwargs):
          if _my.ctx.current_launch:
              return _my.ctx.current_launch.spawn_task(self, *args, **kwargs)
          return TaskLaunch().spawn_task(self, *args, **kwargs)
  
  def extern_task(**kwargs):
      return ExternTask(**kwargs)
  
  class ExternTaskWrapper(object):
      # Note: Can't use __slots__ for this class because __qualname__
      # conflicts with the class variable.
      def __init__(self, thunk, name):
          self.thunk = thunk
          self.__name__ = name
          self.__qualname__ = name
      def __call__(self, *args, **kwargs):
          f = self.thunk(*args, **kwargs)
          if f.value_type != void:
              return f.get()
  
  _next_wrapper_id = 1000
  def extern_task_wrapper(privileges=None, return_type=void, **kwargs):
      global _next_wrapper_id
      extern = extern_task(privileges=privileges, return_type=return_type, **kwargs)
      wrapper_name = str('wrapper_task_%s' % _next_wrapper_id)
      _next_wrapper_id += 1
      wrapper = ExternTaskWrapper(extern, wrapper_name)
      task_wrapper = task(wrapper, privileges=privileges, return_type=return_type, inner=True)
      setattr(sys.modules[__name__], wrapper_name, task_wrapper)
      return task_wrapper
  
  def get_qualname(fn):
      # Python >= 3.3 only
      try:
          return fn.__qualname__.split('.')
      except AttributeError:
          pass
  
      # Python < 3.3
      try:
          import qualname
          return qualname.qualname(fn).split('.')
      except ImportError:
          pass
  
      # Hack: Issue error if we're wrapping a class method and failed to
      # get the qualname
      import inspect
      context = [x[0].f_code.co_name for x in inspect.stack()
                 if '__module__' in x[0].f_code.co_names and
                 inspect.getmodule(x[0].f_code).__name__ != __name__]
      if len(context) > 0:
          raise Exception('To use a task defined in a class, please upgrade to Python >= 3.3 or install qualname (e.g. pip install qualname)')
  
      return [fn.__name__]
  
  def _postprocess(arg, point):
      if hasattr(arg, '_legion_postprocess_task_argument'):
          return arg._legion_postprocess_task_argument(point)
      return arg
  
  class Task (object):
      __slots__ = ['body', 'privileges', 'return_type',
                   'leaf', 'inner', 'idempotent', 'replicable',
                   'calling_convention', 'argument_struct',
                   'task_id', 'registered']
  
      def __init__(self, body, privileges=None, return_type=None,
                   leaf=False, inner=False, idempotent=False, replicable=False,
                   register=True, task_id=None, top_level=False):
          self.body = body
          self.privileges = privileges
          self.return_type = return_type
          self.leaf = bool(leaf)
          self.inner = bool(inner)
          self.idempotent = bool(idempotent)
          self.replicable = bool(replicable)
          self.calling_convention = 'python'
          self.argument_struct = None
          self.task_id = None
          if register:
              self.register(task_id, top_level)
  
      def __call__(self, *args, **kwargs):
          # Hack: This entrypoint needs to be able to handle both being
          # called in user code (to launch a task) and as the task
          # wrapper when the task itself executes. Unfortunately isn't a
          # good way to disentangle these. Detect if we're in the task
          # wrapper case by checking the number and types of arguments.
          if len(args) == 3 and \
             isinstance(args[0], bytearray) and \
             isinstance(args[1], bytearray) and \
             isinstance(args[2], long):
              return self.execute_task(*args, **kwargs)
          else:
              return self.spawn_task(*args, **kwargs)
  
      def spawn_task(self, *args, **kwargs):
          if _my.ctx.current_launch:
              return _my.ctx.current_launch.spawn_task(self, *args, **kwargs)
          return TaskLaunch().spawn_task(self, *args, **kwargs)
  
      def execute_task(self, raw_args, user_data, proc):
          raw_arg_ptr = ffi.new('char[]', bytes(raw_args))
          raw_arg_size = len(raw_args)
  
          # Execute preamble to obtain Legion API context.
          task = ffi.new('legion_task_t *')
          raw_regions = ffi.new('legion_physical_region_t **')
          num_regions = ffi.new('unsigned *')
          context = ffi.new('legion_context_t *')
          runtime = ffi.new('legion_runtime_t *')
          c.legion_task_preamble(
              raw_arg_ptr, raw_arg_size, proc,
              task, raw_regions, num_regions, context, runtime)
  
          # Decode arguments from Pickle format.
          arg_ptr = ffi.cast('char *', c.legion_task_get_args(task[0]))
          arg_size = c.legion_task_get_arglen(task[0])
          if c.legion_task_get_is_index_space(task[0]) and arg_size == 0:
              arg_ptr = ffi.cast('char *', c.legion_task_get_local_args(task[0]))
              arg_size = c.legion_task_get_local_arglen(task[0])
  
          if arg_size > 0 and c.legion_task_get_depth(task[0]) > 0:
              args = pickle.loads(ffi.unpack(arg_ptr, arg_size))
          else:
              args = ()
  
          # Unpack regions.
          regions = []
          for i in xrange(num_regions[0]):
              regions.append(raw_regions[0][i])
  
          # Build context.
          ctx = Context(context, runtime, task, regions)
  
          # Ensure that we're not getting tangled up in another
          # thread. There should be exactly one thread per task.
          try:
              _my.ctx
          except AttributeError:
              pass
          else:
              raise Exception('thread-local context already set')
  
          # Store context in thread-local storage.
          _my.ctx = ctx
  
          # Postprocess arguments.
          point = DomainPoint(None, _handle=c.legion_task_get_index_point(task[0]))
          args = tuple(_postprocess(arg, point) for arg in args)
  
          # Unpack physical regions.
          if self.privileges is not None:
              req = 0
              for i, arg in zip(range(len(args)), args):
                  if isinstance(arg, Region):
                      assert i < len(self.privileges)
                      groups = self.privileges[i]._legion_grouped_privileges(arg.fspace)
                      for priv, _, _, fields in groups:
                          assert req < num_regions[0]
                          instance = raw_regions[0][req]
                          req += 1
  
                          for field in fields:
                              arg._set_instance(field, instance, priv)
              assert req == num_regions[0]
  
          # Execute task body.
          result = self.body(*args)
  
          # Mark any remaining objects as escaped.
          for ref in ctx.owned_objects:
              obj = ref()
              if obj is not None:
                  obj.escaped = True
  
          # Encode result.
          if not self.return_type:
              result_str = pickle.dumps(result, protocol=_pickle_version)
              result_size = len(result_str)
              result_ptr = ffi.new('char[]', result_size)
              ffi.buffer(result_ptr, result_size)[:] = result_str
          else:
              if self.return_type.size > 0:
                  result_ptr = ffi.new(ffi.getctype(self.return_type.cffi_type, '*'), result)
              else:
                  result_ptr = ffi.NULL
              result_size = self.return_type.size
  
          # Execute postamble.
          c.legion_task_postamble(runtime[0], context[0], result_ptr, result_size)
  
          # Clear thread-local storage.
          del _my.ctx
  
      def register(self, task_id, top_level_task):
          assert(self.task_id is None)
  
          if not task_id:
              if not top_level_task:
                  global next_legion_task_id
                  task_id = next_legion_task_id
                  next_legion_task_id += 1
                  # If we ever hit this then we need to allocate more task IDs
                  assert task_id < max_legion_task_id
              else:
                  task_id = 1 # Predefined value for the top-level task
  
          execution_constraints = c.legion_execution_constraint_set_create()
          c.legion_execution_constraint_set_add_processor_constraint(
              execution_constraints, c.PY_PROC)
  
          layout_constraints = c.legion_task_layout_constraint_set_create()
          # FIXME: Add layout constraints
  
          options = ffi.new('legion_task_config_options_t *')
          options[0].leaf = self.leaf
          options[0].inner = self.inner
          options[0].idempotent = self.idempotent
          options[0].replicable = self.replicable
  
          qualname = get_qualname(self.body)
          task_name = ('%s.%s' % (self.body.__module__, '.'.join(qualname)))
  
          c_qualname_comps = [ffi.new('char []', comp.encode('utf-8')) for comp in qualname]
          c_qualname = ffi.new('char *[]', c_qualname_comps)
  
          global global_task_registration_barrier
          if global_task_registration_barrier is not None:
              c.legion_phase_barrier_arrive(_my.ctx.runtime, _my.ctx.context, global_task_registration_barrier, 1)
              global_task_registration_barrier = c.legion_phase_barrier_advance(_my.ctx.runtime, _my.ctx.context, global_task_registration_barrier)
              c.legion_runtime_enable_scheduler_lock()
              c.legion_phase_barrier_wait(_my.ctx.runtime, _my.ctx.context, global_task_registration_barrier)
              # Need to hold this through the end of registration.
              # c.legion_runtime_disable_scheduler_lock()
  
          c.legion_runtime_register_task_variant_python_source_qualname(
              c.legion_runtime_get_runtime(),
              task_id,
              task_name.encode('utf-8'),
              self.replicable or not is_script, # Global
              execution_constraints,
              layout_constraints,
              options[0],
              self.body.__module__.encode('utf-8'),
              c_qualname,
              len(qualname),
              ffi.NULL,
              0)
  
          if global_task_registration_barrier is not None:
              c.legion_phase_barrier_arrive(_my.ctx.runtime, _my.ctx.context, global_task_registration_barrier, 1)
              global_task_registration_barrier = c.legion_phase_barrier_advance(_my.ctx.runtime, _my.ctx.context, global_task_registration_barrier)
              # c.legion_runtime_enable_scheduler_lock()
              c.legion_phase_barrier_wait(_my.ctx.runtime, _my.ctx.context, global_task_registration_barrier)
              c.legion_runtime_disable_scheduler_lock()
  
          c.legion_execution_constraint_set_destroy(execution_constraints)
          c.legion_task_layout_constraint_set_destroy(layout_constraints)
  
          self.task_id = task_id
          return self
  
  def task(body=None, **kwargs):
      if body is None:
          return lambda body: task(body, **kwargs)
      return Task(body, **kwargs)
  
  class _TaskLauncher(object):
      __slots__ = ['task']
  
      def __init__(self, task):
          self.task = task
  
      def preprocess_args(self, args):
          return [
              arg._legion_preprocess_task_argument()
              if hasattr(arg, '_legion_preprocess_task_argument') else arg
              for arg in args]
  
      def gather_futures(self, args):
          normal = []
          futures = []
          for arg in args:
              if isinstance(arg, Future):
                  arg = Future(arg, argument_number=len(futures))
                  futures.append(arg)
              normal.append(arg)
          return normal, futures
  
      def encode_args(self, args):
          task_args = ffi.new('legion_task_argument_t *')
          task_args_buffer = None
          if self.task.calling_convention == 'python':
              arg_str = pickle.dumps(args, protocol=_pickle_version)
              task_args_buffer = ffi.new('char[]', arg_str)
              task_args[0].args = task_args_buffer
              task_args[0].arglen = len(arg_str)
          elif self.task.calling_convention == 'regent':
              arg_struct = self.task.argument_struct(args)
              task_args_buffer = ffi.new('%s*' % arg_struct)
              # Note: ffi.new returns zeroed memory
              for i, arg in enumerate(args):
                  if isinstance(arg, Future):
                      getattr(task_args_buffer, '__map')[i // 64] |= 1 << (i % 64)
              for i, arg in enumerate(args):
                  if not isinstance(arg, Future):
                      arg_name = '__arg_%s' % i
                      arg_value = arg
                      if hasattr(arg, 'handle') and not isinstance(arg, DomainPoint):
                          arg_value = arg.handle[0]
                      setattr(task_args_buffer, arg_name, arg_value)
              for i, arg in enumerate(args):
                  if isinstance(arg, Region):
                      arg_name = '__arg_%s_fields' % i
                      arg_slot = getattr(task_args_buffer, arg_name)
                      for j, field_id in enumerate(arg.fspace.field_ids.values()):
                          arg_slot[j] = field_id
              task_args[0].args = task_args_buffer
              task_args[0].arglen = ffi.sizeof(arg_struct)
          else:
              # FIXME: External tasks need a dedicated calling
              # convention to permit the passing of task arguments.
              task_args[0].args = ffi.NULL
              task_args[0].arglen = 0
          # WARNING: Need to return the interior buffer or else it will be GC'd
          return task_args, task_args_buffer
  
      def attach_region_requirements(self, launcher, args, is_index_launch):
          if is_index_launch:
              def add_region_normal(launcher, handle, *args):
                  return c.legion_index_launcher_add_region_requirement_logical_region(
                      launcher, handle, 0, # projection
                      *args)
              def add_region_reduction(launcher, handle, *args):
                  return c.legion_index_launcher_add_region_requirement_logical_region_reduction(
                      launcher, handle, 0, # projection
                      *args)
              add_partition_normal = c.legion_index_launcher_add_region_requirement_logical_partition
              add_partition_reduction = c.legion_index_launcher_add_region_requirement_logical_partition_reduction
              add_field = c.legion_index_launcher_add_field
          else:
              add_region_normal = c.legion_task_launcher_add_region_requirement_logical_region
              add_region_reduction = c.legion_task_launcher_add_region_requirement_logical_region_reduction
              add_field = c.legion_task_launcher_add_field
  
          for i, arg in zip(range(len(args)), args):
              if isinstance(arg, Region) or (isinstance(arg, SymbolicExpr) and arg.is_region()):
                  if self.task.privileges is None or i >= len(self.task.privileges):
                      raise Exception('Privileges are required on all Region arguments')
                  groups = self.task.privileges[i]._legion_grouped_privileges(arg.fspace)
                  if isinstance(arg, Region):
                      parent = arg.parent if arg.parent is not None else arg
                      for _, priv, redop, fields in groups:
                          if redop is None:
                              req = add_region_normal(
                                  launcher, arg.raw_value(),
                                  priv, 0, # EXCLUSIVE
                                  parent.raw_value(), 0, False)
                          else:
                              req = add_region_reduction(
                                  launcher, arg.raw_value(),
                                  redop, 0, # EXCLUSIVE
                                  parent.raw_value(), 0, False)
                          for field in fields:
                              add_field(
                                  launcher, req, arg.fspace.field_ids[field], True)
                  elif isinstance(arg, SymbolicExpr):
                      # FIXME: Support non-trivial projection functors
                      assert isinstance(arg, SymbolicIndexAccess) and (isinstance(arg.index, SymbolicLoopIndex) or isinstance(arg.index, ConcreteLoopIndex))
                      proj_id = 0
  
                      parent = arg.parent if arg.parent is not None else arg
                      parent = parent.parent if parent.parent is not None else parent
                      for _, priv, redop, fields in groups:
                          if redop is None:
                              req = add_partition_normal(
                                  launcher, arg.raw_value(), proj_id,
                                  priv, 0, # EXCLUSIVE
                                  parent.raw_value(), 0, False)
                          else:
                              req = add_partition_reduction(
                                  launcher, arg.raw_value(), proj_id,
                                  redop, 0, # EXCLUSIVE
                                  parent.raw_value(), 0, False)
                          for field in fields:
                              add_field(
                                  launcher, req, arg.fspace.field_ids[field], True)
              elif self.task.privileges is not None and i < len(self.task.privileges) and self.task.privileges[i]:
                  raise TypeError('Privileges can only be specified for Region arguments, got %s' % type(arg))
  
      def spawn_task(self, *args, **kwargs):
          # Hack: workaround for Python 2 not having keyword-only arguments
          def validate_spawn_task_args(point=None, mapper=0, tag=0):
              return point, mapper, tag
          point, mapper, tag = validate_spawn_task_args(**kwargs)
  
          assert(isinstance(_my.ctx, Context))
  
          args, futures = self.gather_futures(args)
          args = self.preprocess_args(args)
          task_args, task_args_root = self.encode_args(args)
  
          # Construct the task launcher.
          launcher = c.legion_task_launcher_create(
              self.task.task_id, task_args[0], c.legion_predicate_true(), mapper, tag)
          if point is not None:
              point = DomainPoint.coerce(point)
              c.legion_task_launcher_set_point(launcher, point.raw_value())
          self.attach_region_requirements(launcher, args, False)
          for i, arg in zip(range(len(args)), args):
              if self.task.privileges is not None and i < len(self.task.privileges) and self.task.privileges[i] and not isinstance(arg, Region):
                  raise TypeError('Privileges can only be specified for Region arguments, got %s' % type(arg))
              if isinstance(arg, Region):
                  pass # Already attached above
              elif isinstance(arg, Future):
                  c.legion_task_launcher_add_future(launcher, arg.handle)
              elif self.task.calling_convention is None:
                  # FIXME: Task arguments aren't being encoded AT ALL;
                  # at least throw an exception so that the user knows
                  raise Exception('External tasks do not support non-region arguments')
  
          # Launch the task.
          if _my.ctx.current_launch is not None:
              return _my.ctx.current_launch.attach_task_launcher(
                  launcher, point, root=task_args_root)
  
          result = c.legion_task_launcher_execute(
              _my.ctx.runtime, _my.ctx.context, launcher)
          c.legion_task_launcher_destroy(launcher)
  
          # Build future of result.
          future = Future.from_cdata(result, value_type=self.task.return_type)
          c.legion_future_destroy(result)
          return future
  
  class _IndexLauncher(_TaskLauncher):
      __slots__ = ['task', 'domain', 'mapper', 'tag',
                   'global_args', 'local_args', 'region_args', 'future_args',
                   'reduction_op', 'future_map']
  
      def __init__(self, task, domain, mapper, tag):
          super(_IndexLauncher, self).__init__(task)
          self.domain = domain
          self.mapper = mapper
          self.tag = tag
          self.global_args = None
          self.local_args = c.legion_argument_map_create()
          self.region_args = None
          self.future_args = []
          self.reduction_op = None
          self.future_map = None
  
      def __del__(self):
          c.legion_argument_map_destroy(self.local_args)
  
      def spawn_task(self, *args, **kwargs):
          raise Exception('IndexLaunch does not support spawn_task')
  
      def attach_local_args(self, index, *args):
          task_args, _ = self.encode_args(self.preprocess_args(args))
          c.legion_argument_map_set_point(
              self.local_args, index.value.raw_value(), task_args[0], False)
  
      def attach_global_args(self, *args):
          assert self.global_args is None
          self.global_args = args
  
      def attach_region_args(self, *args):
          self.region_args = args
  
      def attach_future_args(self, *args):
          self.future_args = args
  
      def set_reduction_op(self, op):
          self.reduction_op = op
  
      def launch(self):
          # Encode global args (if any).
          if self.global_args is not None:
              global_args, global_args_root = self.encode_args(self.preprocess_args(self.global_args))
          else:
              global_args = ffi.new('legion_task_argument_t *')
              global_args[0].args = ffi.NULL
              global_args[0].arglen = 0
              global_args_root = None
  
          # Construct the task launcher.
          launcher = c.legion_index_launcher_create(
              self.task.task_id, self.domain.raw_value(),
              global_args[0], self.local_args,
              c.legion_predicate_true(), False, self.mapper, self.tag)
  
          assert (self.global_args is not None) != (self.region_args is not None)
          if self.global_args is not None:
              self.attach_region_requirements(launcher, self.global_args, True)
          if self.region_args is not None:
              self.attach_region_requirements(launcher, self.region_args, True)
  
          for arg in self.future_args:
              c.legion_index_launcher_add_future(launcher, arg.handle)
  
          # Launch the task.
          if _my.ctx.current_launch is not None:
              return _my.ctx.current_launch.attach_index_launcher(
                  launcher, root=global_args_root)
  
          launch = c.legion_index_launcher_execute
          redop = []
          if self.reduction_op is not None:
              assert self.task.return_type is not None
              launch = c.legion_index_launcher_execute_reduction
              redop = [_redop_ids[self.reduction_op][self.task.return_type]]
  
          result = launch(
              _my.ctx.runtime, _my.ctx.context, launcher, *redop)
          c.legion_index_launcher_destroy(launcher)
  
          # Build future (map) of result.
          if self.reduction_op is not None:
              self.future_map = Future.from_cdata(result, value_type=self.task.return_type)
              c.legion_future_destroy(result)
          else:
              self.future_map = FutureMap(result, value_type=self.task.return_type)
              c.legion_future_map_destroy(result)
  
  class _MustEpochLauncher(object):
      __slots__ = ['domain', 'launcher', 'roots', 'has_sublaunchers']
  
      def __init__(self, domain=None):
          self.domain = domain
          self.launcher = c.legion_must_epoch_launcher_create(0, 0)
          if self.domain is not None:
              c.legion_must_epoch_launcher_set_launch_domain(
                  self.launcher, self.domain.raw_value())
          self.roots = []
          self.has_sublaunchers = False
  
      def __del__(self):
          c.legion_must_epoch_launcher_destroy(self.launcher)
  
      def spawn_task(self, *args, **kwargs):
          raise Exception('MustEpochLaunch does not support spawn_task')
  
      def attach_task_launcher(self, task_launcher, point, root=None):
          if point is None:
              raise Exception('MustEpochLauncher requires a point for each task')
          if root is not None:
              self.roots.append(root)
          c.legion_must_epoch_launcher_add_single_task(
              self.launcher, point.raw_value(), task_launcher)
          self.has_sublaunchers = True
  
      def attach_index_launcher(self, index_launcher, root=None):
          if root is not None:
              self.roots.append(root)
          c.legion_must_epoch_launcher_add_index_task(
              self.launcher, index_launcher)
          self.has_sublaunchers = True
  
      def launch(self):
          if not self.has_sublaunchers:
              raise Exception('MustEpochLaunch requires at least one point task to be executed')
          result = c.legion_must_epoch_launcher_execute(
              _my.ctx.runtime, _my.ctx.context, self.launcher)
          c.legion_future_map_destroy(result)
  
  class TaskLaunch(object):
      __slots__ = []
      def spawn_task(self, task, *args, **kwargs):
          launcher = _TaskLauncher(task=task)
          return launcher.spawn_task(*args, **kwargs)
  
  class _FuturePoint(object):
      __slots__ = ['launcher', 'point', 'future']
      def __init__(self, launcher, point):
          self.launcher = launcher
          self.point = point
          self.future = None
      def get(self):
          if self.future is not None:
              return self.future.get()
  
          if self.launcher.future_map is None:
              raise Exception('Cannot retrieve a future from an index launch until the launch is complete')
  
          self.future = self.launcher.future_map[self.point]
  
          # Clear launcher and point
          del self.launcher
          del self.point
  
          return self.future.get()
  
  class SymbolicExpr(object):
      def is_region(self):
          return False
  
  class SymbolicIndexAccess(SymbolicExpr):
      __slots__ = ['value', 'index']
      def __init__(self, value, index):
          self.value = value
          self.index = index
      def __str__(self):
          return '%s[%s]' % (self.value, self.index)
      def __repr__(self):
          return '%s[%s]' % (self.value, self.index)
      def _legion_preprocess_task_argument(self):
          if isinstance(self.index, ConcreteLoopIndex):
              return self.value[self.index._legion_preprocess_task_argument()]
          return self
      def _legion_postprocess_task_argument(self, point):
          result = _postprocess(self.value, point)[_postprocess(self.index, point)]
          # FIXME: Clear parent field of regions being used as projection requirements
          if isinstance(result, Region):
              result.parent = None
          return result
      def is_region(self):
          return isinstance(self.value, Partition)
      @property
      def parent(self):
          if self.is_region():
              return self.value.parent
          assert False
      @property
      def fspace(self):
          if self.is_region():
              return self.value.parent.fspace
          assert False
      def raw_value(self):
          if self.is_region():
              return self.value.raw_value()
          assert False
  
  class SymbolicLoopIndex(SymbolicExpr):
      __slots__ = ['name']
      def __init__(self, name):
          self.name = name
      def __str__(self):
          return self.name
      def __repr__(self):
          return self.name
      def _legion_postprocess_task_argument(self, point):
          return point
  
  ID = SymbolicLoopIndex('ID')
  
  class ConcreteLoopIndex(SymbolicExpr):
      __slots__ = ['value']
      def __init__(self, value):
          self.value = value
      def __int__(self):
          return self.value.__int__()
      def __index__(self):
          return self.value.__index__()
      def __str__(self):
          return str(self.value)
      def __repr__(self):
          return repr(self.value)
      def _legion_preprocess_task_argument(self):
          return self.value
  
  def index_launch(domain, task, *args, **kwargs):
      def parse_kwargs(reduce=None, mapper=0, tag=0):
          return reduce, mapper, tag
      reduce, mapper, tag = parse_kwargs(**kwargs)
  
      if isinstance(domain, Domain):
          domain = domain
      elif isinstance(domain, Ispace):
          domain = domain.domain
      else:
          domain = Domain(domain)
      launcher = _IndexLauncher(task=task, domain=domain, mapper=mapper, tag=tag)
      args, futures = launcher.gather_futures(args)
      launcher.attach_global_args(*args)
      launcher.attach_future_args(*futures)
      launcher.set_reduction_op(reduce)
      launcher.launch()
      return launcher.future_map
  
  class IndexLaunch(object):
      __slots__ = ['domain', 'mapper', 'tag', 'launcher', 'point',
                   'saved_task', 'saved_args']
  
      def __init__(self, domain, **kwargs):
          # Hack: workaround for Python 2 not having keyword-only arguments
          def validate_spawn_task_args(mapper=0, tag=0):
              return mapper, tag
          mapper, tag = validate_spawn_task_args(**kwargs)
  
          if isinstance(domain, Domain):
              self.domain = domain
          elif isinstance(domain, Ispace):
              self.domain = domain.domain
          else:
              self.domain = Domain(domain)
          self.mapper = mapper
          self.tag = tag
          self.launcher = None
          self.point = None
          self.saved_task = None
          self.saved_args = None
  
      def __iter__(self):
          _my.ctx.begin_launch(self)
          self.point = ConcreteLoopIndex(None)
          for i in self.domain:
              self.point.value = i
              yield self.point
          _my.ctx.end_launch(self)
          self.launch()
  
      def ensure_launcher(self, task):
          if self.launcher is None:
              self.launcher = _IndexLauncher(
                  task=task, domain=self.domain, mapper=self.mapper, tag=self.tag)
  
      def check_compatibility(self, task, *args):
          # The tasks in a launch must conform to the following constraints:
          #   * Only one task can be launched.
          #   * The arguments must be compatible:
          #       * At a given argument position, the value must always
          #         be a special value, or always not.
          #       * Special values include: regions and futures.
          #       * If a region, the value must be symbolic (i.e. able
          #         to be analyzed as a function of the index expression).
          #       * If a future, the values must be literally identical
          #         (i.e. each argument slot in the launch can only
          #         accept a single future value.)
  
          if self.saved_task is None:
              self.saved_task = task
          if task != self.saved_task:
              raise Exception('An IndexLaunch may contain only one task launch')
  
          if self.saved_args is None:
              self.saved_args = args
          for arg, saved_arg in zip_longest(args, self.saved_args):
              # TODO: Add support for region arguments
              if isinstance(arg, Region) or isinstance(arg, RegionField):
                  if arg != saved_arg:
                      raise Exception('Region argument to IndexLaunch does not match previous value at this position')
              elif isinstance(arg, Future):
                  if arg != saved_arg:
                      raise Exception('Future argument to IndexLaunch does not match previous value at this position')
  
      def spawn_task(self, task, *args):
          self.ensure_launcher(task)
          self.check_compatibility(task, *args)
          args, futures = self.launcher.gather_futures(args)
          self.launcher.attach_local_args(self.point, *args)
          self.launcher.attach_region_args(*args)
          self.launcher.attach_future_args(*futures)
          return _FuturePoint(self.launcher, self.point.value)
  
      def launch(self):
          self.launcher.launch()
  
  class MustEpochLaunch(object):
      __slots__ = ['domain', 'launcher']
  
      def __init__(self, domain=None):
          if isinstance(domain, Domain):
              self.domain = domain
          elif isinstance(domain, Ispace):
              self.domain = ispace.domain
          elif domain is not None:
              self.domain = Domain(domain)
          else:
              self.domain = None
          self.launcher = None
  
      def __enter__(self):
          self.launcher = _MustEpochLauncher(domain=self.domain)
          _my.ctx.begin_launch(self)
  
      def __exit__(self, exc_type, exc_value, tb):
          _my.ctx.end_launch(self)
          if exc_value is None:
              self.launch()
              del self.launcher
  
      def spawn_task(self, *args, **kwargs):
          # TODO: Support index launches
          TaskLaunch().spawn_task(*args, **kwargs)
  
          # TODO: Support return values
  
      def attach_task_launcher(self, *args, **kwargs):
          self.launcher.attach_task_launcher(*args, **kwargs)
  
      def attach_index_launcher(self, *args, **kwargs):
          self.launcher.attach_index_launcher(*args, **kwargs)
  
      def launch(self):
          self.launcher.launch()
  
  @task(leaf=True)
  def _dummy_task():
      return 1
  
  def execution_fence(block=False, future=False):
      c.legion_runtime_issue_execution_fence(_my.ctx.runtime, _my.ctx.context)
      if block or future:
          f = _dummy_task()
          if block:
              f.get()
          if future:
              return f
  
  def print_once(*args, **kwargs):
      fd = (kwargs['file'] if 'file' in kwargs else sys.stdout).fileno()
      message = StringIO()
      kwargs['file'] = message
      print(*args, **kwargs)
      c.legion_runtime_print_once_fd(_my.ctx.runtime, _my.ctx.context, fd, 'w'.encode('utf-8'), message.getvalue().encode('utf-8'))
  
  class Tunable(object):
      # FIXME: Deduplicate this with DefaultMapper::DefaultTunables
      NODE_COUNT = 0
      LOCAL_CPUS = 1
      LOCAL_GPUS = 2
      LOCAL_IOS = 3
      LOCAL_OMPS = 4
      LOCAL_PYS = 5
      GLOBAL_CPUS = 6
      GLOBAL_GPUS = 7
      GLOBAL_IOS = 8
      GLOBAL_OMPS = 9
      GLOBAL_PYS = 10
  
      @staticmethod
      def select(tunable_id):
          result = c.legion_runtime_select_tunable_value(
              _my.ctx.runtime, _my.ctx.context, tunable_id, 0, 0)
          future = Future.from_cdata(result, value_type=uint64)
          c.legion_future_destroy(result)
          return future
  
  class Trace(object):
      __slots__ = ['trace_id']
  
      def __init__(self):
          self.trace_id = _my.ctx.next_trace_id
          _my.ctx.next_trace_id += 1
  
      def __enter__(self):
          c.legion_runtime_begin_trace(_my.ctx.runtime, _my.ctx.context, self.trace_id, True)
  
      def __exit__(self, exc_type, exc_value, tb):
          c.legion_runtime_end_trace(_my.ctx.runtime, _my.ctx.context, self.trace_id)
  
  if is_script:
      # We can't use runpy for this since runpy is aggressive about
      # cleaning up after itself and removes the module before execution
      # has completed.
      def run_path(filename, run_name=None):
          import imp
          module = imp.new_module(run_name)
          setattr(module, '__name__', run_name)
          setattr(module, '__file__', filename)
          setattr(module, '__loader__', None)
          setattr(module, '__package__', run_name.rpartition('.')[0])
  
          # Hide the current module if it exists.
          old_module = sys.modules[run_name] if run_name in sys.modules else None
          sys.modules[run_name] = module
  
          sys.path.append(os.path.dirname(filename))
  
          with open(filename) as f:
              code = compile(f.read(), filename, 'exec')
              exec(code, module.__dict__)
  
          # FIXME: Can't restore the old module because tasks may be
          # continuing to execute asynchronously. We could fix this with
          # an execution fence but it doesn't seem worth it given that
          # we'll be cleaning up the process right after this.
  
          # sys.modules[run_name] = old_module
  
      @task(top_level=True, replicable=True)
      def legion_main():
          # FIXME: Really this should be the number of control replicated shards at this level
          global global_task_registration_barrier
          num_procs = Tunable.select(Tunable.GLOBAL_PYS).get()
          global_task_registration_barrier = c.legion_phase_barrier_create(_my.ctx.runtime, _my.ctx.context, num_procs)
  
          args = input_args(True)
          assert len(args) >= 2
          sys.argv = list(args)
          run_path(args[1], run_name='__main__')
  elif is_legion_python:
      print('WARNING: Executing Python modules via legion_python has been deprecated.')
      print('It is now recommended to run the script directly by passing the path')
      print('to legion_python.')
      print()
  