TypePHP: an ahead-of-time PHP compiler that has to bootstrap itself
Compile PHP to Native Binaries
At a glance
- What is it?
- Swoole's AOT compiler lowers PHP to C++17 and then to machine code, written in PHP and able to compile its own source into the tpc binary. The interesting engineering is in the two phase pipeline, the four output artifacts, and the two documented escape hatches for PHP semantics.
- Who is it for?
- TypePHP makes sense when you have numeric or container-heavy PHP, need a native artifact rather than a bytecode cache, or want your source shipped as a binary. It does not make sense as a drop-in replacement for an arbitrary PHP application, which is a claim the project itself declines to make, pointing instead at a dedicated file of incompatible features.
- Can I use it commercially?
- Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.
Star history
- swoole/typephp1,487 ★ · +91
| Repository | 2026-09-21 | 2026-11-14 |
|---|---|---|
| swoole/typephp | 1.4k | 1.5k |
91 Stars in the last 54 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
tpc compiles itself, and there is no C glue in the compiler
TypePHP is a compiler written in PHP that compiles its own source code into a native binary named tpc. The claim in the file is specific: the bootstrap chain is pure PHP, with no C or C++ glue in the compiler itself. That rules out the usual shortcut where a native runtime component is written in a lower-level language and the front end merely looks like PHP.
The practical consequence is that the very first build of tpc needs a working PHP toolchain, and every build after that can use the previous tpc. The stated requirements are PHP 8.4 or 8.5 as CLI plus development headers and php-config, the matching PHP embed library (libphp.so or libphp.dylib) for binary and shared library builds on Unix-like systems, GCC 9 or newer or Clang with C++17, CMake 3.24 or newer, Composer 2, and the high precision math libraries GMP and MPFR, with libmpdec named in the same sentence before the file is cut off.
The release cadence says this is a project in motion rather than a finished tool. Three releases landed in eight days: v0.9.2 on 2026-09-22, v0.9.3 on 2026-09-24 and v0.9.4 on 2026-09-30, with the last push on the same day as the tag. The version line is still 0.9.x, the file describes the project as under active development, and the license is GPL-3.0, which is worth reading before you decide how to distribute artifacts.
The repository is laid out like a language project rather than a library: src/, tests/, a separate phpunit/ directory, docs/, examples/, benchmark/, wasm/, completions/ for the command line tool, plus cli.php, run-tests.php, package.php and a bump-version.sh script at the root.
The pipeline emits four artifacts while the feature list names three modes
The pipeline diagram in the project page ends with a list of four things it can produce:
PHP source + .stub.php declarations + optional C/C++ sources
│
▼
parse, validate, and collect declarations
│
▼
lower function bodies and constants to C++17
│
▼
native compiler + reusable object/PCH caches
│
▼
executable | PHP extension | shared library | WASI componentThe features section then names three native build modes: a bin executable, an ext loadable PHP extension, and a lib shared library from the same codebase. A WASI component appears in the diagram and in the cross-platform feature line, which also mentions browser output through a component runtime and native Android and iOS builds with the NDK and iOS SDK, but WASI is not one of the three named modes. The outputs are not symmetric: three of them are things you can load on a normal operating system and one is a component model target.
The input side explains more about how the compiler is meant to be driven. Alongside PHP source and optional C or C++ sources, the pipeline expects .stub.php declaration files, which is how internal functions and extension surface get described to the compiler rather than discovered at runtime. A precompiled header cache and a reusable object cache sit in front of the native compiler, so a second build of an unchanged dependency does not pay for headers again.
The comparison table draws the line against the two alternatives people usually ask about. Against an opcode cache it claims compile time full-program type-driven optimization where a cache has none, and against the PHP 8 JIT it claims the same optimisation where the JIT is described as limited and trace based. Both alternatives are marked as leaving bytecode reversible, so source protection is listed as a TypePHP-only row.
Two phases exist because constants need the whole symbol table first
The pipeline splits compilation into a prepare phase and a convert phase, and the reason given is ordering rather than parallelism. The prepare phase builds the complete symbol model without allocating runtime cache identifiers. Constants and declaration defaults keep their syntax tree until the convert phase, where they are lowered once every symbol in the project is known.
That is the whole design decision in one sentence: a constant can only be emitted after the compiler knows what everything it refers to is called and typed. Lowering earlier would mean guessing and patching later. The stated payoff is determinism across multi-file projects, and specifically across the self-hosted build, which is the harder case, because when the compiler compiles itself a symbol in the compiler's own source can be referenced by the code being compiled.
The consequence for anyone extending the compiler is that a new declaration type has to be split across the same seam. Its symbol has to be registered in prepare, and its value has to be lowered in convert, or the ordering guarantee breaks. Anyone reading the source will find the phase boundary as a set of paired entry points rather than a flag.
The generated code is C++17, and the type mapping is the narrow, useful part of it: int, float and bool go straight to int64_t, double and bool, so an inferred numeric local gets native C++ storage instead of a boxed PHP value. High precision arithmetic is a separate set of types, with bigInt backed by GMP, decimal by libmpdec and bigFloat by MPFR, each with typed operators and method APIs rather than a generic conversion path.
A native binary still needs libphp, PHPX and its native libraries beside it
One of the strongest claims in the comparison is native process entry: binary mode starts directly from a native executable and does not require the PHP CLI or a separate interpreter process. The same bullet then qualifies it, because the executable still embeds or links PHPX, libphp and any configured native libraries, all of which must be present in the deployment package.
So the win is process shape, not a self-contained file. You ship one binary instead of a script plus an interpreter, and you avoid interpreter startup, but you inherit a deployment bundle whose contents have to match the build. That is the same obligation the requirements list creates with the embed library, which is needed on Unix-like systems specifically for binary and shared library builds.
The reason dynamic behaviour still works at all is PHPX. Dynamic PHP values, internal functions, reflection and object metadata continue to interoperate with the Zend runtime through it, while user functions that have been compiled are no longer executed as Zend opcodes. That split is the architecture: your own functions become native code, and the parts of PHP that are inherently dynamic stay in the engine. Anyone reading a performance claim should read it against that boundary, because a program dominated by internal function calls and reflection is leaning on the side that did not compile.
The extension mode takes the other exit. It loads as a standard PHP extension, and a project can call supported internal functions and require other Zend extensions explicitly, so it fits an existing PHP deployment instead of replacing it. Shared library mode sits between the two, reusing code across a host process rather than producing something you launch.
Strict calls are the default, so the strict_types declare is dead weight
TypePHP never enables PHP's weak scalar coercion, which makes declare(strict_types=1) unnecessary in a compiled file. That is a real behavioural difference from an interpreter and it is stated as unconditional rather than as an option, so porting code that relies on implicit conversion will surface as a compile or runtime failure instead of silently changing behaviour.
There are exactly two escape hatches named, and both are narrow. For a single dynamic value inside otherwise strongly typed code, std::any() is the escape. For a file that needs PHP integer widening, use varint_types. The first is a value-level opt out, the second is a file-level change of integer strategy, and the file also has toObject(), toInt(), toString() and toArray() as compile-time conversion helpers rather than runtime calls.
The rest of the type surface follows the same philosophy. std::object() and std::ref() cover object handling and references, and std::expected() with std::unexpected() provide an error channel that is a type rather than a thrown value. Attributes do the rest at compile time: Immutable declares a read-only contract, StdList and StdDict carry array shape metadata, and Getter, Setter, With, Constructor, Printer and Arrayable generate typed methods from property declarations. Both groups are checked at compile time with no runtime cost, according to the file, which is the argument for using them instead of writing the same checks by hand.
The subset is defined by a file that names what is left out
The project is explicit about not being a drop-in replacement. It supports a defined, testable subset of PHP rather than claiming compatibility with every dynamic program, and it points readers to a compatibility model section and to a dedicated file of incompatible PHP features before adopting it for an existing application. The repository ships a docs directory with an English documentation tree, and the incompatible-feature list is one of the files inside it.
That framing matters more than usual for a compiler that keeps PHP syntax. The syntax you already know is the selling point, but the semantics behind it are where the subset bites, and the file exists precisely because the boundary cannot be inferred from the syntax. A migration plan that reads only the feature list will underestimate the work.
The examples directory shows the shape of what is expected to work. There are attribute examples, bigint examples, C interop examples, an android-native and an apple-native directory, an appx directory, array and array loop and array append and array get files, autoload_class, a binary tree, a call stack sample, a closure sample, concatenation, several constant samples including a new constant syntax and a constant C array, and a check_parser_api script. It is a broad surface for a 0.9 release, and the presence of both a parser API probe and a cases directory suggests the example set doubles as the compatibility test surface.
Feature coverage extends past PHP itself in one direction worth naming: a Python bridge that generates IDE helpers for Python modules and converts Python scripts to TypePHP, plus a universal methods feature that lets you call methods directly on primitives, so a string can be uppercased and an array asked whether it contains a value, with statically known calls resolved at compile time.
Three linters for three languages sit at the repository root
The root of the repository shows how many languages a single command touches. There is a phpstan.neon for static analysis of the PHP sources, a .php-cs-fixer.dist.php for PHP code style, and a .clang-format for the C++ that the compiler emits. A project that only wrote PHP would not need the third, and the fact that formatting rules for generated C++ sit beside rules for handwritten PHP is a reasonable summary of what this compiler is.
Testing has the same shape. There is a phpunit.xml, a tests/ directory, and a separate phpunit/ directory that appears to vendor the test runner itself. A run-tests.php script sits next to a cli.php entry point and a package.php, and two workflow definitions, project.yml and project-release.yml, cover continuous integration and releases separately. A completions/ directory ships shell completions for the command line, and bin/ holds the executable stub. The version bump is a shell script, bump-version.sh, sitting beside a CHANGELOG.md and a composer.json.
One file does not belong to the toolchain at all: swoole-logo.ico and swoole-logo.svg, a Windows icon alongside the vector logo. The repository is under the swoole organisation and the project page lives at swoole.com/aot, so the branding is inherited rather than incidental, which is a useful signal when you are deciding how tightly the two projects are coupled.
The scale is worth noting too, because it bounds the confidence you can have. The repository shows 1,422 stars, 80 forks and 1 open issue at the time of writing, against a version line at 0.9.4. A single open issue on a project this young usually means either a very clean tracker or a very small user base reporting into it, and the project page points at an incompatible-feature file rather than an issue tracker for compatibility questions.
Editorial conclusion
TypePHP makes sense when you have numeric or container-heavy PHP, need a native artifact rather than a bytecode cache, or want your source shipped as a binary. It does not make sense as a drop-in replacement for an arbitrary PHP application, which is a claim the project itself declines to make, pointing instead at a dedicated file of incompatible features. Before adopting it, read that file against your codebase, confirm you can ship libphp, PHPX and the GMP, MPFR and libmpdec libraries in your deployment package, and test the std::any and integer widening escape hatches on the code paths you cannot type. Version 0.9.4 moved on 2026-09-30, so the compatibility surface is still moving.
Frequently asked questions
What is TypePHP?
An ahead-of-time compiler for PHP from the swoole organisation, under GPL-3.0. It lowers PHP source to C++17 and then to native machine code, producing an executable, a loadable PHP extension or a shared library, and it is written in PHP well enough to compile its own source into the tpc binary.
Does TypePHP replace the PHP interpreter at runtime?
Not entirely. Compiled user functions no longer run as Zend opcodes, but dynamic values, internal functions, reflection and object metadata still interoperate with the Zend runtime through PHPX. A binary-mode executable also still links PHPX, libphp and any configured native libraries, so those must ship with it.
Can I run my existing PHP application through TypePHP unchanged?
The project does not claim that. It says it intentionally supports a defined, testable subset of PHP rather than drop-in compatibility with every dynamic program, and points to a compatibility model section and a dedicated incompatible-features document to read before adopting it for an existing application.
What do I need installed to build TypePHP?
PHP 8.4 or 8.5 as CLI with development headers and php-config, the matching PHP embed library (libphp.so or libphp.dylib) for binary and shared library builds on Unix-like systems, GCC 9 or newer or Clang with C++17, CMake 3.24 or newer, Composer 2, and the GMP and MPFR math libraries.
How does TypePHP compare with OPcache or the PHP JIT?
The project page frames it as a different category rather than a faster cache. TypePHP targets native machine code with compile-time, full-program type-driven optimisation and can emit a native executable, while an opcode cache targets bytecode with no type-driven optimisation and a JIT targets trace-based machine code with limited optimisation. Both alternatives are listed as leaving bytecode reversible.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/swoole-typephp)Star history chart for your README
[](https://hysenlabs.com/projects/swoole-typephp)Was this useful?