commit 42f5dbaff9d4ee36310cad7adeb67fa4b39d5d64
parent 47bdc3b4fa6df858d7d3e5024ed821783ff4be58
Author: James Eversole <james@eversole.co>
Date: Mon, 11 May 2026 13:07:35 -0500
Simple PHP web demo
Diffstat:
7 files changed, 186 insertions(+), 83 deletions(-)
diff --git a/ext/php/public/eval.php b/ext/php/public/eval.php
@@ -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();
+}
diff --git a/ext/php/public/index.php b/ext/php/public/index.php
@@ -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>
diff --git a/ext/php/run.php b/ext/php/run.php
@@ -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);
}
diff --git a/ext/php/src/common.php b/ext/php/src/common.php
@@ -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;
+}
diff --git a/flake.nix b/flake.nix
@@ -111,7 +111,7 @@
buildPhase = "true";
installPhase = ''
mkdir -p $out/share/tricu-php $out/lib $out/bin
- cp -r src run.php $out/share/tricu-php/
+ cp -r src public run.php $out/share/tricu-php/
cp ${tricuZig}/lib/libarboricx.so $out/lib/
cp ${tricuZig}/include/arboricx.h $out/share/tricu-php/
@@ -168,7 +168,6 @@
haskellPackages.ghcid
customGHC
upx
- zig
gcc
python3
];
diff --git a/notes/php-cli-run-flags.md b/notes/php-cli-run-flags.md
@@ -1,9 +1,18 @@
# PHP Recommended Run Flags
```php
-php -d memory_limit=4G \
- -d opcache.enable_cli=1 \
+php -d opcache.enable_cli=1 \
-d opcache.jit_buffer_size=256M \
-d opcache.jit=tracing \
ext/php/run.php run $PATH_TO_ARBORIX_BUNDLE $ARGS
```
+
+For bundle execution test server:
+
+```php
+nix build .#tricu-php
+ARBORICX_LIB=../../../lib/libarboricx.so php \
+ -S localhost:8081 \
+ -t ./result/share/tricu-php/public \
+ -d ffi.enable=true
+```
diff --git a/notes/recursive-consumers.md b/notes/recursive-consumers.md
@@ -2,7 +2,7 @@
## Rule
-Put consumed data first in recursive workers.
+Put consumed data first in recursive workers in `tricu` code.
*AVOID* this shape: