feat(php): use new FFI for Arboricx

This commit is contained in:
2026-05-11 09:18:47 -05:00
parent d7a7a8134c
commit d37d443021
8 changed files with 305 additions and 605 deletions

View File

@@ -63,7 +63,7 @@ Arboricx is the portable executable-object format used by tricu. The project now
| **tricu (self-hosted)** | `kernel_run_arboricx_typed.dag` | A self-hosting Arboricx parser/executor written in tricu itself. Used as a kernel inside the Zig host for maximum portability ("cool but useless" — ~3s for `append`) | | **tricu (self-hosted)** | `kernel_run_arboricx_typed.dag` | A self-hosting Arboricx parser/executor written in tricu itself. Used as a kernel inside the Zig host for maximum portability ("cool but useless" — ~3s for `append`) |
| **Zig** | `ext/zig/` | **Production host** — native bundle parser, WHNF reducer, C ABI (`libarboricx.so` / `.a`), CLI (`tricu-zig`), Python FFI support | | **Zig** | `ext/zig/` | **Production host** — native bundle parser, WHNF reducer, C ABI (`libarboricx.so` / `.a`), CLI (`tricu-zig`), Python FFI support |
| **JavaScript (Node)** | `ext/js/` | Native bundle parser, manifest decoder, Merkle DAG verifier, Tree Calculus reducer, CLI runner | | **JavaScript (Node)** | `ext/js/` | Native bundle parser, manifest decoder, Merkle DAG verifier, Tree Calculus reducer, CLI runner |
| **PHP** | `ext/php/` | Tree Calculus reducer, codecs, kernel loader, CLI runner | | **PHP** | `ext/php/` | FFI wrapper around `libarboricx.so`, CLI runner |
All hosts share the same bundle format and Merkle hashing scheme. All hosts share the same bundle format and Merkle hashing scheme.
@@ -240,6 +240,8 @@ The kernel path is kept as a "cool but useless" fallback — the DAG is tiny (~3
| `packages.default` / `packages.tricu` | Haskell tricu package | | `packages.default` / `packages.tricu` | Haskell tricu package |
| `packages.tricu-zig` | Zig CLI + `libarboricx.a` + `libarboricx.so` + `arboricx.h` | | `packages.tricu-zig` | Zig CLI + `libarboricx.a` + `libarboricx.so` + `arboricx.h` |
| `packages.tricu-zig-tests` | **Separate test target** — C ABI + native bundle + Python FFI tests | | `packages.tricu-zig-tests` | **Separate test target** — C ABI + native bundle + Python FFI tests |
| `packages.tricu-php` | PHP source + `libarboricx.so` + `tricu-php` wrapper script |
| `packages.tricu-php-tests` | **Separate test target** — PHP FFI tests against fixture bundles |
| `packages.tricu-container` | Docker image | | `packages.tricu-container` | Docker image |
| `checks.default` / `checks.tricu` | Haskell test suite via Tasty/HUnit | | `checks.default` / `checks.tricu` | Haskell test suite via Tasty/HUnit |
@@ -287,12 +289,9 @@ tricu/
│ │ │ ├── codecs.js │ │ │ ├── codecs.js
│ │ │ └── cli.js │ │ │ └── cli.js
│ │ └── test/ │ │ └── test/
│ ├── php/ # PHP bundle loader + reducer │ ├── php/ # PHP FFI host for libarboricx.so
│ │ ├── src/ │ │ ├── src/
│ │ │ ── functions.php │ │ │ ── ffi.php
│ │ │ ├── codecs.php
│ │ │ ├── kernel.php
│ │ │ └── Tree/
│ │ └── run.php │ │ └── run.php
│ └── zig/ # Zig production host │ └── zig/ # Zig production host
│ ├── build.zig │ ├── build.zig

View File

@@ -4,32 +4,81 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* run.php — Self-hosted Arboricx PHP host shell. * run.php — Arboricx PHP host shell via libarboricx C ABI.
* *
* Usage: * Usage:
* php run.php run <bundle.arboricx> <arg> [arg ...] * php run.php run <bundle.arboricx> [args...]
* php run.php inspect <bundle.arboricx> * php run.php inspect <bundle.arboricx>
*
* The "run" command:
* 1. Reads the .arboricx bundle as raw bytes
* 2. Encodes bundle bytes as a Tree Calculus byte list
* 3. Encodes each host argument (string or number)
* 4. Calls runArboricxToString via the hardcoded kernel
* 5. Unwraps ok/err, then Host ABI envelope
* 6. Decodes the string payload
*
* This is a minimal host shell — it does NOT parse Arboricx bundles itself.
* The kernel handles bundle parsing internally.
*/ */
require __DIR__ . '/src/functions.php'; require __DIR__ . '/src/ffi.php';
require __DIR__ . '/src/codecs.php';
require __DIR__ . '/src/kernel.php';
use Arboricx\Node; use function Arboricx\{ctx_init, ctx_free, loadBundleDefault, ofNumber, ofString, app, reduce, toString, toBool, toNumber};
use function Arboricx\{app, reduce, ofString, ofNumber, ofBytes, ofList,
formatTree, unwrapResult, unwrapHostValue, decodeHostPayload, // ── Locate libarboricx.so ──────────────────────────────────────────────────
getRunArboricxToString};
function findLib(): string
{
$env = getenv('ARBORICX_LIB');
if ($env !== false && file_exists($env)) {
return $env;
}
$paths = [
__DIR__ . '/../../zig/zig-out/lib/libarboricx.so',
'/usr/local/lib/libarboricx.so',
'/usr/lib/libarboricx.so',
'./libarboricx.so',
];
foreach ($paths as $p) {
if (file_exists($p)) {
return $p;
}
}
fwrite(STDERR, "Error: libarboricx.so not found.\nSet ARBORICX_LIB to its full path.\n");
exit(1);
}
// ── Decode helpers ─────────────────────────────────────────────────────────
function decode(\FFI\CData $ctx, int $root): string
{
// Bool first: false is Leaf, which is also a valid empty string/list.
try {
return toBool($ctx, $root) ? 'true' : 'false';
} catch (\Throwable $e) {
try {
return toString($ctx, $root);
} catch (\Throwable $e2) {
try {
return (string) toNumber($ctx, $root);
} catch (\Throwable $e3) {
throw new \RuntimeException('could not decode result');
}
}
}
}
function decodeType(\FFI\CData $ctx, int $root): string
{
try {
toBool($ctx, $root);
return 'bool';
} catch (\Throwable $e) {
try {
toString($ctx, $root);
return 'string';
} catch (\Throwable $e2) {
try {
toNumber($ctx, $root);
return 'number';
} catch (\Throwable $e3) {
return 'unknown (raw tree)';
}
}
}
}
// ── Commands ───────────────────────────────────────────────────────────────── // ── Commands ─────────────────────────────────────────────────────────────────
@@ -47,94 +96,43 @@ function readBundle(string $path): string
return $bytes; return $bytes;
} }
function cmdRun(string $bundlePath, array $args): void function cmdRun(string $libPath, string $bundlePath, array $args): void
{ {
$bundleBytes = readBundle($bundlePath); $ctx = ctx_init($libPath);
$kernel = getRunArboricxToString();
if ($kernel === null) {
fwrite(STDERR, "Error: runArboricxToString kernel not configured\n");
exit(1);
}
$bundleTree = ofBytes($bundleBytes);
$argTrees = [];
foreach ($args as $arg) {
$argTrees[] = encodeArg($arg);
}
$argsTree = ofList($argTrees);
// Kernel application: runArboricxToString bundle args
$expr = app(app($kernel, $bundleTree), $argsTree);
fwrite(STDERR, "Reducing kernel application...\n");
$result = reduce($expr, 1_000_000_000_000);
// The kernel returns an ok/err pair. On ok, the value is a
// Host ABI envelope: Fork(tag_number, payload_tree).
[$kind, $value, $rest] = unwrapResult($result);
if ($kind === 'error') {
fwrite(STDERR, "Error detail: $rest\n");
exit(1);
}
if ($kind === 'err') {
[$ok2, $code] = Arboricx\toNumber($value);
$codeStr = $ok2 ? (string)$code : formatTree($value);
fwrite(STDERR, "Arboricx error code: $codeStr\n");
fwrite(STDERR, "Rest: " . formatTree($rest) . "\n");
exit(1);
}
[$tag, $payload] = unwrapHostValue($value);
try { try {
$decoded = decodeHostPayload($tag, $payload); $term = loadBundleDefault($ctx, readBundle($bundlePath));
echo $decoded['value'] . "\n";
} catch (\Throwable $e) {
fwrite(STDERR, "Host ABI decode error: " . $e->getMessage() . "\n");
fwrite(STDERR, "Raw tag: $tag, payload: " . formatTree($payload) . "\n");
exit(1);
}
}
function encodeArg(string $arg): Node foreach ($args as $arg) {
{ $argTree = preg_match('/^\d+$/', $arg) ? ofNumber($ctx, (int)$arg) : ofString($ctx, $arg);
return ctype_digit($arg) ? ofNumber((int)$arg) : ofString($arg); $term = app($ctx, $term, $argTree);
}
function cmdInspect(string $bundlePath): void
{
$bundleBytes = readBundle($bundlePath);
$bytes = strlen($bundleBytes);
echo "Bundle: $bundlePath\n";
echo "Size: $bytes bytes\n";
// Run with no arguments to see the default export
$kernel = getRunArboricxToString();
if ($kernel === null) {
fwrite(STDERR, "Warning: kernel not configured, skipping execution\n");
return;
}
$bundleTree = ofBytes($bundleBytes);
$emptyArgs = ofList([]);
$result = reduce(app(app($kernel, $bundleTree), $emptyArgs), 10_000);
[$kind, $value, $rest] = unwrapResult($result);
if ($kind === 'ok') {
echo "\nResult (ok):\n";
try {
[$tag, $payload] = unwrapHostValue($value);
$decoded = decodeHostPayload($tag, $payload);
echo " Tag: $tag (type: " . $decoded['type'] . ")\n";
echo " Value: " . $decoded['value'] . "\n";
} catch (\Throwable $e) {
echo " Raw: " . formatTree($value) . "\n";
} }
} else {
echo "\nResult (err):\n"; $result = reduce($ctx, $term, 1_000_000_000);
[$ok, $code] = Arboricx\toNumber($value); echo decode($ctx, $result) . "\n";
echo " Code: " . ($ok ? (string)$code : formatTree($value)) . "\n"; } finally {
ctx_free($ctx);
}
}
function cmdInspect(string $libPath, string $bundlePath): void
{
$ctx = ctx_init($libPath);
try {
$bundle = readBundle($bundlePath);
echo "Bundle: $bundlePath\nSize: " . strlen($bundle) . " bytes\n\nResult:\n";
$term = loadBundleDefault($ctx, $bundle);
$result = reduce($ctx, $term, 1_000_000_000);
$type = decodeType($ctx, $result);
try {
$value = decode($ctx, $result);
} catch (\RuntimeException $e) {
$value = '(raw tree)';
}
echo " Type: $type\n Value: $value\n";
} finally {
ctx_free($ctx);
} }
} }
@@ -144,31 +142,31 @@ $argv = $_SERVER['argv'] ?? [];
$argc = $_SERVER['argc'] ?? 0; $argc = $_SERVER['argc'] ?? 0;
if ($argc < 2) { if ($argc < 2) {
echo "Arboricx PHP Host Shell\n"; echo "Arboricx PHP Host Shell (via libarboricx C ABI)\n\nUsage:\n";
echo "\nUsage:\n";
echo " php run.php run <bundle.arboricx> [args...]\n"; echo " php run.php run <bundle.arboricx> [args...]\n";
echo " php run.php inspect <bundle.arboricx>\n"; echo " php run.php inspect <bundle.arboricx>\n";
exit(0); exit(0);
} }
$libPath = findLib();
$command = $argv[1]; $command = $argv[1];
switch ($command) { switch ($command) {
case 'run': case 'run':
if ($argc < 3) { if ($argc < 3) {
fwrite(STDERR, "Usage: php run.php run <bundle.arboricx> [args...]\n"); fwrite(STDERR, "Usage: php run.php run <bundle.arboricx> [args...]\n");
exit(1); exit(1);
} }
cmdRun($argv[2], array_slice($argv, 3)); cmdRun($libPath, $argv[2], array_slice($argv, 3));
break; break;
case 'inspect': case 'inspect':
if ($argc < 3) { if ($argc < 3) {
fwrite(STDERR, "Usage: php run.php inspect <bundle.arboricx>\n"); fwrite(STDERR, "Usage: php run.php inspect <bundle.arboricx>\n");
exit(1); exit(1);
} }
cmdInspect($argv[2]); cmdInspect($libPath, $argv[2]);
break; break;
default: default:
echo "Unknown command: $command\n"; fwrite(STDERR, "Unknown command: $command\nUsage: php run.php run|inspect ...\n");
echo "Usage: php run.php run|inspect ...\n";
exit(1); exit(1);
} }

