Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d2b40ab
fix(generator): support PHP 8.5 source layout
claude Aug 6, 2026
055f030
feat: add generated engine definitions for PHP 8.5 (linux-x64-nts)
claude Aug 6, 2026
b057d0f
feat: update hand-written engine constants for PHP 8.5
claude Aug 6, 2026
119b177
feat: target PHP 8.5
claude Aug 6, 2026
8aec47f
docs: update README support matrix and issue template for PHP 8.5
claude Aug 6, 2026
a2ed9aa
fix(ci): make generator manifest and debug image work on both 8.4 and…
claude Aug 6, 2026
44663dc
chore(generator): target only PHP 8.5 on master
claude Aug 6, 2026
8c2fc07
chore(generator): drop the version gate around zend_ast_op_array
claude Aug 6, 2026
8214502
fix: stop duplicating static-variable defaults on redefine
claude Aug 6, 2026
e588c70
Merge pull request #134 from lisachenko/claude/z-engine-php-8.5-hhjbss
lisachenko Aug 6, 2026
8c8eff8
Merge pull request #133 from lisachenko/8.4
lisachenko Aug 6, 2026
22a70b1
chore(deps-dev): update phpunit/phpunit requirement || ^13.0
dependabot[bot] Aug 7, 2026
4f351fa
Merge pull request #136 from lisachenko/dependabot/composer/phpunit/p…
lisachenko Aug 7, 2026
8b27361
Merge pull request #137 from lisachenko/8.4
lisachenko Aug 7, 2026
c88dff4
Merge remote-tracking branch 'origin/8.4' into merge/8.4-into-master
claude Aug 7, 2026
7c83b37
fix(test): restore swapped $this so the live frame stays heap-safe on…
claude Aug 7, 2026
08fa106
Merge pull request #140 from lisachenko/merge/8.4-into-master
lisachenko Aug 7, 2026
d73bd7a
feat(core): support PHP 8.4 and 8.5 in parallel from one codebase
claude Aug 7, 2026
874c055
chore(ci): build matrix and generator targets for PHP 8.4 + 8.5
claude Aug 7, 2026
e6f18f2
docs: describe the parallel PHP 8.4 + 8.5 support model
claude Aug 7, 2026
ae0fbc9
fix(ci): exclude the 8.5-only fixture from PHPStan analysis
claude Aug 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ body:
id: php-version
attributes:
label: PHP version (full `php -v` first line)
placeholder: 'PHP 8.4.3 (cli) (built: ...) (NTS)'
placeholder: 'PHP 8.5.3 (cli) (built: ...) (NTS)'
validations:
required: true
- type: dropdown
Expand All @@ -34,7 +34,7 @@ body:
id: z-engine-version
attributes:
label: z-engine version / branch
placeholder: 'dev-master, or the 8.4 branch'
placeholder: 'dev-master or the 8.4 branch (both support PHP 8.4 + 8.5)'
validations:
required: true
- type: textarea
Expand Down
45 changes: 33 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ on:
- '[0-9].[0-9]'
pull_request:

# This branch targets PHP 8.4 only - engine structures are version-specific.
env:
PHP_MINOR: '8.4'

# This branch supports PHP 8.4 and 8.5 in parallel: Core selects the generated
# engine definitions of the RUNNING minor at boot, so every engine-touching job
# runs against both minors.
jobs:
tests:
name: Tests (release, NTS)
name: Tests (release, NTS, PHP ${{ matrix.php }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.4', '8.5']
steps:
- uses: actions/checkout@v4

Expand All @@ -29,7 +32,7 @@ jobs:
# (AGENTS.md).
uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_MINOR }}
php-version: ${{ matrix.php }}
extensions: ffi, opcache
ini-values: ffi.enable=1, zend.assertions=1, opcache.enable=1, opcache.enable_cli=0, opcache.jit=off, opcache.jit_buffer_size=0
coverage: none
Expand All @@ -52,17 +55,29 @@ jobs:
- name: Opcache/SHM coverage must not silently skip
run: composer test:opcache

