feat(php): Simple web demo

This commit is contained in:
2026-05-11 13:07:35 -05:00
parent d37d443021
commit ea748b2e5e
7 changed files with 186 additions and 83 deletions

53
ext/php/public/eval.php Normal file
View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
error_reporting(E_ALL);
ini_set('display_errors', '1');
if (!extension_loaded('ffi')) {
http_response_code(500);
echo "Error: PHP FFI extension is not loaded.\n";
echo "If you are using the Nix build, run the included server script:\n";
echo " ./result/bin/tricu-php-server\n";
exit;
}
require __DIR__ . '/../src/common.php';
use function Arboricx\{ctx_init, ctx_free, loadBundleDefault, ofNumber, ofString, app, reduce, decode, findLib, readBundle};
header('Content-Type: text/plain; charset=utf-8');
try {
if (!isset($_FILES['bundle']) || $_FILES['bundle']['error'] !== UPLOAD_ERR_OK) {
throw new \RuntimeException('Bundle upload failed.');
}
$args = [];
for ($i = 0; $i < 5; $i++) {
$v = $_POST["arg$i"] ?? '';
if ($v !== '') {
$args[] = $v;
}
}
$libPath = findLib();
$ctx = ctx_init($libPath);
try {
$term = loadBundleDefault($ctx, readBundle($_FILES['bundle']['tmp_name']));
foreach ($args as $arg) {
$argTree = preg_match('/^\d+$/', $arg) ? ofNumber($ctx, (int)$arg) : ofString($ctx, $arg);
$term = app($ctx, $term, $argTree);
}
$result = reduce($ctx, $term, 1_000_000_000);
echo decode($ctx, $result);
} finally {
ctx_free($ctx);
}
} catch (\Throwable $e) {
http_response_code(500);
echo 'Error: ' . $e->getMessage();
}

30
ext/php/public/index.php Normal file
View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Arboricx Web</title>
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
</head>
<body>
<h1>Arboricx Bundle Runner</h1>
<form hx-post="eval.php" hx-target="#result" enctype="multipart/form-data">
<p>
<label>Bundle (.arboricx)<br>
<input type="file" name="bundle" accept=".arboricx" required></label>
</p>
<?php for ($i = 0; $i < 5; $i++): ?>
<p>
<label>Arg <?= $i + 1 ?> <small>(ignored if empty)</small><br>
<input type="text" name="arg<?= $i ?>"></label>
</p>
<?php endfor; ?>
<p>
<button type="submit">Run</button>
</p>
</form>
<pre id="result"></pre>
</body>
</html>

View File

@@ -11,89 +11,16 @@ declare(strict_types=1);
* php run.php inspect <bundle.arboricx>
*/
require __DIR__ . '/src/ffi.php';
require __DIR__ . '/src/common.php';
use function Arboricx\{ctx_init, ctx_free, loadBundleDefault, ofNumber, ofString, app, reduce, toString, toBool, toNumber};
// ── Locate libarboricx.so ──────────────────────────────────────────────────
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)';
}
}
}
}
use function Arboricx\{ctx_init, ctx_free, loadBundleDefault, ofNumber, ofString, app, reduce, toString, toBool, toNumber, findLib, decode, decodeType, readBundle};
// ── Commands ─────────────────────────────────────────────────────────────────
function readBundle(string $path): string
function bail(string $msg): void
{
if (!file_exists($path)) {
fwrite(STDERR, "Error: bundle not found: $path\n");
exit(1);
}
$bytes = file_get_contents($path);
if ($bytes === false) {
fwrite(STDERR, "Error: could not read bundle: $path\n");
exit(1);
}
return $bytes;
fwrite(STDERR, "Error: $msg\n");
exit(1);
}
function cmdRun(string $libPath, string $bundlePath, array $args): void
@@ -109,6 +36,8 @@ function cmdRun(string $libPath, string $bundlePath, array $args): void
$result = reduce($ctx, $term, 1_000_000_000);
echo decode($ctx, $result) . "\n";
} catch (\Throwable $e) {
bail($e->getMessage());
} finally {
ctx_free($ctx);
}
@@ -131,6 +60,8 @@ function cmdInspect(string $libPath, string $bundlePath): void
$value = '(raw tree)';
}
echo " Type: $type\n Value: $value\n";
} catch (\Throwable $e) {
bail($e->getMessage());
} finally {
ctx_free($ctx);
}

81
ext/php/src/common.php Normal file
View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Arboricx;
require __DIR__ . '/ffi.php';
use function Arboricx\{ctx_init, ctx_free, loadBundleDefault, ofNumber, ofString, app, reduce, toString, toBool, toNumber};
function findLib(): string
{
$env = getenv('ARBORICX_LIB');
if ($env !== false && file_exists($env)) {
return $env;
}
$paths = [
__DIR__ . '/../../zig/zig-out/lib/libarboricx.so',
__DIR__ . '/../libarboricx.so',
'/usr/local/lib/libarboricx.so',
'/usr/lib/libarboricx.so',
'./libarboricx.so',
];
foreach ($paths as $p) {
if (file_exists($p)) {
return $p;
}
}
throw new \RuntimeException('libarboricx.so not found. Set ARBORICX_LIB to its full path.');
}
function decode(\FFI\CData $ctx, int $root): string
{
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)';
}
}
}
}
function readBundle(string $path): string
{
if (!file_exists($path)) {
throw new \RuntimeException("bundle not found: $path");
}
$bytes = file_get_contents($path);
if ($bytes === false) {
throw new \RuntimeException("could not read bundle: $path");
}
return $bytes;
}