View File

@@ -1,179 +0,0 @@
<?php
declare(strict_types=1);
namespace Arboricx;
use function Arboricx\{leaf, stem, fork, isLeaf, isStem, isFork, reduce, stemChild, forkLeft, forkRight, same_tree, formatTree};
$GLOBALS['ARB_NUM_CACHE'] = [];
function ofNumber(int $n): Node
{
if ($n < 0) throw new \InvalidArgumentException('ofNumber: negative values not supported');
if ($n === 0) return leaf();
if ($n < 256 && isset($GLOBALS['ARB_NUM_CACHE'][$n])) {
return $GLOBALS['ARB_NUM_CACHE'][$n];
}
$bitTree = ($n % 2) === 1 ? stem(leaf()) : leaf();
$result = fork($bitTree, ofNumber(intdiv($n, 2)));
if ($n < 256) {
$GLOBALS['ARB_NUM_CACHE'][$n] = $result;
}
return $result;
}
function toNumber(Node $tree): array
{
$tree = reduce($tree);
if (isLeaf($tree)) return [true, 0];
if (!isFork($tree)) return [false, 'Invalid Tree Calculus number'];
$bitTree = reduce(forkLeft($tree));
if (isLeaf($bitTree)) {
$bit = 0;
} elseif (isStem($bitTree) && isLeaf(stemChild($bitTree))) {
$bit = 1;
} else {
return [false, 'Invalid bit in Tree Calculus number'];
}
[$ok, $rest] = toNumber(forkRight($tree));
return $ok ? [true, $bit + 2 * $rest] : [false, $rest];
}
function ofList(array $elements): Node
{
$result = leaf();
for ($i = count($elements) - 1; $i >= 0; $i--) {
$result = fork($elements[$i], $result);
}
return $result;
}
function toList(Node $tree): array
{
$tree = reduce($tree);
if (isLeaf($tree)) return [true, []];
if (!isFork($tree)) return [false, 'Invalid Tree Calculus list'];
[$ok, $rest] = toList(forkRight($tree));
if (!$ok) return [false, $rest];
array_unshift($rest, forkLeft($tree));
return [true, $rest];
}
/**
* Strings are lists of byte values (each character encoded as a number tree).
*/
function ofString(string $s): Node
{
$bytes = [];
for ($i = 0, $len = strlen($s); $i < $len; $i++) {
$bytes[] = ofNumber(ord($s[$i]));
}
return ofList($bytes);
}
function toString(Node $tree): array
{
[$ok, $elements] = toList($tree);
if (!$ok) return [false, $elements];
$result = '';
foreach ($elements as $elem) {
[$ok2, $num] = toNumber($elem);
if (!$ok2 || $num < 0 || $num > 255) {
return [false, 'Invalid character code in Tree Calculus string'];
}
$result .= chr($num);
}
return [true, $result];
}
function ofBytes(string $s): Node
{
return ofString($s);
}
function toBytes(Node $tree): array
{
return toString($tree);
}
function unwrapResult(Node $tree): array
{
$tree = reduce($tree);
if (!isFork($tree)) return ['error', null, 'Result is not a valid ok/err pair'];
$tag = reduce(forkLeft($tree));
$restPair = reduce(forkRight($tree));
if (!isFork($restPair)) return ['error', null, 'Result payload is not a valid pair'];
$value = forkLeft($restPair);
$rest = forkRight($restPair);
$isTrue = same_tree($tag, stem(leaf()));
return $isTrue ? ['ok', $value, $rest] : ['err', $value, $rest];
}
/**
* Host ABI tag constants.
*
* The kernel wraps successful results in a host value envelope:
* Fork(tag_number, payload_tree)
*/
const HOST_TREE_TAG = 0;
const HOST_STRING_TAG = 1;
const HOST_NUMBER_TAG = 2;
const HOST_BOOL_TAG = 3;
const HOST_LIST_TAG = 4;
const HOST_BYTES_TAG = 5;
function unwrapHostValue(Node $tree): array
{
$tree = reduce($tree);
if (!isFork($tree)) throw new \InvalidArgumentException('Host ABI value must be a pair');
$tag = reduce(forkLeft($tree));
$payload = forkRight($tree);
[$ok, $tagNum] = toNumber($tag);
if (!$ok) throw new \InvalidArgumentException('Host ABI tag must be a number');
return [(int)$tagNum, $payload];
}
function isBool(Node $tree): bool
{
$tree = reduce($tree);
return isLeaf($tree) || (isStem($tree) && isLeaf(stemChild($tree)));
}
function decodeHostPayload(int $tag, Node $payload): array
{
switch ($tag) {
case HOST_TREE_TAG:
return ['type' => 'tree', 'value' => formatTree($payload)];
case HOST_STRING_TAG:
[$ok, $str] = toString($payload);
if (!$ok) throw new \InvalidArgumentException('Host ABI string decode failed: ' . $str);
return ['type' => 'string', 'value' => $str];
case HOST_NUMBER_TAG:
[$ok, $num] = toNumber($payload);
if (!$ok) throw new \InvalidArgumentException('Host ABI number decode failed: ' . $num);
return ['type' => 'number', 'value' => $num];
case HOST_BOOL_TAG:
if (!isBool($payload)) throw new \InvalidArgumentException('Host ABI bool decode failed');
return ['type' => 'bool', 'value' => isLeaf(reduce($payload)) ? false : true];
case HOST_LIST_TAG:
[$ok, $xs] = toList($payload);
if (!$ok) throw new \InvalidArgumentException('Host ABI list decode failed: ' . $xs);
return ['type' => 'list', 'value' => $xs];
case HOST_BYTES_TAG:
[$ok, $bytes] = toBytes($payload);
if (!$ok) throw new \InvalidArgumentException('Host ABI bytes decode failed: ' . $bytes);
return ['type' => 'bytes', 'value' => $bytes];
default:
throw new \InvalidArgumentException('Unknown Host ABI tag: ' . $tag);
}
}