# The PHP 8.5 file-cache shapes (attributed constants, const closures) use
# 8.5-only syntax; their group is asserted skip-free only where it applies.
- name: PHP 8.5 file-cache shapes must not silently skip
if: matrix.php != '8.4'
run: composer test:opcache85

- name: Worker-loop soak (flat memory after warm-up)
run: php -d ffi.enable=1 -d zend.assertions=1 -d opcache.jit=off tools/examples/worker-loop.php

static-analysis:
name: PHPStan (level max)
name: PHPStan (level max, PHP ${{ matrix.php }})
runs-on: ubuntu-latest
# The analysis runs per minor: the version-dependent constants and the
# PHP_VERSION_ID branches resolve differently on 8.4 and 8.5.
strategy:
fail-fast: false
matrix:
php: ['8.4', '8.5']
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_MINOR }}
php-version: ${{ matrix.php }}
extensions: ffi
ini-values: ffi.enable=1
coverage: none
Expand All @@ -76,15 +91,19 @@ jobs:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_MINOR }}
php-version: '8.5'
extensions: ffi
coverage: none
- uses: ramsey/composer-install@v3
- run: composer cs:check

tests-internal-debug:
name: Internal group (debug build)
name: Internal group (debug build, PHP ${{ matrix.php }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.4', '8.5']
# The segfault-prone destructive tests run only against a --enable-debug
# PHP build, where corruption surfaces as assertion failures. There is no
# official debug PHP image, so we build one here from the official image's
Expand All @@ -99,7 +118,7 @@ jobs:
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_MINOR }}
php-version: ${{ matrix.php }}
extensions: ffi
coverage: none

Expand All @@ -110,7 +129,7 @@ jobs:
run: |
docker build \
-f tools/docker/php-debug.Dockerfile \
--build-arg "PHP_VERSION=${PHP_MINOR}" \
--build-arg "PHP_VERSION=${{ matrix.php }}" \
-t z-engine-php:debug .

- name: Run internal group with process isolation
Expand All @@ -130,6 +149,8 @@ jobs:
header-drift:
name: Generated headers up to date
runs-on: ubuntu-latest
# A single job regenerates every default target (8.4 and 8.5) - the drift
# check must prove the WHOLE committed include/ tree matches the pipeline.
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
Expand Down
50 changes: 34 additions & 16 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,33 @@ human contributors and automated agents alike.

## The one rule that is non-negotiable: version matching

**Never run z-engine code or tests against a PHP minor version other than the
one the current branch targets.** The engine's C structures change between
every minor version (`zend_class_entry` alone changed size in 8.1, 8.3 and
8.4). z-engine reads those structures by offset. Run it on a mismatched
**Never run z-engine code or tests against a PHP minor version the current
branch has no bundled definitions for.** The engine's C structures change
between every minor version (`zend_class_entry` alone changed size in 8.1, 8.3
and 8.4). z-engine reads those structures by offset. Run it on an unsupported
version and you are reading and writing the wrong memory — the result is a
crash, or worse, silent corruption.

- `master` targets the newest supported PHP minor (currently **8.5**).
- Branch `8.4` targets **PHP 8.4**.
The current line supports **PHP 8.4 and 8.5 in parallel**: both minors have
generated definitions bundled (`include/8.4/`, `include/8.5/`), `Core::init()`
selects the set matching the *running* interpreter, and the few hand-declared
values that moved between minors (`Core::ZEND_ACC_USE_GUARDS`, the
`NodeKind::AST_*` kinds, the statics-ownership rules in `FunctionBodySwap`)
are `PHP_VERSION_ID` conditionals. z-engine owns ALL of that complexity —
consumers never deal with version-dependent headers.

- Branch `8.4` (default) and `master` both carry the parallel 8.4 + 8.5
support; `master` is where support for the next minor lands first.
- Branch `8.0` is the frozen legacy line for PHP 8.0.

