tricu

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

ffi.php (9568B)


      1 <?php
      2 declare(strict_types=1);
      3 namespace Arboricx;
      4 
      5 final class TricuError extends \RuntimeException
      6 {
      7     public function __construct(public readonly int $status, string $message)
      8     {
      9         parent::__construct("tricu status $status: $message");
     10     }
     11 }
     12 
     13 function findLib(): string
     14 {
     15     $override = getenv('TRICU_LIB');
     16     if ($override !== false && $override !== '') {
     17         $path = realpath($override);
     18         if ($path === false || !is_file($path)) throw new \RuntimeException("TRICU_LIB not found: $override");
     19         return $path;
     20     }
     21     foreach (['libtricu-haskell.so', 'libtricu-haskell.dylib', 'tricu-haskell.dll'] as $name) {
     22         $path = __DIR__ . '/../' . $name;
     23         if (is_file($path)) return realpath($path);
     24     }
     25     throw new \RuntimeException('Set TRICU_LIB to the Haskell embedding library');
     26 }
     27 
     28 final class Library
     29 {
     30     private static ?\FFI $native = null;
     31     private static ?string $path = null;
     32     public readonly \FFI $api;
     33     public function __construct(?string $path = null)
     34     {
     35         if (PHP_INT_SIZE !== 8) throw new \RuntimeException('A 64-bit PHP build is required');
     36         $resolved = realpath($path ?? findLib());
     37         if ($resolved === false) throw new \RuntimeException('Library not found');
     38         if (self::$native !== null && self::$path !== $resolved) {
     39             throw new \RuntimeException('One Haskell embedding library per process');
     40         }
     41         if (self::$native === null) {
     42             $header = false;
     43             foreach ([__DIR__.'/..', __DIR__.'/../../haskell/include'] as $directory) {
     44                 if (is_file($directory.'/tricu.h')) {
     45                     $header = file_get_contents($directory.'/tricu.h');
     46                     break;
     47                 }
     48             }
     49             if ($header === false) throw new \RuntimeException('Cannot read tricu headers');
     50             $header = preg_replace('/^#.*$/m', '', $header);
     51             $header = preg_replace('/extern\s+"C"\s*\{/', '', $header);
     52             $header = preg_replace('/^\}\s*$/m', '', $header);
     53             $api = \FFI::cdef($header, $resolved);
     54             $api->tricu_init();
     55             if ($api->tricu_abi_version() !== 1) throw new \RuntimeException('Haskell embedding ABI 1 required');
     56             self::$native = $api;
     57             self::$path = $resolved;
     58         }
     59         $this->api = self::$native;
     60     }
     61     public function context(): Context { return new Context($this); }
     62 }
     63 
     64 final class Context
     65 {
     66     private readonly \FFI $api;
     67     private readonly \FFI\CData $identity;
     68     private bool $closed = false;
     69     public function __construct(Library $library)
     70     {
     71         $this->api = $library->api;
     72         $this->identity = $this->api->new('tricu_context');
     73         $status = $this->api->tricu_context_new(\FFI::addr($this->identity));
     74         if ($status !== 0) throw new TricuError($status, 'creating context');
     75     }
     76     public function close(): void
     77     {
     78         if (!$this->closed) {
     79             $this->check($this->api->tricu_context_free($this->identity->cdata));
     80             $this->closed = true;
     81         }
     82     }
     83     // Explicit close is preferred; destruction also releases abandoned contexts.
     84     public function __destruct() { if (!$this->closed) $this->api->tricu_context_free($this->identity->cdata); }
     85     private function check(int $status): void
     86     {
     87         if ($status === 0) return;
     88         $ptr = $this->api->new('uint8_t *');
     89         $len = $this->api->new('size_t');
     90         $diagnostic = $this->api->tricu_last_error($this->identity->cdata, \FFI::addr($ptr), \FFI::addr($len));
     91         $message = 'invalid context';
     92         if ($diagnostic === 0) {
     93             try { $message = \FFI::string($this->api->cast('char *', $ptr), (int)$len->cdata); }
     94             finally { $this->api->tricu_buffer_free($ptr); }
     95         }
     96         throw new TricuError($status, $message);
     97     }
     98     private function out(string $name, mixed ...$arguments): \FFI\CData
     99     {
    100         // FFI scalar calls take PHP scalar values. For uint64, PHP exposes the
    101         // same 64 bits as a signed int; FFI converts those bits back to uint64.
    102         $arguments = array_map(fn($arg) => $arg instanceof \FFI\CData &&
    103             \FFI::typeof($arg)->getKind() === \FFI\CType::TYPE_UINT64 ? $arg->cdata : $arg, $arguments);
    104         $out = $this->api->new('uint64_t');
    105         $this->check($this->api->$name($this->identity->cdata, ...[...$arguments, \FFI::addr($out)]));
    106         return $out;
    107     }
    108     public function release(\FFI\CData $value): void { $this->check($this->api->tricu_release($this->identity->cdata, $value->cdata)); }
    109     public function retain(\FFI\CData $value): \FFI\CData { return $this->out('tricu_retain', $value); }
    110     public function leaf(): \FFI\CData { return $this->out('tricu_leaf'); }
    111     public function stem(\FFI\CData $value): \FFI\CData { return $this->out('tricu_stem', $value); }
    112     public function fork(\FFI\CData $a, \FFI\CData $b): \FFI\CData { return $this->out('tricu_fork', $a, $b); }
    113     public function apply(\FFI\CData $fn, \FFI\CData $arg): \FFI\CData { return $this->out('tricu_apply', $fn, $arg); }
    114     public function reduce(\FFI\CData $value): \FFI\CData { return $this->out('tricu_reduce', $value); }
    115     public function child(\FFI\CData $value, int $index): \FFI\CData
    116     {
    117         if ($index < 0 || $index > 0xffffffff) throw new \RangeException('invalid child index');
    118         return $this->out('tricu_child', $value, $index);
    119     }
    120     public function kind(\FFI\CData $value): int
    121     {
    122         $out = $this->api->new('uint32_t');
    123         $this->check($this->api->tricu_kind($this->identity->cdata, $value->cdata, \FFI::addr($out)));
    124         return $out->cdata;
    125     }
    126     public function toBool(\FFI\CData $value): bool
    127     {
    128         $out = $this->api->new('uint32_t');
    129         $this->check($this->api->tricu_to_bool($this->identity->cdata, $value->cdata, \FFI::addr($out)));
    130         return $out->cdata !== 0;
    131     }
    132     public function number(int|string $value): \FFI\CData
    133     {
    134         $text = (string)$value;
    135         if (!preg_match('/^(0|[1-9][0-9]*)$/D', $text) || strlen($text) > 20 ||
    136             (strlen($text) === 20 && strcmp($text, '18446744073709551615') > 0)) {
    137             throw new \RangeException('number must fit uint64');
    138         }
    139         // PHP integers are signed. Byte-wise arithmetic preserves the full
    140         // uint64 range when marshaling decimal input.
    141         $bytes = array_fill(0, 8, 0);
    142         foreach (str_split($text) as $digit) {
    143             $carry = (int)$digit;
    144             for ($i = 0; $i < 8; $i++) {
    145                 $n = $bytes[$i]*10+$carry;
    146                 $bytes[$i] = $n & 255;
    147                 $carry = $n >> 8;
    148             }
    149         }
    150         if (pack('S', 1) !== "\x01\x00") $bytes = array_reverse($bytes);
    151         $scalar = $this->api->new('uint64_t');
    152         \FFI::memcpy(\FFI::addr($scalar), pack('C*', ...$bytes), 8);
    153         return $this->out('tricu_from_u64', $scalar);
    154     }
    155     public function toNumber(\FFI\CData $value): string
    156     {
    157         $out = $this->out('tricu_to_u64', $value);
    158         $bytes = array_values(unpack('C*', \FFI::string($this->api->cast('char *', \FFI::addr($out)), 8)));
    159         if (pack('S', 1) === "\x01\x00") $bytes = array_reverse($bytes);
    160         $decimal = '0';
    161         foreach ($bytes as $byte) {
    162             $carry = $byte;
    163             $next = '';
    164             for ($i = strlen($decimal)-1; $i >= 0; $i--) {
    165                 $n = (int)$decimal[$i]*256+$carry;
    166                 $next = (string)($n % 10).$next;
    167                 $carry = intdiv($n, 10);
    168             }
    169             while ($carry > 0) { $next = (string)($carry % 10).$next; $carry = intdiv($carry, 10); }
    170             $decimal = ltrim($next, '0') ?: '0';
    171         }
    172         return $decimal;
    173     }
    174     private function input(string $value): \FFI\CData
    175     {
    176         $buffer = $this->api->new('uint8_t['.max(1, strlen($value)).']');
    177         if ($value !== '') \FFI::memcpy($buffer, $value, strlen($value));
    178         return $buffer;
    179     }
    180     public function bytes(string $value): \FFI\CData { return $this->out('tricu_from_bytes', $this->input($value), strlen($value)); }
    181     public function string(string $value): \FFI\CData { return $this->out('tricu_from_utf8', $this->input($value), strlen($value)); }
    182     private function buffer(string $name, \FFI\CData $value): string
    183     {
    184         $ptr = $this->api->new('uint8_t *'); $len = $this->api->new('size_t');
    185         $this->check($this->api->$name($this->identity->cdata, $value->cdata, \FFI::addr($ptr), \FFI::addr($len)));
    186         try { return \FFI::string($this->api->cast('char *', $ptr), (int)$len->cdata); }
    187         finally { $this->api->tricu_buffer_free($ptr); }
    188     }
    189     public function toBytes(\FFI\CData $value): string { return $this->buffer('tricu_to_bytes', $value); }
    190     public function toString(\FFI\CData $value): string { return $this->buffer('tricu_to_utf8', $value); }
    191     public function loadBundle(string $bytes, string $name): \FFI\CData
    192     {
    193         return $this->out('tricu_load_bundle', $this->input($bytes), strlen($bytes), $this->input($name), strlen($name));
    194     }
    195     public function runIO(\FFI\CData $program): array
    196     {
    197         $outcome = $this->api->new('uint32_t');
    198         $value = $this->api->new('tricu_handle'); $cleanup = $this->api->new('tricu_handle');
    199         $this->check($this->api->tricu_run_io($this->identity->cdata, $program->cdata,
    200             \FFI::addr($outcome), \FFI::addr($value), \FFI::addr($cleanup)));
    201         return ['outcome' => $outcome->cdata, 'value' => $value, 'cleanup' => $cleanup];
    202     }
    203 }