138
ext/php/src/ffi.php Normal file
View File

@@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace Arboricx;
/**
* FFI wrapper around libarboricx.so.
*
* Loads the shared library and exposes typed wrappers for the C ABI.
*/
final class ArboricxFFI
{
private static ?\FFI $ffi = null;
public static function init(string $libPath): void
{
if (self::$ffi !== null) {
return;
}
// Nix output layout first, then repo layout.
$candidates = [
__DIR__ . '/../arboricx.h',
__DIR__ . '/../../zig/include/arboricx.h',
];
$headerRaw = false;
foreach ($candidates as $path) {
$headerRaw = file_get_contents($path);
if ($headerRaw !== false) break;
}
if ($headerRaw === false) {
throw new \RuntimeException('Cannot read arboricx.h');
}
// PHP FFI only parses plain C declarations.
$header = $headerRaw;
$header = preg_replace('/#.*\n/', "\n", $header);
$header = preg_replace('/extern\s+"C"\s*\{/', '', $header);
$header = str_replace('}', '', $header);
$header = preg_replace('/\n\s*\n+/', "\n", $header);
self::$ffi = \FFI::cdef($header, $libPath);
}
public static function ffi(): \FFI
{
if (self::$ffi === null) {
throw new \RuntimeException('ArboricxFFI not initialized. Call ArboricxFFI::init($libPath) first.');
}
return self::$ffi;
}
}
function ctx_init(string $libPath): \FFI\CData
{
ArboricxFFI::init($libPath);
$ctx = ArboricxFFI::ffi()->arboricx_init();
if ($ctx === null) {
throw new \RuntimeException('arboricx_init failed');
}
return $ctx;
}
function ctx_free(\FFI\CData $ctx): void
{
ArboricxFFI::ffi()->arboricx_free($ctx);
}
function app(\FFI\CData $ctx, int $func, int $arg): int
{
return ArboricxFFI::ffi()->arb_app($ctx, $func, $arg);
}
function reduce(\FFI\CData $ctx, int $root, int $fuel = 1_000_000_000): int
{
return ArboricxFFI::ffi()->arb_reduce($ctx, $root, $fuel);
}
function ofNumber(\FFI\CData $ctx, int $n): int
{
return ArboricxFFI::ffi()->arb_of_number($ctx, $n);
}
function ofString(\FFI\CData $ctx, string $s): int
{
return ArboricxFFI::ffi()->arb_of_string($ctx, $s);
}
function toNumber(\FFI\CData $ctx, int $root): int
{
$out = ArboricxFFI::ffi()->new('uint64_t');
$ok = ArboricxFFI::ffi()->arb_to_number($ctx, $root, \FFI::addr($out));
if (!$ok) {
throw new \RuntimeException('arb_to_number failed');
}
return (int) $out->cdata;
}
function toString(\FFI\CData $ctx, int $root): string
{
$ptr = ArboricxFFI::ffi()->new('uint8_t*');
$len = ArboricxFFI::ffi()->new('size_t');
$ok = ArboricxFFI::ffi()->arb_to_string($ctx, $root, \FFI::addr($ptr), \FFI::addr($len));
if (!$ok) {
throw new \RuntimeException('arb_to_string failed');
}
$length = (int) $len->cdata;
$result = '';
for ($i = 0; $i < $length; $i++) {
$result .= chr($ptr[$i]);
}
ArboricxFFI::ffi()->arboricx_free_buf($ctx, $ptr, $length);
return $result;
}
function toBool(\FFI\CData $ctx, int $root): bool
{
$out = ArboricxFFI::ffi()->new('int');
$ok = ArboricxFFI::ffi()->arb_to_bool($ctx, $root, \FFI::addr($out));
if (!$ok) {
throw new \RuntimeException('arb_to_bool failed');
}
return (bool) $out->cdata;
}
function loadBundleDefault(\FFI\CData $ctx, string $bytes): int
{
$cdata = ArboricxFFI::ffi()->new('uint8_t[' . strlen($bytes) . ']');
for ($i = 0; $i < strlen($bytes); $i++) {
$cdata[$i] = ord($bytes[$i]);
}
$result = ArboricxFFI::ffi()->arb_load_bundle_default($ctx, $cdata, strlen($bytes));
if ($result === 0) {
throw new \RuntimeException('arb_load_bundle_default failed');
}
return $result;
}