`Core::init()` enforces this at runtime and refuses to boot on the wrong minor.
Do not try to defeat that guard.
`Core::init()` enforces the supported range at runtime and refuses to boot on
any other minor. Do not try to defeat that guard.

When you add or change version-dependent behavior, it must work on EVERY minor
the branch supports: express the difference as a `PHP_VERSION_ID` conditional
(constants included — a kind that exists on only one minor gets a negative
sentinel on the others, see `NodeKind`), and make sure CI's matrix exercises
both sides of the branch you introduced.

## Branch model

Expand All @@ -30,13 +44,14 @@ never cherry-picked downward. The succession is declared in
which opens a merge-up PR when a version branch is pushed.

```
8.0 (frozen) 8.4 ──► master (8.5)
8.0 (frozen) 8.4 (default, PHP 8.4 + 8.5) ──► master (PHP 8.4 + 8.5)
```

So a bug that exists in both 8.4 and 8.5 is fixed on `8.4`, and the cascade
carries it into `master`. A bug that only exists on 8.5 is fixed on `master`
directly. When resolving a merge-up conflict inside `include/`, do **not**
merge the generated headers textually — regenerate them on the target branch
Since the 8.4/8.5 unification both `8.4` and `master` support the same two
minors; fixes land on `8.4` and the cascade carries them into `master`, which
is also where support for the NEXT minor (8.6) will be added first. When
resolving a merge-up conflict inside `include/`, do **not** merge the
generated headers textually — regenerate them on the target branch
(`composer gen-headers`) instead.

## Generated engine definitions — never hand-edit
Expand All @@ -53,13 +68,16 @@ Everything under `include/<minor>/<os>-<arch>-<ts>/` is generated:
Regenerate them with:

```bash
composer gen-headers # all targets for this branch (needs Docker)
composer gen-headers # all targets for this branch: 8.4 AND 8.5 (needs Docker)
```

The generator (`tools/generator/`) runs inside the official `php:<minor>` Docker
image so the artifacts always match a real build. Regenerate whenever you:
image so the artifacts always match a real build. The default target list in
`tools/generator/generate.php` covers every supported minor — a symbol change
regenerates all of them, never just the minor you happen to be running.
Regenerate whenever you:

- bump the branch to a new PHP minor,
- add a new PHP minor to the branch,
- add or remove an engine symbol in `tools/generator/symbols.php`,
- or CI's `header-drift` job goes red.

Expand Down
9 changes: 5 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ humans and automated tools, and this document is the short human version.

## Before you start

- **Match your PHP version to the branch.** `master` targets the newest
supported PHP minor, branch `8.4` targets PHP 8.4, and so on. Running against
the wrong minor crashes PHP — this is the single most important rule.
- **Run only supported PHP minors.** The current line supports PHP 8.4 and
8.5 in parallel (both branch `8.4` and `master`); branch `8.0` is the frozen
legacy line. Running against an unsupported minor crashes PHP — this is the
single most important rule.
- Develop against a **debug PHP build** when you can (`--enable-debug`, FFI
enabled). It turns silent memory corruption into loud assertion failures. A
ready-made image lives in `tools/docker/php-debug.Dockerfile`.
Expand Down Expand Up @@ -46,7 +47,7 @@ composer test:internal
## Pull request checklist

- [ ] Targets the correct branch for the PHP version affected
- [ ] `composer test` passes on the matching PHP minor
- [ ] `composer test` passes on every supported PHP minor (CI runs 8.4 and 8.5)
- [ ] `composer phpstan` and `composer cs:check` are green
- [ ] Generated `include/` artifacts regenerated if engine symbols changed
- [ ] Tests added or updated
Expand Down
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,15 @@ FFI lets PHP load shared libraries, call C functions, and read C structures with
- PHP with the **FFI** extension enabled
- **x64, non-thread-safe (NTS)** builds

