Repository navigation
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.