View File

@@ -1,255 +0,0 @@
<?php
declare(strict_types=1);
namespace Arboricx;
/**
* Node-based Tree Calculus graph.
*
* Nodes are plain PHP objects so the runtime's refcounting GC
* can reclaim unreachable subtrees automatically.
*
* tag 0 = Leaf
* tag 1 = Stem(child)
* tag 2 = Fork(left, right)
* tag 3 = App(function, argument) -- evaluator thunk
*/
final class Node
{
public int $tag;
public ?Node $a;
public ?Node $b;
public function __construct(int $tag = 0, ?Node $a = null, ?Node $b = null)
{
$this->tag = $tag;
$this->a = $a;
$this->b = $b;
}
}
$GLOBALS['ARB_LEAF'] = new Node();
$GLOBALS['ARB_CONS'] = [];
function newNode(int $tag, ?Node $a = null, ?Node $b = null): Node
{
if ($tag !== 3) {
$key = $tag === 1
? '1:' . spl_object_id($a)
: '2:' . spl_object_id($a) . ':' . spl_object_id($b);
if (isset($GLOBALS['ARB_CONS'][$key])) {
return $GLOBALS['ARB_CONS'][$key];
}
$n = new Node($tag, $a, $b);
$GLOBALS['ARB_CONS'][$key] = $n;
return $n;
}
return new Node($tag, $a, $b);
}
function leaf(): Node
{
return $GLOBALS['ARB_LEAF'];
}
function stem(Node $c): Node
{
return newNode(1, $c);
}
function fork(Node $l, Node $r): Node
{
return newNode(2, $l, $r);
}
function app(Node $f, Node $x): Node
{
return newNode(3, $f, $x);
}
function tag(Node $t): int
{
return $t->tag;
}
function isLeaf(Node $t): bool
{
return $t->tag === 0;
}
function isStem(Node $t): bool
{
return $t->tag === 1;
}
function isFork(Node $t): bool
{
return $t->tag === 2;
}
function isApp(Node $t): bool
{
return $t->tag === 3;
}
function stemChild(Node $t): Node
{
return $t->a;
}
function forkLeft(Node $t): Node
{
return $t->a;
}
function forkRight(Node $t): Node
{
return $t->b;
}
function appFunc(Node $t): Node
{
return $t->a;
}
function appArg(Node $t): Node
{
return $t->b;
}
function reduce(Node $term, int $fuel = 100_000_000_000_000_000): Node
{
return whnf($term, $fuel);
}
function whnf(Node $term, int &$fuel): Node
{
while (true) {
if ($term->tag !== 3) {
return $term;
}
$orig = $term;
$f = whnf($term->a, $fuel);
$x = $term->b;
$ftag = $f->tag;
// apply Leaf b = Stem b
if ($ftag === 0) {
$orig->tag = 1;
$orig->a = $x;
$orig->b = null;
return $orig;
}
// apply (Stem a) b = Fork a b
if ($ftag === 1) {
$orig->tag = 2;
$orig->a = $f->a;
$orig->b = $x;
return $orig;
}
if ($ftag !== 2) {
throw new \RuntimeException('apply: function did not reduce to tree');
}
$left = whnf($f->a, $fuel);
$right = $f->b;
$ltag = $left->tag;
// apply (Fork Leaf a) _ = a
if ($ltag === 0) {
$result = whnf($right, $fuel);
if ($orig !== $result) {
$orig->tag = $result->tag;
$orig->a = $result->a;
$orig->b = $result->b;
}
if ($fuel <= 0) throw new \RuntimeException('fuel exhausted');
$fuel--;
return $orig;
}
// apply (Fork (Stem a) b) c = (a c) (b c)
if ($ltag === 1) {
$inner1 = newNode(3, $left->a, $x);
$inner2 = newNode(3, $right, $x);
$orig->tag = 3;
$orig->a = $inner1;
$orig->b = $inner2;
$term = $orig;
if ($fuel <= 0) throw new \RuntimeException('fuel exhausted');
$fuel--;
continue;
}
if ($ltag !== 2) {
throw new \RuntimeException('apply: invalid Fork left child');
}
$arg = whnf($x, $fuel);
$atag = $arg->tag;
// apply (Fork (Fork a b) c) Leaf = a
if ($atag === 0) {
$result = whnf($left->a, $fuel);
if ($orig !== $result) {
$orig->tag = $result->tag;
$orig->a = $result->a;
$orig->b = $result->b;
}
if ($fuel <= 0) throw new \RuntimeException('fuel exhausted');
$fuel--;
return $orig;
}
// apply (Fork (Fork a b) c) (Stem u) = b u
if ($atag === 1) {
$orig->tag = 3;
$orig->a = $left->b;
$orig->b = $arg->a;
$term = $orig;
if ($fuel <= 0) throw new \RuntimeException('fuel exhausted');
$fuel--;
continue;
}
// apply (Fork (Fork a b) c) (Fork u v) = (c u) v
if ($atag === 2) {
$inner = newNode(3, $right, $arg->a);
$orig->tag = 3;
$orig->a = $inner;
$orig->b = $arg->b;
$term = $orig;
if ($fuel <= 0) throw new \RuntimeException('fuel exhausted');
$fuel--;
continue;
}
throw new \RuntimeException('apply: argument did not reduce to tree');
}
}
function same_tree(Node $a, Node $b): bool
{
if ($a === $b) return true;
if ($a->tag !== $b->tag) return false;
if ($a->tag === 0) return true;
if ($a->tag === 1) return same_tree($a->a, $b->a);
if ($a->tag === 2) return same_tree($a->a, $b->a) && same_tree($a->b, $b->b);
if ($a->tag === 3) return same_tree($a->a, $b->a) && same_tree($a->b, $b->b);
return false;
}
function formatTree(Node $t, int $depth = 0): string
{
if ($depth > 200) return '...';
if ($t->tag === 0) return 'Leaf';
if ($t->tag === 1) return 'Stem(' . formatTree($t->a, $depth + 1) . ')';
if ($t->tag === 2) return 'Fork(' . formatTree($t->a, $depth + 1) . ', ' . formatTree($t->b, $depth + 1) . ')';
if ($t->tag === 3) return 'App(' . formatTree($t->a, $depth + 1) . ', ' . formatTree($t->b, $depth + 1) . ')';
return 'Unknown';
}

