TypePHP compatibility: what a native build changes

Compare real PHP and compiled TypePHP results: null array keys, entry points, strict typing, runtime dependencies and the limits of a pinned compatibility test.
From PHP source to a native artifact
TypePHP lowers PHP source to C++ and compiles that code into a native artifact. That introduces two separate questions: whether a program can be compiled, and whether the compiled program preserves the behavior its callers depend on. Successful compilation does not answer the second question.
This review uses TypePHP revision c481e06f3ca8f430488022ff37e712ea961082ff, whose compiler reports version 0.9.4. The compatibility probe uses PHP 8.5.11 ZTS and PHPX 2.9.3 on Linux ARM64. The observations below come from running the PHP-hosted bootstrap compiler and its native output. They do not establish self-hosted compiler behavior, framework compatibility or a performance gain.
Native code still has runtime dependencies
The pinned README describes compiled user functions alongside PHPX/Zend interoperability for dynamic values, internal functions and object metadata. The normal executable mode uses the PHP Embed runtime. In this probe, the link command includes libphpx, libphp, GMP and MPFR. Shipping the executable therefore also requires a compatible native runtime environment.
The README also allows embedded, uncompiled PHP dependencies to execute through the Zend VM. Calling an artifact native does not establish that every dependency has become native machine code. Inspect the chosen mode and actual dependency path before making that claim about an application.
Nano is a distinct documented build option. It composes its runtime from PHP Nano and PHPX sources without linking the host's libphp, but excludes capabilities including networking, dynamic PHP loading and process execution at this revision. Those restrictions matter for a program that fetches a URL or launches another command. This probe does not build Nano.
A null array key changes the output shape
The null-key.php fixture defines a global main(): void, creates $items = ['first'], assigns $items[$nullKey] = 'second' with $nullKey = null, and prints JSON. The PHP baseline includes that exact file and calls main(). The TypePHP build uses the function as its executable entry point.
PHP 8.5.11 prints {"0":"first","":"second"} and emits a deprecation notice for using null as an array offset. TypePHP prints ["first","second"]. The first result contains an empty-string key. The second appends a numbered element, leaving a consecutive list that JSON encodes as an array.
This difference can affect a caller even when both executions succeed. A consumer expecting an object property named "" will not receive that property from the compiled result. An assertion that checks only the element count would miss the change. Compare keys and encoded shape as well as values.
The companion explicit-keys.php chooses both operations explicitly: $items[''] = 'second' followed by $items[] = 'third'. Both runtimes print {"0":"first","":"second","1":"third"} for that case. This is a verified alternative for the demonstrated intent, not evidence that every array operation is interchangeable. Validate nullable keys at the application's input boundary according to its own data contract.
<?php
function main(): void
{
$nullKey = null;
$items = ['first'];
$items[$nullKey] = 'second';
echo json_encode($items), "\n";
}
<?php
function main(): void
{
$items = ['first'];
$items[''] = 'second';
$items[] = 'third';
echo json_encode($items), "\n";
}
Some programs fail before native compilation
The global-code.php fixture consists of a top-level echo. Ordinary PHP runs it. TypePHP rejects it with Unsupported statement: Stmt_Echo. Moving the work into the global entry function changes the program's execution structure. A framework bootstrap that runs statements while loading files needs a more careful assessment than changing a filename or install command.
The weak-types.php fixture has a valid entry function but declares strict_types=0. PHP runs it when the baseline invokes main(). TypePHP rejects the declaration with declare(strict_types=0) is not allowed. This establishes the explicit weak-mode rejection, not the behavior of every scalar conversion or dynamic call. The pinned incompatibility document supplies further restrictions. Each relevant one needs an application-specific test.
<?php
echo "global execution\n";
<?php
declare(strict_types=0);
function main(): void
{
echo "weak mode\n";
}
Evaluate the boundary your application actually uses
I recommend starting with a replaceable operation whose inputs, outputs and errors can be asserted in both implementations. Include missing fields, nullable values and rejected inputs, not just a successful calculation. Keep the PHP implementation available until those results agree with the intended contract or each deliberate difference has been accepted by its callers.
The fixture compares four original programs and two repaired variants. The repaired variants move the top-level echo into main and change strict_types=0 to strict_types=1. Both then compile and print the same output as PHP. These controls distinguish the intended restrictions from a generally broken compiler environment. It records the source hashes, compiler output, runtime versions and native package versions. Execution has no external network access and runs as an unprivileged user in disposable containers. Dependency retrieval occurs during the image build. The Composer dependencies are locked. The native toolchain uses a signed Debian package snapshot dated 12 October 2026. This fixes the historical test environment, not a production security-update policy.
Download the complete fixture below, extract it, and run python3 verify.py with Docker running. The runner builds the pinned environment and prints JSON evidence. The archive includes the source, dependency lock, build recipe and captured results. The build needs network access to retrieve dependencies. Each comparison runs without external networking, with a read-only root and bounded time, memory, processes and output. The PHP and compiler containers hash the source they mount before executing it. No application checkout, credentials or Docker socket is mounted.
The result establishes these six cases on the recorded architecture. It does not test arbitrary Composer packages, PHP extensions, HTTP request handling or deployment of the generated binaries. Keep those as separate acceptance tests for the application you intend to move.
No timing result is reported. A performance experiment would additionally need equivalent validation and outputs, repeated measurements, and separate startup and sustained-workload results. These compatibility results give a concrete reason to establish correctness before interpreting a speed comparison.
Reproduce the examples
Four exact PHP sources, native/PHP comparisons, two repair controls, pinned build recipe, dependency lock and recorded evidence. Requires Docker and Python 3; no benchmark or framework migration claim.
Download the source and tests (.zip)
