tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

tricu.py (10879B)


      1 """Small ctypes binding for native Haskell ABI 1; handles are owned integers.
      2 
      3 Use Context as a context manager. Release intermediate handles in long-lived
      4 contexts; close releases all remaining roots. Returned Python bytes own their data.
      5 """
      6 import ctypes as C
      7 from pathlib import Path
      8 import os
      9 from threading import Lock
     10 
     11 _loaded_path = None
     12 _loaded_native = None
     13 _load_lock = Lock()
     14 
     15 
     16 def find_library():
     17     override = os.environ.get("TRICU_LIB")
     18     if override:
     19         path = Path(override).resolve()
     20         if not path.is_file():
     21             raise FileNotFoundError(f"TRICU_LIB not found: {path}")
     22         return path
     23     for directory in (Path(__file__).parent, Path(__file__).parents[2]/"lib"):
     24         for name in ("libtricu-haskell.so", "libtricu-haskell.dylib", "tricu-haskell.dll"):
     25             path = directory/name
     26             if path.is_file():
     27                 return path.resolve()
     28     raise FileNotFoundError("Set TRICU_LIB to the Haskell embedding library")
     29 
     30 U64, U32, SIZE = C.c_uint64, C.c_uint32, C.c_size_t
     31 PTR = C.c_void_p
     32 P64, P32 = C.POINTER(U64), C.POINTER(U32)
     33 
     34 
     35 class TricuError(RuntimeError):
     36     def __init__(self, status, message):
     37         super().__init__(f"tricu status {status}: {message}")
     38         self.status = status
     39 
     40 
     41 class Library:
     42     def __init__(self, path=None):
     43         global _loaded_path, _loaded_native
     44         resolved = Path(path).resolve() if path is not None else find_library()
     45         with _load_lock:
     46             if _loaded_path is not None and resolved != _loaded_path:
     47                 raise RuntimeError('One Haskell embedding library per process')
     48             if _loaded_native is None:
     49                 _loaded_native = C.CDLL(str(resolved))
     50                 _loaded_path = resolved
     51             self.native = _loaded_native
     52         signatures = {
     53             'init': (None, []), 'abi_version': (U32, []),
     54             'context_new': (U32, [P64]), 'context_free': (U32, [U64]),
     55             'retain': (U32, [U64, U64, P64]), 'release': (U32, [U64, U64]),
     56             'leaf': (U32, [U64, P64]), 'stem': (U32, [U64, U64, P64]),
     57             'fork': (U32, [U64, U64, U64, P64]),
     58             'apply': (U32, [U64, U64, U64, P64]),
     59             'reduce': (U32, [U64, U64, P64]), 'kind': (U32, [U64, U64, P32]),
     60             'child': (U32, [U64, U64, U32, P64]),
     61             'to_bool': (U32, [U64, U64, P32]),
     62             'from_u64': (U32, [U64, U64, P64]), 'to_u64': (U32, [U64, U64, P64]),
     63             'from_bytes': (U32, [U64, PTR, SIZE, P64]),
     64             'from_utf8': (U32, [U64, PTR, SIZE, P64]),
     65             'to_bytes': (U32, [U64, U64, C.POINTER(PTR), C.POINTER(SIZE)]),
     66             'to_utf8': (U32, [U64, U64, C.POINTER(PTR), C.POINTER(SIZE)]),
     67             'load_bundle': (U32, [U64, PTR, SIZE, PTR, SIZE, P64]),
     68             'run_io': (U32, [U64, U64, P32, P64, P64]),
     69             'last_error': (U32, [U64, C.POINTER(PTR), C.POINTER(SIZE)]),
     70             'buffer_free': (None, [PTR]),
     71         }
     72         for name, (result, args) in signatures.items():
     73             fn = getattr(self.native, 'tricu_' + name)
     74             fn.restype, fn.argtypes = result, args
     75         self.native.tricu_init()
     76         if self.native.tricu_abi_version() != 1:
     77             raise RuntimeError('unsupported Haskell embedding ABI')
     78 
     79     def context(self):
     80         return Context(self)
     81 
     82 
     83 class Context:
     84     def __init__(self, library):
     85         self.library, self.native = library, library.native
     86         out = U64()
     87         status = self.native.tricu_context_new(C.byref(out))
     88         if status:
     89             raise TricuError(status, 'creating context')
     90         self.identity = out.value
     91 
     92     def _check(self, status):
     93         if status:
     94             ptr, size = PTR(), SIZE()
     95             diagnostic = self.native.tricu_last_error(self.identity, C.byref(ptr), C.byref(size))
     96             message = self._copy(ptr, size).decode('utf-8') if not diagnostic else 'invalid context'
     97             raise TricuError(status, message)
     98 
     99     def _copy(self, ptr, size):
    100         try:
    101             return C.string_at(ptr, size.value)
    102         finally:
    103             self.native.tricu_buffer_free(ptr)
    104 
    105     def _out(self, name, *args, output=U64):
    106         out = output()
    107         self._check(getattr(self.native, 'tricu_' + name)(self.identity, *args, C.byref(out)))
    108         return out.value
    109 
    110     def close(self):
    111         if self.identity:
    112             self._check(self.native.tricu_context_free(self.identity))
    113             self.identity = 0
    114 
    115     def __enter__(self):
    116         return self
    117 
    118     def __exit__(self, *_):
    119         self.close()
    120 
    121     def scope(self):
    122         """Own temporary handles created/adopted through the returned scope."""
    123         return HandleScope(self)
    124 
    125     def apply_many(self, function, *arguments):
    126         """Borrow inputs; return one owned curried application (or retained fn)."""
    127         current = self.retain(function)
    128         try:
    129             for argument in arguments:
    130                 following = self.apply(current, argument)
    131                 self.release(current)
    132                 current = following
    133             return current
    134         except BaseException:
    135             self.release(current)
    136             raise
    137 
    138     def release(self, handle):
    139         self._check(self.native.tricu_release(self.identity, handle))
    140 
    141     def retain(self, handle):
    142         return self._out('retain', handle)
    143 
    144     def leaf(self):
    145         return self._out('leaf')
    146 
    147     def stem(self, child):
    148         return self._out('stem', child)
    149 
    150     def fork(self, left, right):
    151         return self._out('fork', left, right)
    152 
    153     def apply(self, fn, arg):
    154         return self._out('apply', fn, arg)
    155 
    156     def reduce(self, value):
    157         return self._out('reduce', value)
    158 
    159     def kind(self, value):
    160         return self._out('kind', value, output=U32)
    161 
    162     def child(self, value, index):
    163         if not isinstance(index, int) or not 0 <= index <= 0xffffffff:
    164             raise ValueError('invalid child index')
    165         return self._out('child', value, index)
    166 
    167     def number(self, value):
    168         if not isinstance(value, int) or not 0 <= value < 2**64:
    169             raise ValueError('number must fit uint64')
    170         return self._out('from_u64', value)
    171 
    172     def to_bool(self, handle):
    173         return bool(self._out('to_bool', handle, output=U32))
    174 
    175     def to_number(self, handle):
    176         return self._out('to_u64', handle)
    177 
    178     def bytes(self, value):
    179         return self._out('from_bytes', value, len(value))
    180 
    181     def string(self, value):
    182         encoded = value.encode('utf-8')
    183         return self._out('from_utf8', encoded, len(encoded))
    184 
    185     def _buffer(self, name, handle):
    186         ptr, size = PTR(), SIZE()
    187         self._check(getattr(self.native, 'tricu_' + name)(
    188             self.identity, handle, C.byref(ptr), C.byref(size)))
    189         return self._copy(ptr, size)
    190 
    191     def to_bytes(self, handle):
    192         return self._buffer('to_bytes', handle)
    193 
    194     def to_string(self, handle):
    195         return self._buffer('to_utf8', handle).decode('utf-8')
    196 
    197     def load_bundle(self, data, name):
    198         encoded = name.encode('utf-8')
    199         return self._out('load_bundle', data, len(data), encoded, len(encoded))
    200 
    201     def run_io(self, program):
    202         """Return (outcome, value_handle, cleanup_handle); payloads stay lazy."""
    203         outcome, value, cleanup = U32(), U64(), U64()
    204         self._check(self.native.tricu_run_io(self.identity, program,
    205                     C.byref(outcome), C.byref(value), C.byref(cleanup)))
    206         return outcome.value, value.value, cleanup.value
    207 
    208 
    209 class HandleScope:
    210     """Deterministic roots for one synchronous operation in a reusable context.
    211 
    212     Context methods remain usable independently. Only handles returned by this
    213     scope's constructors/operations or explicitly adopted with own() belong here.
    214     Inputs are borrowed. detach() transfers a root back to the caller.
    215     """
    216     def __init__(self, context):
    217         self.context = context
    218         self._handles = {}
    219         self._closed = False
    220 
    221     def __enter__(self):
    222         self._check_open()
    223         return self
    224 
    225     def __exit__(self, kind, error, traceback):
    226         try:
    227             self.close()
    228         except Exception as cleanup:
    229             if error is None:
    230                 raise
    231             if hasattr(error, 'add_note'):
    232                 error.add_note(f'Handle scope cleanup failed: {cleanup}')
    233 
    234     def _check_open(self):
    235         if self._closed:
    236             raise RuntimeError('handle scope is closed')
    237 
    238     def own(self, handle):
    239         """Adopt one existing independently owned root, without retaining it."""
    240         self._check_open()
    241         if handle in self._handles:
    242             raise ValueError('handle already owned by scope')
    243         self._handles[handle] = None
    244         return handle
    245 
    246     def detach(self, handle):
    247         self._check_open()
    248         if handle not in self._handles:
    249             raise ValueError('handle is not owned by scope')
    250         del self._handles[handle]
    251         return handle
    252 
    253     def release(self, handle):
    254         self._check_open()
    255         if handle not in self._handles:
    256             raise ValueError('handle is not owned by scope')
    257         self.context.release(handle)
    258         del self._handles[handle]
    259 
    260     def close(self):
    261         if self._closed:
    262             return
    263         self._closed = True
    264         handles, self._handles = self._handles, {}
    265         first_error = None
    266         if self.context.identity:
    267             for handle in reversed(handles):
    268                 try:
    269                     self.context.release(handle)
    270                 except Exception as error:
    271                     if first_error is None:
    272                         first_error = error
    273         if first_error is not None:
    274             raise first_error
    275 
    276     def _create(self, method, *args):
    277         self._check_open()
    278         return self.own(getattr(self.context, method)(*args))
    279 
    280     def leaf(self): return self._create('leaf')
    281     def stem(self, child): return self._create('stem', child)
    282     def fork(self, left, right): return self._create('fork', left, right)
    283     def retain(self, value): return self._create('retain', value)
    284     def reduce(self, value): return self._create('reduce', value)
    285     def child(self, value, index): return self._create('child', value, index)
    286     def number(self, value): return self._create('number', value)
    287     def string(self, value): return self._create('string', value)
    288     def bytes(self, value): return self._create('bytes', value)
    289     def apply(self, function, argument): return self._create('apply', function, argument)
    290     def apply_many(self, function, *arguments): return self._create('apply_many', function, *arguments)
    291     def load_bundle(self, data, name): return self._create('load_bundle', data, name)
    292 
    293     def run_io(self, program):
    294         self._check_open()
    295         outcome, value, cleanup = self.context.run_io(program)
    296         self.own(value)
    297         self.own(cleanup)
    298         return outcome, value, cleanup