View File

@@ -1,53 +0,0 @@
<?php
declare(strict_types=1);
namespace Arboricx;
/**
* Kernel loader.
*
* The kernel is the self-hosted Arboricx runtime compiled to a raw Tree
* Calculus term. It is shipped as a ternary string and parsed on demand.
*
* Ternary string format (matches Research.hs toTernaryString):
* '0' → Leaf
* '1' rest → Stem(parse(rest))
* '2' l r → Fork(parse(l), parse(r))
*/
use function Arboricx\{leaf, stem, fork};
function loadKernelTernary(string $name): string
{
$file = __DIR__ . '/' . $name . '.ternary';
if (!file_exists($file)) return '';
$content = file_get_contents($file);
return $content !== false ? trim($content) : '';
}
function parseTernary(string $s, int &$pos = 0): Node
{
if ($pos >= strlen($s)) {
throw new \RuntimeException('parseTernary: unexpected end of string');
}
$char = $s[$pos++];
return match ($char) {
'0' => leaf(),
'1' => stem(parseTernary($s, $pos)),
'2' => fork(parseTernary($s, $pos), parseTernary($s, $pos)),
default => throw new \RuntimeException("parseTernary: unexpected char '$char' at position $pos"),
};
}
function getRunArboricxToString(): ?Node
{
$term = loadKernelTernary('kernel_run_arboricx_to_string');
if ($term === '') return null;
$pos = 0;
$tree = parseTernary($term, $pos);
if ($pos !== strlen($term)) {
throw new \RuntimeException("kernel ternary has trailing data: parsed $pos of " . strlen($term) . ' bytes');
}
return $tree;
}