Engine memory layouts change between every PHP minor version, so each PHP minor has its own generated definitions and its own branch.
Engine memory layouts change between every PHP minor version. Z-Engine hides that complexity from you: it bundles generated, version-exact definitions for **every supported minor in parallel** (`include/8.4/`, `include/8.5/`) and `Core::init()` selects the set matching the *running* interpreter at boot. Install one release, run it on PHP 8.4 or 8.5 — no per-version branch juggling in your application.

| PHP | OS / Arch / TS | Branch | Status |
|-----|----------------|--------|--------|
| 8.5 | linux-x64-nts | `master` | 🚧 in progress |
| 8.4 | linux-x64-nts | `8.4` | ✅ supported |
| 8.4 + 8.5 | linux-x64-nts | `8.4` / `master` | ✅ supported in parallel |
| 8.0 | linux-x64-nts | `8.0` | 🧊 frozen (legacy) |
| macOS / Windows / ZTS | — | — | 📋 [tracked in issues](https://github.com/lisachenko/z-engine/issues) |

> **Version matching is not optional.** Running Z-Engine against a PHP minor it was not built for corrupts memory. `Core::init()` enforces the match and aborts with a clear message rather than letting you crash.
> **Version matching is not optional.** Running Z-Engine against a PHP minor it has no bundled definitions for corrupts memory. `Core::init()` enforces the match and aborts with a clear message rather than letting you crash.

## Memory safety & long-running PHP

Expand Down Expand Up @@ -275,7 +274,7 @@ These libraries are built entirely on Z-Engine and make good, real-world reading

## Contributing

Z-Engine has a couple of unusual rules — most importantly, **match your PHP version to the branch** and develop against a debug build. See **[CONTRIBUTING.md](CONTRIBUTING.md)** and **[AGENTS.md](AGENTS.md)** (the full contract for humans and automated tools). Engine definitions are generated from the PHP source by `tools/generator/` and never hand-edited.
Z-Engine has a couple of unusual rules — most importantly, **run only PHP minors the branch bundles definitions for** (currently 8.4 and 8.5) and develop against a debug build. See **[CONTRIBUTING.md](CONTRIBUTING.md)** and **[AGENTS.md](AGENTS.md)** (the full contract for humans and automated tools). Engine definitions are generated from the PHP source by `tools/generator/` and never hand-edited.

```bash
composer test # safe suite
Expand Down
11 changes: 9 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
}
],
"require": {
"php": "~8.4.0",
"php": "~8.4.0 || ~8.5.0",
"ext-ffi": "*"
},
"require-dev": {
"phpunit/phpunit": "^12.2",
"phpunit/phpunit": "^12.2 || ^13.0",
"phpstan/phpstan": "^2.1",
"friendsofphp/php-cs-fixer": "^3.75"
},
Expand All @@ -36,6 +36,7 @@
"test": "phpunit",
"test:internal": "phpunit --group internal --process-isolation",
"test:opcache": "phpunit --group opcache --fail-on-skipped",
"test:opcache85": "phpunit --group opcache-php85 --fail-on-skipped",
"phpstan": "phpstan analyse",
"cs:check": "php-cs-fixer fix --dry-run --diff",
"cs:fix": "php-cs-fixer fix",
Expand All @@ -45,12 +46,18 @@
"test": "Run the test suite (segfault-prone internal group excluded)",
"test:internal": "Run the segfault-prone internal test group with process isolation (use a debug PHP build)",
"test:opcache": "Run the opcache/shared-memory tests and FAIL if any of them skipped (they are self-skipping when opcache is unavailable)",
"test:opcache85": "Run the PHP 8.5-only file-cache shape tests and FAIL if any skipped (run this on PHP 8.5+)",
"phpstan": "Run static analysis at the maximum level",
"cs:check": "Check coding standards without fixing",
"cs:fix": "Fix coding standards",
"gen-headers": "Regenerate FFI engine definitions from the PHP source (requires docker)"
},
"config": {
"sort-packages": true
},
"extra": {
"branch-alias": {
"dev-master": "8.5.x-dev"
}
}
}
Loading
Loading