* test(phpunit): anchor TypePhp autoloading to the current checkout
A git worktree shares vendor/ with the primary checkout via a symlink, and
Composer's generated autoloader resolves the TypePhp\ prefix relative to the
realpath of vendor/. The suite then silently loads and tests the OTHER
checkout's src/ tree. Prepend an autoloader anchored to this checkout so the
tests always exercise the sources they ship with; in a standalone checkout
this is a no-op.
* fix(translator): allow overrides to add a by-ref return
Zend's inheritance check treats return-by-reference as covariant
(zend_do_perform_implementation_check): an error is raised only when the
parent returns by reference and the child does not. The child adding `&`
is a strictly stronger guarantee and is accepted:
class A { public function f(): array {} }
class B extends A { public function &f(): array {} } // OK in Zend
validateMethodOverrideSignature compared returnsByRef with exact equality,
rejecting this valid program. Make the check one-directional; dropping a
parent's by-ref return remains fatal.
The >=16-significant-digit float-literal-to-php::Decimal promotion
(docs/en/HIGH_PRECISION_TYPES.md) counted every digit in the raw
literal with preg_replace('/[^0-9]/'), so exponent digits and trailing
zeros counted as significant: 1.23456789012345e300 (15 significant
digits) and 999999999999999.0 became Decimal, making
is_float(2.220446049250313E-16) compile to false. Hex literals whose
digits contain E (0x123456789E1234567) matched the [.eE] test and
became Decimal("0x..."), where Zend folds an overflowing hex literal
to its exact double.
Three fixes, keeping the documented feature:
- Count true mantissa significant digits (strip sign, exponent,
leading and trailing zeros) and additionally require that the double
cannot reproduce the literal exactly - a literal that round-trips
(every var_export/serialize output, PHP_FLOAT_EPSILON) has lost
nothing and stays float, while 3.14159265358979323846 still promotes.
- Exclude hex/octal/binary notation from the reclassification.
- When a Decimal-classified literal meets a float-typed expression in
a binary op or comparison, demote the literal to its exact double
instead of the "Cannot convert float expression to Decimal" fatal:
PHP evaluates every float literal as a double, so
0.1 + 0.2 == 0.30000000000000004 is valid PHP and must be true.
constantNumericValue() matched strtolower($name) with no namespace
resolution, so two invalid folds happened: `namespace N;
const PHP_INT_MAX = 5; PHP_INT_MAX + 1` folded to 9.22e18 where PHP
resolves the namespaced constant and yields 6, and a lowercase
`php_int_max` silently folded to the global value where PHP raises an
undefined-constant Error.
Resolve the fetched name the way parseConstFetch() does: a `use const`
alias resolves to its target, a fully qualified name is global, an
unqualified name inside a namespace participates in PHP's runtime
fallback (Namespace\NAME can be defined before the fetch executes) and
therefore never provably names the global, and the match is now
case-sensitive. Only a provable global PHP_INT_MAX/PHP_INT_MIN folds.
getCExpr() emitted int class constants, property defaults and parameter
defaults with strval(), so `const M = PHP_INT_MIN;` produced
`ZVAL_LONG(&const_M_value, -9223372036854775808)`. C parses that as
unary minus applied to the literal 9223372036854775808, which exceeds
long long and is ill-formed, so the generated extension source does not
compile. The expression path already handles this via genIntegerLiteral
(ZEND_LONG_MIN); give the stub metadata path the same spelling.
The float paths (17-digit round-trip, -0.0 sign, INF/NAN) were already
fixed upstream in 2d81626a; the new test pins those literals down
together with the int boundary values.
* fix(parser): always parenthesize the unary minus operand
Unary minus concatenated '-' directly onto the operand's generated C++.
For a compound operand the minus then bound to the wrong subexpression:
PHP's -($a ? $b : $c) emitted `-cond ? b : c`, which C++ parses as
`(-cond) ? b : c` — the negation lands on the condition and the branch
choice itself can flip (pick(1,2,3) returned 2 instead of -2). The
previous str_starts_with('-') guard only covered operands already
beginning with '-' (the `--` token-pasting case).
Emit '-(' operand ')' unconditionally; this subsumes the pre-decrement
guard. Unary '+' emits no operator text and boolean/bitwise not already
close their operands, so they are unaffected.
* test(operator): accept parenthesized negative infinity literal
parseUnaryMinus now always parenthesizes its operand, so the -INF
float literal is emitted as -(std::numeric_limits<double>::infinity()).
The C++ value is unchanged; only the spelling assertion needed updating.
* fix(parser): keep bare numeric literals unparenthesized under unary minus
A single-token numeric literal cannot change the C++ parse; emitting
-7L directly keeps the generated code and the existing test snapshots
readable. Every other operand stays parenthesized.
- Stabilize property receivers for compound assignments to ensure single evaluation
- Implement checked int property compound assignments with proper PHP arithmetic
- Add materialization of object receivers for property compound assignments
- Support native types handling for compound assignment operations
- Fix sequence handling for dynamic property fetch writes in compound assignments
- Update tests to verify correct behavior of int property compound assignments
- Modify native property assignment operations to use direct references
- Add comprehensive test coverage for typed property compound assignment scenarios
The classMethodOverride registration in prepareClassMethod() depends on
file preprocessing order. With a "sandwich" order (ancestor first, leaf
second, intermediate class last), the ancestor method's override flag is
missed: the upward marking cannot cross the not-yet-registered
intermediate class, and the downward subclass lookup ran before the leaf
was prepared. findNativeMethod() then devirtualizes the late-bound call
into a direct native call, silently ignoring the override. In Hyperf this
turns SoftDeletes::delete() into a physical DELETE.
Finalize the flags once the complete class graph is known:
finalizeMethodOverrideFlags() walks every declared method's complete
parent chain and marks each existing ancestor method of the same name as
overridden (method count x inheritance depth). It runs from
Translator::convertFile() next to finalizeDeclarationExpressions(), so
both the project pipeline and the public prepareFile()/convertFile() API
share the same pre-conversion finalization, guarded by a dirty/finalized
flag reset in prepareFile().
Tests:
- tests/compiler/devirtualize/override-order-sandwich.phpt (fails on
master with "base", passes with "leaf")
- tests/compiler/devirtualize/override-order-normal.phpt (control)
- phpunit/src/DevirtualizeOrderTest.php driving the public
prepareFile()/convertFile() API in both orders, asserting the
generated Base::delete() body dispatches dynamically
Address both review findings on the trait composition fix:
1. An abstract trait requirement was discarded without validating the
concrete implementation. composeTraitAst now validates the
implementation - the class's own method, a concrete method from
another trait (in either collection order), or a class method
matching an aliased abstract - against the abstract declaration
before dropping it, following Zend's trait-composition rules:
matching staticness, a kept by-reference return, no additional
required parameters, contravariant parameter types, and a covariant
return type. Type variance reuses the existing override-check
machinery on the preprocessed definitions; late-bound self/static/
parent keywords are unified through the recorded type keywords, and
self-in-trait resolutions are remapped to the consuming class.
Visibility is intentionally not restricted: Zend allows an
implementation of any visibility to fulfill an abstract trait
requirement (verified against Zend 8.4).
2. Multiple alias adaptations of the same method depended on source
order because a same-name visibility change mutated the statement
that later adaptations cloned. Every adaptation now derives its
flags from the immutable original flags, and the original statement
is only mutated after all adaptations are processed, so
`value as protected; value as alias;` leaves `alias` public in both
adaptation orders (matching Zend, where each adaptation derives from
the original and the last same-name adaptation wins).
Both behaviors were pinned against Zend PHP 8.4 before implementing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add new integration workflow for PHP 8.4 and 8.5 ZTS testing
- Create comprehensive integration test runner script for EXT/LIB boundaries
- Add lifecycle tests covering CLI, php-S, and PHP-FPM execution environments
- Implement provider library integration tests with stub generation
- Add FastCGI protocol support for PHP-FPM communication testing
- Include detailed README documentation for integration test suite
- Set up automatic artifact collection for integration test failures
- Updated version from 0.6.7 to 0.6.8 in project.yml
- Updated file-version from 0.6.7.1112 to 0.6.8.1112 in project.yml
- Updated product-version from 0.6.7 to 0.6.8 in project.yml
- Updated VERSION constant from 0.6.7 to 0.6.8 in Translator.php
- Added bump-version.sh script for automated version management
- Script synchronizes version across project.yml and Translator.php files
- Remove src and bin directories from analysis scope
- Replace phpstan-bootstrap.php with bin/bootstrap.php for consistent autoloader setup
- Remove redundant CLI constants definition in favor of existing bootstrap file
- Clean up obsolete phpstan-bootstrap.php file entirely
- Created English acknowledgements document recognizing core developers and contributors
- Added comprehensive list of Git contributors with their specific areas of contribution
- Included community and ecosystem contributors who helped with testing and outreach
- Documented foundational projects like GCC, Clang/LLVM, MSVC, ISO C++ committee, PHP
- Listed supporting libraries such as PHPX, Composer, CLImate, TopSort, Symfony components
- Added Chinese translation of the acknowledgements document
- Recognized contributors for PHP semantic compatibility, type system, optimization work
- Added acknowledgements section to English README
- Included thanks to developers and contributors
- Mentioned supporting open-source communities and projects
- Added link to full acknowledgements document
- Translated and added acknowledgements section to Chinese README
- Maintained consistency between both language versions
- Change "ARM64" to "arm64" in Linux workflow name
- Change "ARM64" to "arm64" in macOS workflow name
- Ensure consistent lowercase naming across all platform workflows
- Updated swoole/phpx from ~2.6.8 to ~2.6.9 in composer.json
- Bumped minor version to incorporate latest bug fixes and improvements
- Maintained compatibility with existing codebase integration
An unpacked call carries a single Node\Arg regardless of its runtime
arity, so the arity check alone still accepted intval(...$args) as a
single-argument Native conversion and lowered the array itself:
intval(...['ff', 16]) // php::toInt(withBase) -> int(1)
strval(...['42']) // php::toString(single) -> "Array" + warning
A named argument has the same shape and need not be the value being
converted: intval(bogus: 1) must raise "Unknown named parameter", not
fold to a cast of 1.
Reject both in dispatchConversion() so the runtime determines the
expanded arity and the parameter names, matching what
dispatchFuncCall() already does for every other builtin.
The first whitelist was too broad. ConstFetch, ClassConstFetch and the
base Node\Scalar type all admit expressions PHP must still evaluate, so
count([UNDEFINED_COUNT_LITERAL]), count([KnownClass::MISSING]) and
count(["{$object->property}"]) folded to 1, dropping two Errors and a
__get() call. The defined-variable check was not a purity proof either:
hasVar() only reports a compiler slot, not that the variable is still
initialized on every path after unset().
Narrow the fold to items whose evaluation cannot be observed:
- literal Int_, Float_ and String_ (an interpolated string is a distinct
InterpolatedString node, so String_ already excludes it);
- the language constants true, false and null only;
- unary plus/minus over a literal int or float;
- recursively safe nested arrays.
Variables, general constant and class constant fetches, interpolated
strings and every other expression stay on the runtime path, and
by-reference items are now rejected explicitly alongside keys and
unpacking.
Cover the three reported cases plus a by-reference item, a plain
variable read and a defined class constant in both the fold-decision
test and the PHPT.
- Updated swoole/phpx from ~2.6.7 to ~2.6.8 in composer.json
- Bumped dependency version to latest stable release
- Maintained compatibility with existing codebase
- Translate Chinese comments to English in dump-ast.php
- Update comment headers from Chinese to English in extractor.php
- Replace Chinese documentation comments with English equivalents
- Maintain all functionality while improving internationalization
- Standardize commenting style across CLI utility files
Consolidate float-to-C++ literal generation into Utils::genFloatLiteral().
Previously:
- genCValue() stringified floats directly via (string), emitting 'INF', '-INF', 'NAN', or losing the floating-point decimal point for whole numbers like 1.0 -> '1'.
- BinaryOpTrait::genFloatLiteral() used sprintf('%.17g') without handling INF or NAN.
Now all float code generation paths delegate to Utils::genFloatLiteral(), mapping INF/-INF/NAN to std::numeric_limits<double> and ensuring whole numbers retain .0.
The four scalar conversions are lowered to a single-argument Native cast
by dispatchConversion, which reads args[0] and ignores the rest. intval()
takes a $base as its second argument, so it was silently discarded:
intval("ff", 16); // php::toInt("ff") -> 0, PHP gives 255
intval("0x1A", 16); // php::toInt("0x1A") -> 0, PHP gives 26
intval("101", 2); // php::toInt("101") -> 101, PHP gives 5
Nothing reports the loss: the program compiles clean and the number is
simply wrong, which is easy to miss in the code that most often uses a
base - parsing hex colors, permission masks and binary flags.
A conversion call with any arity other than one now falls through to the
dynamic path, where both arguments are passed to the runtime function.
Single-argument intval(), strval(), floatval() and boolval() keep their
Native cast, so the common case is unchanged.
type_conv.phpt gains the base cases, with a literal and a variable base;
none of them were covered anywhere in the test suite. ConversionArityTest
pins the lowering decision in the generated C++.