feat(php): use new FFI for Arboricx
This commit is contained in:
11
AGENTS.md
11
AGENTS.md
@@ -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
|
||||||
|
|||||||
218
ext/php/run.php
218
ext/php/run.php
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
138
ext/php/src/ffi.php
Normal 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;
|
||||||
|
}
|
||||||
@@ -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';
|
|
||||||
}
|
|
||||||
@@ -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
55
flake.nix
55
flake.nix
@@ -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
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user