Repository navigation
Releases: manticorephp/compiler
Release list
v0.15.0
docker run --rm -v "$PWD":/work -u "$(id -u):$(id -g)" \
ghcr.io/manticorephp/compiler:0.15.0 manticore compile app.php -o app
curl -fsSL https://github.com/manticorephp/compiler/releases/download/v0.15.0/install.sh | bash
The tarballs carry the compiler and its stdlib, not a toolchain: manticore
assembles its IR with clang and links with cc, against the host's
pcre2, openssl, sqlite3 and curl. install.sh checks for those and says
which are missing. The image carries the toolchain it was built with.
On Alpine (musl) use ghcr.io/manticorephp/compiler:0.15.0-alpine or the linux-musl tarball —
install.sh picks it by itself.
Built cold from v0.15.0 — no cached compiler took part.
v0.15.0 compiles and serves a Symfony application natively, adds ext/bcmath, a
much larger ext/openssl and operator overloading, and gives the CLI an
upgrade, a doctor and build progress. A performance sweep over ~150 string,
array, SPL, date and regex functions removes the quadratic cases and brings 109
of 151 measured functions within 1.2x of php 8.5. Memory ABI is now v22.
Highlights
A Symfony app, native
examples/symfony-appbuilds to one binary that serves/(200) and an HTML
404 underHttp\Server, in prod and in dev, including a cold start with no
var/.examples/symfony-consoleruns.GET /on macOS arm64 withwrk: 1 worker 8.9k -> 15.8k rps, 8 workers
56k -> 83k rps. Linux (Docker VM): ~14k rps per worker, ~100k on 8.- The request path no longer leaks: a 404 grew 1.7 KB per request, now ~60 B.
Causes: property stores through an erased receiver,+and[...$a]retaining
a key twice,array_pop/array_shiftresults, in-place closure envs, and a
cycle collector that did not count freed shells. - Dev boot needed reflection: parameter default factories for trait-imported,
interface and prelude-class methods,self::X/parent::Xdefaults evaluated
in the declaring class, class constants through every extended interface,
ReflectionProperty::setValue()onException/Error.
Performance sweep (tools/bench/basic_fns_sweep.php)
| Case (80k) | Before | After |
|---|---|---|
array_diff / array_intersect / *_key |
1.1 – 15 s | 7 – 11 ms |
ArrayObject / ArrayIterator foreach |
1.7 s (20k) | 0.8 ms |
fwrite to php://memory (40k) |
120 ms | 0.9 ms |
.= through a string &$s parameter (40k) |
12.7 ms | 0.4 ms |
array_shift / array_unshift loop |
x17 | x3.7, O(1) amortized |
similar_text, periodic input |
770 ms | 4 us |
| call by runtime name | 22 us | 5 ns (inline cache) |
Also 5-150x faster: str_repeat, array_unique, strrev, base64/hex,
serialize/unserialize, in_array/array_search, ksort/asort, sort
(native stable merge sort), array_slice/reverse/merge, range, date(),
str_replace, preg_* (PCRE JIT, in-place ovector). WebSocket applyMask
74 us -> 5.8 us. Still >= 2x slower than php: base64 (needs SIMD),
nested_arr_read_modify, http_build_query, str_ireplace, ArrayObject,
strtotime.
Smaller IR, faster builds
- Dynamic-call-by-name thunks (
$cb(...),callable) are emitted once inlib/manticore_stdlib.oand exported, instead of rebuilt in every program. On 19 sampled cases IR drops 54.9 -> 41.2 MB and compile wall 52.8 -> 34.3 s;lib/manticore_stdlib.ogrows 7.2 -> 8.9 MB. Hello world is byte-identical. CIlinux-amd64suite: 91 -> 42 min.
CLI
manticore upgrade [--check] [--version X]: checksummed, staged swap with
rollback, previous binary kept asbin/.manticore.prev; package-manager and
container installs are refused.--checkexits 0 current / 1 update / 2 error.manticore doctor [--offline] [--no-smoke]:[ OK ]/[WARN]/[FAIL]for
clang >= 15,cc, stdlib, pcre2/openssl/zlib, a hello-world compile and run,
update availability.manticore completions bash|zsh|fish, generated from the command metadata.compile/buildprogress:--progress=auto|live|plain|json|off,--quiet,
--timings,--color. Piped output stays silent unlessCIis set; stdout is
never touched.- Arg-parse errors read
<command>: <error>(exit code still 64).
New extensions and language features
ext/bcmathin pure PHP: all 14 functions, byte-identical to Zend on 58k fuzz
cases;BcMath\Numbermatches methods, scales and messages (30k fuzz cases).ext/opensslover libcrypto, ~85% of functions: ciphers (CBC, CTR, GCM, CCM,
OCB, chacha20-poly1305), digests, keys (RSA, EC, DSA, DH, Ed25519, X25519,
Ed448, X448), sign/verify, x509, CSR, SPKAC, seal/open, CMS, PKCS#7/#12.
Not done:openssl.cnf-driven options.- Operator overloading (superset):
Manticore\Overloadablewith__operator/
__comparefor+ - * / % ** <=> == != < <= > >=, unary minus,+=,++/--
on a statically typed operand. Operators on an erased operand are not
dispatched yet (#263). - Stdlib: SPL iterators (
LimitIterator,CachingIterator,RegexIterator, ...),
ext/random(Randomizer,Mt19937,Xoshiro256StarStar,Pcg*; seeded
sequences match Zend),ext/filter,crypt/password_*,hash_pbkdf2/
hash_hkdf,str_increment, quoted-printable, uuencode,soundex,
metaphone,fpow,log1p/expm1/asinh/acosh/atanh,error_clear_last,
forward_static_call,get_error_handler, and more. matchwithout a default arm throwsUnhandledMatchError.- Readonly classes: dynamic properties and
unsetthrowErrorlike Zend.
Fixes
About 110 bugs, mostly found by the sweeps and the Symfony work:
- Coercion failures throw instead of coercing silently (#188); 39 bugs in eval
order, lifetimes, numerics, raw-int erasure and TypeDef/GC (#247); 10 more in
voidvar_dump, enum array cast, callable slots, by-refstdClass
properties and generatorvalid(#250). - Int/bool arguments to
floatparameters convert on every call path (#228). - A by-value closure capture written through (
$l[] = 1,array_pop($l)) was
released twice at exit (#275). addslashesis binary safe (#279);hex2binrejects odd and non-hex input
(#281); numeric strings compare as numbers in<andsort(#283, #308).array_merge(...$l)emptied the source's inner arrays (#309); a barearray
parameter written with another element type stored raw pointers (#307);
array_shift/array_popleaked and kept destructors waiting (#314, #315).printf/sprintfformat non-finite floats asNaN/INF(#266); a literal
printf('%f', NAN)stays on the inlinesnprintfpath and printsnan.ReflectionFunction,ReflectionPropertyand trait-method reflection gaps
(#287, #288, #289); first dev boot segfault inDeepClone::getHydrator(#290).Dstyped arrays: inline bitwise stores andTypedArray::apply()
(28 -> 6.6 ns/element).
Compatibility and upgrade notes
- Memory ABI v22.
array_shift/array_unshiftslide the array header
(ARRAY_HEAD_GAP_OFFSET). Rebuild programs and any.obuilt with an older
compiler. No bootstrap pin raise is needed. COMPILER_VERSIONis the single version source;tools/check_version.sh
enforces it in CI and the release workflow.BOOTSTRAP_VERSIONstays 0.14.0.- PCRE JIT is new and used when the host PCRE2 has it; without JIT, matching
falls back to the interpreter silently. - Where Zend warns and carries on, the new functions throw
(ini_parse_quantity,convert_uudecode, openssl on junk certs/keys).
Known issues
- JSON and XML descriptor output in
symfony-consolediffers from Zend (#296,
#297). - The lenient cell-to-string cast does not raise php's
TypeError(#332, partial). - Dynamic dispatch on an untyped receiver is fixed narrowly for
bindTo(#292). forward_static_calldoes not forward the late-static-binding class.- Open bugs found during the sweep: #319-#325, #329.
Verification
- CI on
fe5476b4: linux-arm64, linux-amd64, alpine-arm64, alpine-amd64, native-runner, bootstrap-from-pin, version-source green; macOS arm64 2221/2224 with one load flake (http_workers, passes alone 3/3). - Local macOS arm64 on the pre-#335 tree:
tests/aot/run.sh -j 02220 passed;xfail.sh0 XPASS. - Not run: difftest, selfhost fixpoint, full local Linux gate.
Full log: v0.14.0...v0.15.0
v0.14.0
docker run --rm -v "$PWD":/work -u "$(id -u):$(id -g)" \
ghcr.io/manticorephp/compiler:0.14.0 manticore compile app.php -o app
curl -fsSL https://github.com/manticorephp/compiler/releases/download/v0.14.0/install.sh | bash
The tarballs carry the compiler and its stdlib, not a toolchain: manticore
assembles its IR with clang and links with cc, against the host's
pcre2, openssl, sqlite3, curl and zlib (and ICU, for a program that uses intl).
install.sh checks for those and says which are missing. The image carries
the toolchain it was built with. On Alpine (musl) use
ghcr.io/manticorephp/compiler:0.14.0-alpine or the linux-musl tarball —
install.sh picks it by itself.
v0.14.0 adds Manticore\Ds\Map, Set and Vec — native, insertion-ordered
containers with strict keys — and rewrites JSON on a native encoder and decoder
that follow php's grammar and error codes. ext/zlib now runs on the host libz,
and Http\Server is tuned for real load. The compiler now bootstraps from a
pinned release instead of a Zend seed, so building from a clean clone needs no
php. A memory bug that glibc hid and musl exposed — a nested array literal freed
while the callee still held it — is fixed at its root, which also clears the
last open Alpine failure.
Highlights
Native containers: Map, Set, Vec (superset — docs/ds.md)
Manticore\Ds\Map<K,V>andSet<T>: insertion-ordered hash containers with
strict keys. Keys areint,stringorobject(by identity);1and"1"
are two different keys, and anything else is aTypeError.
An absent key throwsOutOfBoundsException.Vec<T>: a growable list over the native cell buffer.foreachover a Map, Set or Vec walks the storage directly, with no generator
frame.each,map,filter,reduce,any,allandfindare available,
and a captureless literal closure passed to them is fused into the loop.- Run under php through a polyfill (
tools/ds_polyfill.php), so the same program
can be checked against Zend. - 1M insert + lookup + remove on macOS arm64: string keys 270 ms against 284 ms for
a php array, int keys 85 ms against 82 ms, 1M object ids in aSet<object>
93 ms against 99 ms. The point is strict keys and identity keys at array speed,
not a faster array.
JSON on a native runtime
json_encode,json_decodeandjson_validatestay native for every flag
(JSON_THROW_ON_ERROR,JSON_PRETTY_PRINT,JSON_UNESCAPED_*,JSON_HEX_*,
JSON_PARTIAL_OUTPUT,JSON_BIGINT_AS_STRINGand the rest). A call with any
flag used to fall back to compiled PHP at about three times php's time.- The decoder follows php's grammar and error codes (syntax, control character,
UTF-8, UTF-16, depth, state mismatch), canonicalises int keys and turns big
integers into floats or strings as php does. - Objects encode through a class descriptor walker instead of a per-object map: the
json_objectsbenchmark went from 0.14 s to 0.08 s, 4.4 times php. UInt64Arrayencodes values at or above 2^63 exactly.
ext/zlib over the host libz
gzencode,gzcompressand friends call the system zlib, so they produce php's
bytes.gzencodeis about ten times faster than the previous pure-PHP encoder.
Http\Server under load
- A listener honours the stream context's
socket.backlog(php's default is 32;
it was a fixed 16), andHttp\Serverlistens with 511, so a burst of connects no
longer overflows the queue into resets.stream_context_set_optionsis added. - Server defaults fit real load: no keep-alive request cap, 4096 connections per
worker, and a worker yields after each accept so prefork workers share a burst
instead of the first one taking it all.
Bootstrap without php
- The compiler builds from the release named in
BOOTSTRAP_VERSION, fetched and
checksummed bytools/fetch_bootstrap.sh.bin/build, CI, the release workflow,
the Dockerfiles andinstall.shall start there. The Zend cold seed and its
tooling are removed, and a clean clone needs no php. A newbootstrap-from-pin
CI job builds the tree from the pin alone.
Language & runtime
- A nested array literal passed to a callee that keeps it (
$this->rows = $r, a
global, a static) is no longer released after the call. The inner arrays used to
be given back while the kept buffer still listed them. glibc masked the freed
read; musl faulted, which is howarray_eq_nested_lists_null_rowsfailed on
Alpine. Constructor, method, static and function calls are all covered. - A narrow
#[TypeDef]representation is enforced where a value is created: an
out-of-range value is aValueErrorinstead of a silent truncation, andf32
rounds to binary32. ReflectionFunctionreflects aClosure, including first-class-callable
closures, parameter attributes and defaults, andgetClosureThis(). A dynamic
closure call boxes an object argument, and a spread into a closure pads omitted
optional parameters. symfony/console 7.4 runs.foreachover an array of cells co-owns its value, so a key write in the loop
copies instead of mutating the source's element in place or freeing a buffer the
array still holds. This was heap corruption in the native php-cs-fixer fork, which
reported 28 bogus files on a symfony slice.- Numeric-string keys are canonicalised on dynamic array set,
issetand by-ref
element paths, andArrayAccessoffsets reachoffset*()as written. - A string-keyed array hands its keys out as
int|string, as php canonicalises
numeric-string keys at the store.
v0.13.0
docker run --rm -v "$PWD":/work -u "$(id -u):$(id -g)" \
ghcr.io/manticorephp/compiler:0.13.0 manticore compile app.php -o app
curl -fsSL https://github.com/manticorephp/compiler/releases/download/v0.13.0/install.sh | bash
The tarballs carry the compiler and its stdlib, not a toolchain: manticore
assembles its IR with clang and links with cc, against the host's
pcre2, openssl, sqlite3 and curl (and ICU, for a program that uses intl).
install.sh checks for those and says which are missing. The image carries
the toolchain it was built with. On Alpine (musl) use
ghcr.io/manticorephp/compiler:0.13.0-alpine or the linux-musl tarball —
install.sh picks it by itself.
v0.13.0 rebuilds memory ownership and adds native data structures. Ownership of
locals is now decided per program point by a dataflow pass, generators own what
they hold, exceptions unwind at zero cost, and the destructors still owed run at
shutdown. On top of that, Manticore\Ds brings fixed-width typed arrays over a
native buffer, SplFixedArray moves onto the same buffer, and the SPL heaps and
queues are added or rewritten with php's behaviour. Array lookups got faster, and
every open bug now lives as a runnable repro next to its GitHub issue.
Highlights
Native containers: Manticore\Ds (superset — docs/ds.md)
- Typed arrays over one native buffer runtime:
Int8/16/32/64Array,
UInt8/16/32/64Array,Float32/64Array,BitArray, andByteBuffer(typed,
endianness-aware reads and writes at a byte offset). - Growable (
push(),pop(),$a[] = $v) with whole-arraysum(),min(),
max(),equals(); values out of an element's range throwValueError, never wrap. - Bound to an element type through the usual generics docblocks:
/** @var UInt16Array<TokenKind> */stores 2 bytes and reads back as the
#[TypeDef]class, methods included. A binding the array cannot hold is a compile error. - For a receiver in a local, a property or a static property,
$a[$i],
$a[$i] = $vand$a[] = $vare an inline bounds check plus a load or store, and a
counted loop loads the buffer and its length once. 10M get/set on anInt32Array
in a property: 168 ms → 6.4 ms (array<int,int>: ~39 ms); 10M appends: 27 ms
(array: 51 ms); a 2M-readgetUInt32scan is ~60× faster thanunpack().
SPL with php's behaviour
SplFixedArraystores its elements in the native buffer (no PHP array underneath).SplHeap,SplMinHeap,SplMaxHeap,SplPriorityQueue— new, with Zend's sift
order, corruption protocol, dump and serialization.SplDoublyLinkedList,SplQueue,SplStackrewritten: O(1)shift/dequeue,
LIFO offsets,add(), frozen stack / queue modes.- Both families were fuzzed against php's own classes (operations, exceptions,
var_dump,print_r,serialize, iteration) with no difference.
Ownership, rebuilt
- Locals are owned per program point (
OwnershipFlow): a join compensates with a
retain, drops sit before an overwrite and at every exit, a store into a container is
a move only when the local is dead after it. A representation mismatch at a join is
a build failure, never a silent wrong answer. - Property, static property, global and
catch-variable overwrites release the old
value; cell elements are owned by their hint; by-ref write-through checks the retype. - Generators own their params, locals, key, sent and return values; destroying a
suspended generator runs itsfinallyblocks first, as php does. - Zero-cost exceptions: landing pads and unwind tables,
finallyon every exit,
exception chaining when a throw leavesfinally. - Destructors still owed at shutdown run in creation order (statics, cycles,
exit()). - A WebSocket soak of 5000 connect/echo/close cycles grows RSS by 368 KB (7.4 MB at
3000 before). On the php-cs-fixer fork the symfony corpus peaks at 2.6 GB instead of
3.3 GB.
Faster arrays
- The bucket index keeps a 32-bit hash tag beside each entry: a probe skips mismatching
slots without loading the entry; string-keyed lookups are about 30% faster. issetand??probe once and evaluate base and key once, key first as php's
delayed fetch does; string-keyed reads,array_key_existsandunsetnormalise a
canonical int-string key the same way.- Backtrace push/pop are emitted inline, and split builds inline small callees across
parts.
Bugs live as runnable repros
- An open bug is a repro under
tests/aot/repro/{wrong,crash,leak}plus a GitHub issue;
tests/aot/xfail.shruns them on every CI push and reports a fixed one as XPASS.
tools/file_bug.shfiles the issue from the repro.
Language & runtime
json_encodehonoursJsonSerializableand emits a backed enum's value;
print_randvar_dumpshow object properties with php's:protected/
:"Class":privatemarkers;__unserializereceives int keys as ints.pack()/unpack()implement the float codese E d g G f.- Generics keep a
#[TypeDef]binding: short class names inFoo<Bar>resolve through
use, specializations are built over the TypeDef, and@return Trefines a
compatible native hint. - Fixed crashes on values held in
mixed: anArrayAccesselement write, an enum case
afterinstanceof, a float??. - The tokenizer follows Zend's number grammar and heredoc escapes;
proc_openwires a
stream resource onto the child's fd. IntlTimeZone::getIanaID()needs ICU 74, as in php; a program using intl now links
against older ICU.analyzereports front-end errors instead of answering "No errors found".
Compatibility
- Memory ABI v18. A library
.obuilt by an older compiler must be rebuilt;
bin/buildneeds no seed for the bump. Manticore\Dsis a superset with no php oracle.
Known gaps
- #110 a narrow
#[TypeDef]repr is not enforced on creation (an out-of-range value is
truncated in a narrow slot) — fix ready, next release. - #107
foreachover a typed array bound to a#[TypeDef]yields the plain scalar. - #106 a class with a
#[TypeDef]property makes anyvar_dump()a compile error. - #109
json_encodeof aUInt64Arraywrites values ≥ 2^63 as negative ints. - #67 nested-list
==still crashes on Alpine arm64 (musl); correct elsewhere. - Self-build peak RSS is ~12% above the pre-ownership baseline.
v0.12.0
docker run --rm -v "$PWD":/work -u "$(id -u):$(id -g)" \
ghcr.io/manticorephp/compiler:0.12.0 manticore compile app.php -o app
curl -fsSL https://github.com/manticorephp/compiler/releases/download/v0.12.0/install.sh | bash
The tarballs carry the compiler and its stdlib, not a toolchain: manticore
assembles its IR with clang and links with cc, against the host's
pcre2, openssl, sqlite3 and curl. install.sh checks for those and says
which are missing. The image carries the toolchain it was built with.
On Alpine (musl) use ghcr.io/manticorephp/compiler:0.12.0-alpine or the linux-musl tarball —
install.sh picks it by itself.
v0.12.0 adds Unicode: mbstring with every encoding Zend ships, and ext/intl over the
system ICU. It also adds a WebSocket server and client (ws:// and wss://, with
permessage-deflate) on top of Http\Server. Under async(), regular-file I/O and
getaddrinfo now run on a blocking-offload thread pool, and every async timer runs on the
monotonic clock. Alpine (musl) is now a supported target with its own images and tarballs.
php-cs-fixer now builds and runs natively. Most of the work behind that went into memory
ownership and into making the compiler faster.
Highlights
Unicode: ext/mbstring and ext/intl
- mbstring, pure PHP, takes every encoding Zend knows (names, aliases and single-byte
tables generated from Zend; UTF-16/32 and UCS-2/4 in PHP; CJK and stateful encodings via
the host iconv). Case mapping in everyMB_CASE_*mode, width,mb_detect_encoding
scored as Zend scores it, MIME-header / numeric-entity / HTML-entity / UTF-7 / Base64 /
QPrint / UUENCODE codecs. Fuzzed against Zend over 23 encodings.mb_ereg*not yet. - ext/intl is complete over ICU's C API:
Normalizer,grapheme_*,Collator,
NumberFormatter,Locale,Transliterator,IntlChar(62 methods, 666 constants),
IntlTimeZone,IntlCalendar,IntlDateFormatter,IntlDatePatternGenerator,
IntlBreakIterator, IDN,Spoofchecker,UConverter,ResourceBundle,
MessageFormatter,IntlListFormatter, the procedural twins and the intl error state. extension_loaded()answers true formbstringandintl. ICU is linked dynamically,
only into a program that uses intl; pkg-config finds it, including Homebrew's keg-only
icu4c.
WebSocket (Http\WebSocket)
- RFC 6455 server on
Http\Server(viaHttp\Response::takeoverandServer::onStop)
and an outbound client,Http\WebSocket\connect;wss://over thetls://listener
and client. - permessage-deflate (RFC 7692), all four parameters, context takeover; an inflation bound
answers 1009 before a decompression bomb is allocated. - Callback layer:
Handler+run()/handler()(onCloseexactly once, 1011 on a
throwing handler) andHubbroadcast. The client refuses CR/LF/NUL in headers and URLs. - Own test suite plus an Autobahn|Testsuite harness (
tools/autobahn.sh). Guide:
docs/websocket.md.
Async: blocking-offload pool and monotonic clock
- Under
async(), path-opened file I/O,file_get_contents/file_put_contents, the
statfamily,opendir/readdir/scandir,unlink/rename/mkdir/rmdirand
getaddrinforun on a small pool of native threads while the task parks: a slow file no
longer stalls the loop. PHP never runs on a pool thread; output is byte-identical. - The pool is lazy and per process;
MANTICORE_BLOCKING_THREADSsets its size (default 4,
1..64);Async\stats()gainsoffloaded/pool_threads. A pooled call costs about
10 µs; cancellation and starvation are documented indocs/async.md. - Deadlines, timers, stream timeouts and backoffs run on the monotonic clock: an NTP or
date -sstep no longer stalls or mass-fires them.awaitDeadlinedrops its waiter on
expiry/cancel (an unbuffered send could report delivery and lose the value).
Alpine / musl
- Alpine is a CI target on arm64 and amd64; releases ship
:<v>-alpine/:alpineimages
andlinux-musltarballs, andinstall.shpicks the musl tarball on a musl host.
musl surfaced three real memory bugs glibc had absorbed; all fixed.
Arithmetic errors, WeakMap
/,%andintdivthrowDivisionByZeroError;intdiv(PHP_INT_MIN, -1)and a
negative shift throwArithmeticError(they answered INF, 1, 0 or garbage). A shift of
64+ is 0 / -1;% -1no longer traps on x86_64.WeakMapandWeakReference: entries die with their object.
php-cs-fixer runs natively
- php-cs-fixer compiles and runs as a native binary; its 52 front-end type errors and five
parse gaps were fixed at their roots.@PSR12over Manticore's ownsrc/: 242 of 254
files byte-identical to Zend's output (was 203).@Symfony: 30.8 s to 26.1 s to 24.6 s. - On the way:
#[Overload], core interfaces declared in the prelude, and the SPL iterator
layer (IteratorIterator,RecursiveIteratorIterator, directory iterators,
SplDoublyLinkedList,SplObjectStorage, …, each checked byte-for-byte against Zend).
Language & runtime
Objects and classes
- Objects compare the way php compares them under
==,<=>, ordering,switch,sort,
maxandin_array. Private state counts, DateTime compares by instant, and enum cases
and closures compare by identity. New attributes control this:#[CompareKey],
#[CompareNone]and#[Uncomparable](seedocs/attributes.md).#[Struct]compares
field by field. (string)/echo/concat of an object without__toStringthrows php's Error instead of
printing an address.- Property defaults are applied once, at allocation:
parent::__construct()no longer
resets a property the child already set. - A destructor runs exactly once (a resurrected object stays alive);
$o = nullruns it at
that point; objects in amixed/nullable property are owned, so their destructors run. $o->$m()follows php's visibility, falling through to__callor php's exact Error.new Xof an unknown class throws "Class "X" not found"; an abstract class is never
instantiated.instanceof self|static|parentandinstanceof Closurework.get_class()/get_debug_type()of a closure answerClosure.
Arrays and references
- The int/string/closure element types of an array are kept on store.
$f = [1.5]; $f[0] = 3no longer givesfloat(3). sort,rsortandusortreindex sparse and string-keyed arrays.$r = &$a[...]on a cell element creates a counted reference box, so it survives
buffer relocation. A nested write through a referenced element keeps the binding.- A by-ref
foreachwalks the live array, even when the body unsets elements. - A static property is by-ref addressable, so
uksort(self::$defs, …)sorts the property
itself. - Write targets evaluate the name, then the dimensions, then the value, each once.
Previously$a[$i++] op= …and$o->{f()}[g()] = vran side effects twice or out of
order.
Control flow and generators
break/continue/gotoedges,switch,matchandtry/catchjoin variable types
like an if/else merge (a local re-typed on one path could print a pointer).- A loop condition that stores to its own variable (
while (null !== ($i = next($i))))
terminates. yield fromaccepts any iterable; a finished generator answerscurrent()/key()NULL;
next()/send()prime an unstarted generator; anIteratorAggregatereturning a
generator iterates; an interface extendingIteratoris Traversable.
Strings and scalars
^ & | ~are byte-wise on two strings;10 < "9"compares numerically (false).- String offsets follow php's numeric-string rules (see Breaking for what now throws).
__NAMESPACE__folds to the parse-time namespace;constant($name)/defined($name)
resolve computed names at run time.- Parser:
0.floats, exponent underscores, the empty statement;!$o instanceof Aand
-2 ** 2follow php's precedence. - Docblocks:
int<a,b>, phpstan refined scalars,list<T>,non-empty-list<T>,
function-level@template Tand phpstan aliases lower to their base types.
Closures and callables
- Every callable form pads omitted trailing parameters and packs variadics
(array_map('trim')could trim with a random mask). [$obj, 'm'],[C::class, 'm']and invokable objects are invoked through an erased
callee;bindToon an unnamed closure reads its real layout.- Closure envs are reference-counted like objects; closures in arrays are owned by the array.
Standard library
mbstring,ext/intl,WeakMapandWeakReference: see Highlights.- zlib: incremental
deflate_init/deflate_add(context takeover, sync flush) and
inflate_init/inflate_addthat decodes a stream fed in any chunking. - New:
hash('xxh128')(pure PHP),get_defined_functions(),posix_mkfifo,
isInternal()/isUserDefined()onReflectionFunctionandReflectionClass;
new ReflectionFunction($name)accepts a computed name. array_any/array_all/array_find/array_find_keypass($value, $key).sprintf/snprintfresults are no longer truncated at 255 bytes.implode()of Stringable elements calls__toString;var_exportwrites a NUL byte
outside the quotes;var_dumpevaluates all its arguments before printing.isset,??andempty()on anArrayAccessvalue calloffsetExists.SplFixedArrayreads in place, throwsOutOfBoundsException, andsetSizetrims in place.parse_urlreplaces control bytes with_;realpath()resolves relative paths against
a cached working directory.pcntl_fork()flushes stdout first; libc int returns are sign-extended, so
pcntl_waitpid(-1, …)loops end on Linux.strtr,preg_replace(_callback),str_(i)replace,filter_varandproc_open
accept php's array forms;is_*predicates can be called by a runtime name.- FFI:
peek_f64/poke_f64read and write a C double; raw-memory builtins accept a
`Ffi...
v0.11.0
docker run --rm -v "$PWD":/work -u "$(id -u):$(id -g)" \
ghcr.io/manticorephp/compiler:0.11.0 manticore compile app.php -o app
curl -fsSL https://github.com/manticorephp/compiler/releases/download/v0.11.0/install.sh | sh
The tarballs carry the compiler and its stdlib, not a toolchain: manticore
assembles its IR with clang and links with cc, against the host's
pcre2, openssl, sqlite3 and curl. install.sh checks for those and says
which are missing. The image carries the toolchain it was built with.
Built cold from v0.11.0 — no cached compiler took part.
Changes (127 commits, since v0.10.0)
- one base, and the release self-hosts
- the dry run reaches the notes, which is the only part of a release nobody has ever executed
- the cached compiler is validated by running it, not by matching an id
- the published compiler is a seed source, so CI stops cold-seeding
- libxml2-dev is declared, not inherited
- clang comes from Debian's own archive, and apt.llvm.org leaves the image entirely
- CI
- proc_open() actually spawns
- supervise() puts a crashed worker back on a backoff, and the prefork shutdown test stops asking the clock
- an arrow fn with a yield IS a generator, and MAX_FILE_SIZE matches case-insensitively
- unset() on a name bound to storage the CALL does not own breaks the BINDING, not the storage
- isset() asks whether a variable EXISTS, not whether it is truthy
- the timer heap drops its dead slots
- a cell-typed conditional argument is released like any other owned temp
- http_workers scans a port range nobody else touches
- tools/docker
- a __get with a CONCRETE declared return hands its raw value back
- a top-level return in the ENTRY script ends it, bare or valued
- a failed write to stdout ends the program, as php's CLI does
- AGENTS.md
- docs/http.md
- analyze
- http
- http
- http
- http: Response:
- stdlib
- http
- TypeCheck is on by default — the value-channel epic's last exit criterion. Two of its own rules had kept it ga…
- a shape-typed ENUM field decodes to its ORDINAL
- a by-ref argument can name a module cell
- a module cell co-owns the closure it holds
- bin/build
- cellguard baseline
- bin/build exports MANTICORE_CELLGUARD=strict by default — the compiler and the stdlib build under the cell ver…
- (object) of a run-time-classified value dispatches by tag — an object is itself (retained), anything else a fr…
- tests
- EmitLlvm
- A mixed / unhinted property is ONE storage with ONE repr
- cellguard
- cellguard
- InferTypes
- emit
- docs
- shapes: the TypeError spells the shape and the field as php does (Type::phpString — `array{0: Node, 1
- docs
- shapes
- shapes
- runtime
- http
- async
- monomorphize
- emit
- docs
- docs
- typecheck
- emit
- typecheck
- analyze: DocType:
- emit
- infer
- emit
- emit
- infer
- emit: the property-borrow scan vetoed `Task:
- emit
- emit
- infer: a constant-key read off a shape is typed with the field; ArrayAccess_:
- mir
- mir
- emit
- lower
- mir
- tools
- emit
- runtime
- verify
- emit
- docs
- abi: props_fn offset is MemoryAbi:
- … and 47 more, see the full log
Full log: v0.10.0...v0.11.0
v0.10.0
What's Changed
- Async under load: bounded parks, a non-quadratic channel, and a measured fiber stack by @blacktrs in #7
- Generators: a resumed try owns its landing pad, in the frame it is running in by @blacktrs in #9
New Contributors
Full Changelog: v0.6.0...v0.10.0
Manticore 0.6.0
Manticore is a PHP-to-native AOT compiler written in PHP — and it compiles itself. It turns a large subset of PHP 8.5 into standalone, statically-linked arm64/x86_64 binaries via LLVM, with no PHP runtime in the output.
This is the first tagged release: the point where self-hosting is complete and the compiler builds and runs on both macOS and Linux.
Highlights
-
Self-hosting. The native compiler rebuilds itself byte-for-byte (
bin/build); the Zend seed is only the cold first boot. The self-host fixpoint (stage-2 IR == stage-3 IR) is green on macOS and Linux. -
macOS + Linux. Both platforms build the compiler and pass the full AOT suite (582/582). Output binaries are fully static and call libc and nothing else.
-
A broad standard library, byte-compatible with PHP wherever an oracle exists: the DateTime family + procedural date/time (
strtotime, timezones), networking with TLS (streams, sockets, DNS/inet), hashing/HMAC/crypto, base64/URL, CSV/INI, theprintf/sscanfformatting family, and PCRE via libpcre2. -
Install from source — no prebuilt binary, the compiler builds itself:
curl -fsSL https://raw.githubusercontent.com/manticorephp/compiler/main/install.sh | bash export PATH="$HOME/.manticore/bin:$PATH" manticore version # -> manticore 0.6.0
or
composer create-project manticorephp/compiler. Re-running upgrades in place, rebuilding the new version with the installed compiler itself.
Compile & run
manticore compile app.php -o app && ./app
echo '<?php echo "hi\n";' | manticore compile -o /tmp/hi && /tmp/hiBuild-host requirements
- clang / LLVM ≥ 15 (Manticore emits opaque-pointer IR)
- PHP 8.5 — cold bootstrap only; the output never calls a PHP runtime
- cc, libpcre2, OpenSSL 3, pkg-config
- Linux: glibc ≥ 2.33
See docs/install.md for per-OS setup.
Notes
Versioning starts at 0.6.0 to reflect the scope already shipped (self-host + the standard-library work above), not a from-scratch 0.1. The extension model is deliberately not Zend's: no C-extension API or php.ini — libraries and FFI via a build manifest.