Skip to content

Releases: manticorephp/compiler

v0.15.0

Choose a tag to compare

@github-actions github-actions released this 18 Oct 13:13
Immutable release. Only release title and notes can be modified.
fe5476b
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-app builds to one binary that serves / (200) and an HTML
    404 under Http\Server, in prod and in dev, including a cold start with no
    var/. examples/symfony-console runs.
  • GET / on macOS arm64 with wrk: 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_shift results, 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::X defaults evaluated
    in the declaring class, class constants through every extended interface,
    ReflectionProperty::setValue() on Exception/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 in lib/manticore_stdlib.o and 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.o grows 7.2 -> 8.9 MB. Hello world is byte-identical. CI linux-amd64 suite: 91 -> 42 min.

CLI

  • manticore upgrade [--check] [--version X]: checksummed, staged swap with
    rollback, previous binary kept as bin/.manticore.prev; package-manager and
    container installs are refused. --check exits 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 / build progress: --progress=auto|live|plain|json|off, --quiet,
    --timings, --color. Piped output stays silent unless CI is set; stdout is
    never touched.
  • Arg-parse errors read <command>: <error> (exit code still 64).

New extensions and language features

  • ext/bcmath in pure PHP: all 14 functions, byte-identical to Zend on 58k fuzz
    cases; BcMath\Number matches methods, scales and messages (30k fuzz cases).
  • ext/openssl over 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\Overloadable with __operator /
    __compare for + - * / % ** <=> == != < <= > >=, 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.
  • match without a default arm throws UnhandledMatchError.
  • Readonly classes: dynamic properties and unset throw Error like 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
    void var_dump, enum array cast, callable slots, by-ref stdClass
    properties and generator valid (#250).
  • Int/bool arguments to float parameters convert on every call path (#228).
  • A by-value closure capture written through ($l[] = 1, array_pop($l)) was
    released twice at exit (#275).
  • addslashes is binary safe (#279); hex2bin rejects odd and non-hex input
    (#281); numeric strings compare as numbers in < and sort (#283, #308).
  • array_merge(...$l) emptied the source's inner arrays (#309); a bare array
    parameter written with another element type stored raw pointers (#307);
    array_shift / array_pop leaked and kept destructors waiting (#314, #315).
  • printf/sprintf format non-finite floats as NaN/INF (#266); a literal
    printf('%f', NAN) stays on the inline snprintf path and prints nan.
  • ReflectionFunction, ReflectionProperty and trait-method reflection gaps
    (#287, #288, #289); first dev boot segfault in DeepClone::getHydrator (#290).
  • Ds typed arrays: inline bitwise stores and TypedArray::apply()
    (28 -> 6.6 ns/element).

Compatibility and upgrade notes

  • Memory ABI v22. array_shift/array_unshift slide the array header
    (ARRAY_HEAD_GAP_OFFSET). Rebuild programs and any .o built with an older
    compiler. No bootstrap pin raise is needed.
  • COMPILER_VERSION is the single version source; tools/check_version.sh
    enforces it in CI and the release workflow.
  • BOOTSTRAP_VERSION stays 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-console differs 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_call does 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 0 2220 passed; xfail.sh 0 XPASS.
  • Not run: difftest, selfhost fixpoint, full local Linux gate.

Full log: v0.14.0...v0.15.0

v0.14.0

Choose a tag to compare

@github-actions github-actions released this 09 Oct 14:33
Immutable release. Only release title and notes can be modified.
7f55d99
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> and Set<T>: insertion-ordered hash containers with
    strict keys. Keys are int, string or object (by identity); 1 and "1"
    are two different keys, and anything else is a TypeError.
    An absent key throws OutOfBoundsException.
  • Vec<T>: a growable list over the native cell buffer.
  • foreach over a Map, Set or Vec walks the storage directly, with no generator
    frame. each, map, filter, reduce, any, all and find are 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 a Set<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_decode and json_validate stay native for every flag
    (JSON_THROW_ON_ERROR, JSON_PRETTY_PRINT, JSON_UNESCAPED_*, JSON_HEX_*,
    JSON_PARTIAL_OUTPUT, JSON_BIGINT_AS_STRING and 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_objects benchmark went from 0.14 s to 0.08 s, 4.4 times php.
  • UInt64Array encodes values at or above 2^63 exactly.

ext/zlib over the host libz

  • gzencode, gzcompress and friends call the system zlib, so they produce php's
    bytes. gzencode is 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), and Http\Server listens with 511, so a burst of connects no
    longer overflows the queue into resets. stream_context_set_options is 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 by tools/fetch_bootstrap.sh. bin/build, CI, the release workflow,
    the Dockerfiles and install.sh all start there. The Zend cold seed and its
    tooling are removed, and a clean clone needs no php. A new bootstrap-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 how array_eq_nested_lists_null_rows failed 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 a ValueError instead of a silent truncation, and f32
    rounds to binary32.
  • ReflectionFunction reflects a Closure, including first-class-callable
    closures, parameter attributes and defaults, and getClosureThis(). A dynamic
    closure call boxes an object argument, and a spread into a closure pads omitted
    optional parameters. symfony/console 7.4 runs.
  • foreach over 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, isset and by-ref
    element paths, and ArrayAccess offsets reach offset*() 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

Choose a tag to compare

@github-actions github-actions released this 07 Oct 09:25
Immutable release. Only release title and notes can be modified.
5757310
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, and ByteBuffer (typed,
    endianness-aware reads and writes at a byte offset).
  • Growable (push(), pop(), $a[] = $v) with whole-array sum(), min(),
    max(), equals(); values out of an element's range throw ValueError, 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] = $v and $a[] = $v are an inline bounds check plus a load or store, and a
    counted loop loads the buffer and its length once. 10M get/set on an Int32Array
    in a property: 168 ms → 6.4 ms (array<int,int>: ~39 ms); 10M appends: 27 ms
    (array: 51 ms); a 2M-read getUInt32 scan is ~60× faster than unpack().

SPL with php's behaviour

  • SplFixedArray stores 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, SplStack rewritten: 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 its finally blocks first, as php does.
  • Zero-cost exceptions: landing pads and unwind tables, finally on every exit,
    exception chaining when a throw leaves finally.
  • 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.
  • isset and ?? probe once and evaluate base and key once, key first as php's
    delayed fetch does; string-keyed reads, array_key_exists and unset normalise 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.sh runs them on every CI push and reports a fixed one as XPASS.
    tools/file_bug.sh files the issue from the repro.

Language & runtime

  • json_encode honours JsonSerializable and emits a backed enum's value;
    print_r and var_dump show object properties with php's :protected /
    :"Class":private markers; __unserialize receives int keys as ints.
  • pack() / unpack() implement the float codes e E d g G f.
  • Generics keep a #[TypeDef] binding: short class names in Foo<Bar> resolve through
    use, specializations are built over the TypeDef, and @return T refines a
    compatible native hint.
  • Fixed crashes on values held in mixed: an ArrayAccess element write, an enum case
    after instanceof, a float ??.
  • The tokenizer follows Zend's number grammar and heredoc escapes; proc_open wires 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.
  • analyze reports front-end errors instead of answering "No errors found".

Compatibility

  • Memory ABI v18. A library .o built by an older compiler must be rebuilt;
    bin/build needs no seed for the bump.
  • Manticore\Ds is 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 foreach over a typed array bound to a #[TypeDef] yields the plain scalar.
  • #106 a class with a #[TypeDef] property makes any var_dump() a compile error.
  • #109 json_encode of a UInt64Array writes 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

Choose a tag to compare

@github-actions github-actions released this 30 Sep 09:15
Immutable release. Only release title and notes can be modified.
ca403e4
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 every MB_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 for mbstring and intl. 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 (via Http\Response::takeover and Server::onStop)
    and an outbound client, Http\WebSocket\connect; wss:// over the tls:// 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() (onClose exactly once, 1011 on a
    throwing handler) and Hub broadcast. 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
    stat family, opendir/readdir/scandir, unlink/rename/mkdir/rmdir and
    getaddrinfo run 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_THREADS sets its size (default 4,
    1..64); Async\stats() gains offloaded / pool_threads. A pooled call costs about
    10 µs; cancellation and starvation are documented in docs/async.md.
  • Deadlines, timers, stream timeouts and backoffs run on the monotonic clock: an NTP or
    date -s step no longer stalls or mass-fires them. awaitDeadline drops 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 / :alpine images
    and linux-musl tarballs, and install.sh picks the musl tarball on a musl host.
    musl surfaced three real memory bugs glibc had absorbed; all fixed.

Arithmetic errors, WeakMap

  • /, % and intdiv throw DivisionByZeroError; intdiv(PHP_INT_MIN, -1) and a
    negative shift throw ArithmeticError (they answered INF, 1, 0 or garbage). A shift of
    64+ is 0 / -1; % -1 no longer traps on x86_64.
  • WeakMap and WeakReference: 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. @PSR12 over Manticore's own src/: 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,
    max and in_array. Private state counts, DateTime compares by instant, and enum cases
    and closures compare by identity. New attributes control this: #[CompareKey],
    #[CompareNone] and #[Uncomparable] (see docs/attributes.md). #[Struct] compares
    field by field.
  • (string)/echo/concat of an object without __toString throws 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 = null runs it at
    that point; objects in a mixed/nullable property are owned, so their destructors run.
  • $o->$m() follows php's visibility, falling through to __call or php's exact Error.
  • new X of an unknown class throws "Class "X" not found"; an abstract class is never
    instantiated. instanceof self|static|parent and instanceof Closure work.
  • get_class() / get_debug_type() of a closure answer Closure.

Arrays and references

  • The int/string/closure element types of an array are kept on store.
    $f = [1.5]; $f[0] = 3 no longer gives float(3).
  • sort, rsort and usort reindex 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 foreach walks 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()] = v ran side effects twice or out of
    order.

Control flow and generators

  • break/continue/goto edges, switch, match and try/catch join 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 from accepts any iterable; a finished generator answers current()/key() NULL;
    next()/send() prime an unstarted generator; an IteratorAggregate returning a
    generator iterates; an interface extending Iterator is 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 A and
    -2 ** 2 follow php's precedence.
  • Docblocks: int<a,b>, phpstan refined scalars, list<T>, non-empty-list<T>,
    function-level @template T and 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; bindTo on 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, WeakMap and WeakReference: see Highlights.
  • zlib: incremental deflate_init/deflate_add (context takeover, sync flush) and
    inflate_init/inflate_add that decodes a stream fed in any chunking.
  • New: hash('xxh128') (pure PHP), get_defined_functions(), posix_mkfifo,
    isInternal()/isUserDefined() on ReflectionFunction and ReflectionClass;
    new ReflectionFunction($name) accepts a computed name.
  • array_any/array_all/array_find/array_find_key pass ($value, $key).
  • sprintf/snprintf results are no longer truncated at 255 bytes.
  • implode() of Stringable elements calls __toString; var_export writes a NUL byte
    outside the quotes; var_dump evaluates all its arguments before printing.
  • isset, ?? and empty() on an ArrayAccess value call offsetExists.
  • SplFixedArray reads in place, throws OutOfBoundsException, and setSize trims in place.
  • parse_url replaces 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_var and proc_open
    accept php's array forms; is_* predicates can be called by a runtime name.
  • FFI: peek_f64/poke_f64 read and write a C double; raw-memory builtins accept a
    `Ffi...
Read more

v0.11.0

Choose a tag to compare

@github-actions github-actions released this 23 Sep 07:06
Immutable release. Only release title and notes can be modified.
3943258
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

Choose a tag to compare

@blacktrs blacktrs released this 19 Sep 08:28
8df614e

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

Choose a tag to compare

@blacktrs blacktrs released this 21 Jul 12:25
97d747c

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, the printf/sscanf formatting 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/hi

Build-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.