File diff suppressed because one or more lines are too long

View File

@@ -50,7 +50,6 @@
''; '';
}; };
# Separate test target — not included in `nix flake check`
tricuZigTests = pkgs.stdenv.mkDerivation { tricuZigTests = pkgs.stdenv.mkDerivation {
pname = "tricu-zig-tests"; pname = "tricu-zig-tests";
version = "0.1.0"; version = "0.1.0";
@@ -100,11 +99,64 @@
echo "All Zig tests passed" > $out/result echo "All Zig tests passed" > $out/result
''; '';
}; };
# ------------------------------------------------------------------
# PHP FFI host
# ------------------------------------------------------------------
tricuPhp = pkgs.stdenv.mkDerivation {
pname = "tricu-php";
version = "0.1.0";
src = ./ext/php;
nativeBuildInputs = [ pkgs.makeWrapper phpWithFfi tricuZig ];
buildPhase = "true";
installPhase = ''
mkdir -p $out/share/tricu-php $out/lib $out/bin
cp -r src run.php $out/share/tricu-php/
cp ${tricuZig}/lib/libarboricx.so $out/lib/
cp ${tricuZig}/include/arboricx.h $out/share/tricu-php/
makeWrapper ${phpWithFfi}/bin/php $out/bin/tricu-php \
--add-flags "$out/share/tricu-php/run.php" \
--set ARBORICX_LIB "$out/lib/libarboricx.so" \
--prefix LD_LIBRARY_PATH : "$out/lib"
'';
};
# ------------------------------------------------------------------
# PHP FFI tests (separate target)
# ------------------------------------------------------------------
phpWithFfi = pkgs.php.withExtensions (exts: [ pkgs.phpExtensions.ffi ]);
tricuPhpTests = pkgs.stdenv.mkDerivation {
pname = "tricu-php-tests";
version = "0.1.0";
src = ./.;
nativeBuildInputs = [ phpWithFfi tricuPhp ];
buildPhase = "true";
doCheck = true;
checkPhase = ''
export ARBORICX_LIB=${tricuPhp}/lib/libarboricx.so
export LD_LIBRARY_PATH=${tricuPhp}/lib:$LD_LIBRARY_PATH
ulimit -s 32768
# Run PHP host against fixture bundles
php ext/php/run.php run test/fixtures/id.arboricx hello
php ext/php/run.php run test/fixtures/append.arboricx "Hello, " "world!"
php ext/php/run.php run test/fixtures/true.arboricx
php ext/php/run.php run test/fixtures/false.arboricx
php ext/php/run.php run test/fixtures/notQ.arboricx "t t t"
mkdir -p $out
echo "All PHP tests passed" > $out/result
'';
};
in { in {
packages.${packageName} = tricuPackage; packages.${packageName} = tricuPackage;
packages.default = tricuPackage; packages.default = tricuPackage;
packages.tricu-zig = tricuZig; packages.tricu-zig = tricuZig;
packages.tricu-zig-tests = tricuZigTests; packages.tricu-zig-tests = tricuZigTests;
packages.tricu-php = tricuPhp;
packages.tricu-php-tests = tricuPhpTests;
checks.${packageName} = tricuPackageTests; checks.${packageName} = tricuPackageTests;
checks.default = tricuPackageTests; checks.default = tricuPackageTests;
@@ -124,6 +176,7 @@
inputsFrom = [ inputsFrom = [
tricuPackage tricuPackage
tricuZig tricuZig
tricuPhp
]; ];
}; };