* fix(codegen): evaluate compound ??= RHS only when the target is not set
PHP evaluates the right-hand side of ??= lazily: `$a = 1;
$a ??= sideEffect() + 1;` never calls sideEffect(). When the RHS was a
compound expression the compiler materialized its lowered statements
(the call result temporary) into the enclosing statement context, so
the generated C++ executed the side-effecting call unconditionally
before the isset check.
Generalize the conditional-lambda lowering that already protected
native-object targets: whenever the RHS captured before/after
statements, emit an immediately-invoked lambda whose not-set branch
contains those statements, the assignment and the cleanup. The simple
inline form (`$b ??= f()`) keeps its existing conditional-expression
codegen unchanged.
* fix(codegen): stabilize ??= targets and finish the RHS before assigning
Zend evaluates a coalesce-assignment target's receiver and array keys
exactly once, before the isset check and regardless of its outcome; the
string-based lowering mentioned the target on every use (isset, read,
write, returned value), so a side-effecting receiver ran twice when the
target was set and three times when it was not. Side-effecting target
subexpressions are now materialized into temporaries in source order
(array containers keep their original variable — writing through a
copied temporary would write to the copy — while object receivers are
handles) and the rewritten target reuses them everywhere.
The captured branch also assigned the target before running the RHS's
deferred write-backs, so a postfix increment on the RHS finished after
the outer assignment — observable by a set hook on the target. The RHS
now completes into a temporary (write-backs included) before the target
is written, and the assignment expression itself is returned so the
target is not read again afterwards.
* fix(codegen): stabilize every ??= target subexpression, bound temporary lifetimes
Three target-stabilization gaps in the coalesce-assignment lowering:
- A value-producing array container (makeArray()[keyName()] ??= 42) was
left unstabilized: the dimension was materialized first, reversing
PHP's container-then-key source order, and the container ran once for
the isset check and again for the write. Non-variable containers are
now materialized in source order; plain-variable containers keep
write-through semantics, and a value-producing container is itself
the temporary PHP writes into.
- Dynamic property names were re-evaluated on every mention:
$box->{propertyName()} ran the name expression twice per branch, and
StaticPropertyFetch was not handled at all. Dynamic instance names,
static class expressions and static property names are now
materialized once, in PHP evaluation order (receiver, name, then
dimension), recursively through chained targets.
- The materialized temporaries were function-scoped, deferring the
receiver's destructor to function exit where PHP destroys it at the
end of the statement. Temporaries now follow the established lifetime
idiom (stabilizeAssignOpPropertyReceiver): zval-owning Variants are
.unset() at statement end and Native pointer temporaries reset to
nullptr.
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.
* 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
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.
parseUnaryMinus emitted '-' . $code without guarding against an operand
that itself starts with '-', pasting into the C++ pre-decrement token:
`- -$x` compiled to `--x`. On a php::Var operand the generated
translation unit fails to build ("expression is not assignable"); on a
native int operand it builds and silently decrements: a function
`(int $x) => - -$x` returned 8 for input 9.
Parenthesize the operand exactly when its emitted code starts with '-'
(a nested unary minus or a negative literal), so plain literals keep
their compact form (`-7L`). Binary operands are already self-wrapped in
parentheses, and unary plus needs no change since it returns the
operand unchanged.
The flag checks that translate `break N` / `continue N` were emitted only
at the end of each enclosing loop body. After the inner construct exited
with the countdown flag set, every trailing statement of the enclosing
body still executed before the check ran:
foreach ([1] as $x) {
foreach ([1] as $y) { break 2; }
echo "leaked"; // ran in compiled output, not in PHP
}
The native (int-typed) switch path was worse: its check sat inside the
do-while(0) wrapper, decrementing the flag a second time for the switch
level the C++ `break` had already exited. A `break 2` from a native
switch inside a loop therefore never exited the loop at all.
Emit the propagation check immediately after every nested loop / switch
statement instead, from the statement dispatcher, and drop the dead
end-of-body emissions. The check now also distinguishes the enclosing
construct: when it sits inside a switch, a continue that lands on the
switch level lowers to `break`, matching PHP's continue-targets-switch
semantics.
parseBreak/parseContinue now reject levels exceeding the number of
enclosing breakable constructs - the same compile-time validation PHP
performs (`Cannot 'break' 2 levels`) - which the countdown scheme
relies on to terminate at an enclosing construct.
The continue-2-while scenario in break-continue-level.phpt encoded the
old leaked behavior: its `$i++` after the inner loop only ran because of
the misplaced check; standard PHP loops forever on it. The counter now
advances before the inner loop.
The flag checks that translate `break N` / `continue N` were emitted only
at the end of each enclosing loop body. After the inner construct exited
with the countdown flag set, every trailing statement of the enclosing
body still executed before the check ran:
foreach ([1] as $x) {
foreach ([1] as $y) { break 2; }
echo "leaked"; // ran in compiled output, not in PHP
}
The native (int-typed) switch path was worse: its check sat inside the
do-while(0) wrapper, decrementing the flag a second time for the switch
level the C++ `break` had already exited. A `break 2` from a native
switch inside a loop therefore never exited the loop at all.
Emit the propagation check immediately after every nested loop / switch
statement instead, from the statement dispatcher, and drop the dead
end-of-body emissions. The check now also distinguishes the enclosing
construct: when it sits inside a switch, a continue that lands on the
switch level lowers to `break`, matching PHP's continue-targets-switch
semantics.
parseBreak/parseContinue now reject levels exceeding the number of
enclosing breakable constructs - the same compile-time validation PHP
performs (`Cannot 'break' 2 levels`) - which the countdown scheme
relies on to terminate at an enclosing construct.
The continue-2-while scenario in break-continue-level.phpt encoded the
old leaked behavior: its `$i++` after the inner loop only ran because of
the misplaced check; standard PHP loops forever on it. The counter now
advances before the inner loop.
- Removed unnecessary null check for left and right types in binary operation detection
- Eliminated redundant null check for closure statements before global collection
- Simplified type checking logic by relying on subsequent primitive type validation
- Improved code flow by removing early returns that were masking actual logic
- Removed CLAUDE.md guidance file for Claude Code usage
- Removed code reuse improvement plan documentation
- Removed encapsulation review documentation
- Updated composer.json to specify phpstan version constraint
- Fixed namespace issues in Extractor.php exception handling
- Renamed main function in bin/extractor.php to avoid conflicts
- Updated LICENSE to standard GPL format
- Implement bin/analyze-test-coverage.php command line tool for coverage analysis
- Add TestCoverageAnalyzer class to generate PHP version x feature x evidence matrix
- Create markdown and JSON output formats for coverage reports
- Integrate AST node coverage with explicit denominators from php-parser
- Add strict mode for CI validation of parse issues and fixture references
- Update ClassTest.php to reflect new object-to-array conversion behavior
- Modify MethodCallTrait to remove fatal error on missing toArray methods
- Add documentation for test coverage analyzer usage and format
- Include package-lock.json with sharp dependency for image processing
- Add test case for Zend class to array declaration with parameters rejection
- Add test case for Zend class to array declaration with wrong return type
- Add test case for known Zend class missing required toArray method
- Add test case for known Zend class resolving toArray through magic call
- Add test case for native class rejecting toArray parameters at declaration
- Update existing native class keyword return type test file
- Create new test file for native class to array parameters validation
- Add dynamic method test for toArray dispatch supporting real methods and __call
- Implement final promoted property parsing with explicit visibility requirement
- Add support for exit(message: $value) named argument syntax
- Reject unknown named arguments in exit/die function calls
- Enforce explicit visibility declaration for final promoted properties
- Add test coverage for final promoted property inheritance errors
- Update documentation for supported PHP 8.4+ features and limitations
- Generate proper reflection metadata for final promoted properties
- Support native class final promoted property compilation
- Extend Override attribute to support properties in addition to methods
- Add comprehensive property override validation including parent property matching
- Implement property shadowing prevention for private parent properties
- Add trait property override validation at use site
- Support promoted and hooked properties in override validation
- Update error messages to include property targets for override rejection
- Add internal marker preservation for property override validation
- Implement final property and property hook inheritance restrictions
- Add parent property hook call syntax support with validation
- Support PHP 8.4 final property metadata preservation in reflection
- Add property hook inheritance and reflection test cases
- Update native class property validation to prevent private property hiding
- Implement interface property hook final restriction
- Add property get hook reference return validation error
- Update stub generation with property
- Implement clone-with functionality with one/two argument support
- Add preservation of evaluation order for clone-with operations
- Support dynamic, numeric and object-valued properties in clone-with
- Handle error cases and stop property updates at first error
- Reject active references and unwrap sole remaining references
- Respect property scope and unlock readonly properties during cloning
- Add first-class callable and string callable support for clone
- Preserve lexical class scope in Zend call frames for method calls
- Update documentation with clone-with compatibility requirements
- Add comprehensive test coverage for clone-with features
- Implement scope argument handling in runtime function calls
- Add new NegativeCompatibilityTest class with 320 lines of test cases
- Implement closure and arrow function reference return validation with error messages
- Add foreach list destructuring reference binding validation and error reporting
- Move foreach by-reference variable validation to proper location in parser
- Update documentation to reflect foreach list destructuring reference limitations
- Refactor incompatibility classification documentation for clarity
- Remove outdated attribute argument limitation from documentation
- Create diagnostic reporter for controlled compiler boundary testing
- Add comprehensive test coverage for PHP incompatibility boundaries
- Ensure clean failure modes instead of crashes or invalid C++ emission
- Add support for __NAMESPACE__ magic constant resolution in namespace, class, and global scopes
- Add support for __PROPERTY__ magic constant that resolves to property name within property context
- Implement namespace\name relative name resolution for functions, constants, classes, and members
- Update AnonClassGenerator to properly parse identifiers instead of using toString()
- Extend CompilerBase to handle Scalar_MagicConst_Namespace and Scalar_MagicConst_Property
- Modify class inheritance and interface implementation to use parseIdentifier for proper name resolution
- Add comprehensive tests for namespace magic constant behavior across different scopes
- Add tests for property magic constant resolving correctly in property contexts only
- Implement runtime attribute factory lowering with proper namespace context handling
- Update visitor pattern to track property magic boundaries and namespace context correctly
- Document incompatibility with PHP 8.5 #[NoDiscard]
- Removed isConstFetch and isAssignOp methods from AstNodeType
- Removed corresponding test cases for isConstFetch and isAssignOp
- Updated CallArgumentGenerator to use setValue and appendValue instead of set and append
- Removed parseNamedCallArgs method from CallArgumentGenerator
- Removed unused getPlatformFullLinkFlags method from Clang backend
- Updated ClosureGenerator to use appendValue instead of append
- Removed abstract compileFile and linkObjects methods from CompilerBackend
- Removed unused getObjectPropVarInfo and getDecimalLiteralString methods
- Removed resetReturnType and mustNoCall methods from CompilerBase
- Removed testParseIncludesUsesNewArchitecture, testParseLdflagsUsesNewArchitecture,
testParseLibsUsesNewArchitecture and testBackwardCompatibility methods
- Removed isUniversalCtags property and related logic from Extractor
- Removed extractWithMetadata, generateStatistics and exportToMarkdown methods from Extractor
- Removed extractMetadata, extractComments, isPHPFunction and parseDocTags methods from Extractor
- Removed FINAL_TEST_SUCCESS.md file containing old test results
- Updated ArrayExpressionTrait to use setValue/appendValue methods when not by reference
- Changed set/append calls to respect byRef flag in array expressions
- Fixed assign operation to use appendValue instead of append for direct writes
- Added comprehensive test case for array write reference behavior
- Implemented proper dereferencing logic for array assignments
- Added cleanup script for TypePHP temporary files with safety checks
- Replace php_get_class with get_class throughout the codebase
- Replace php_get_persistent_class with get_persistent_class
- Replace php_get_func with get_func and update related helpers
- Replace php_get_persistent_method with get_persistent_method
- Replace php_get_persistent_prop with get_persistent_prop
- Replace php_globals_array with php::globalsArray
- Replace php_deindirect with php::deindirect
- Replace php_get_called_ce with php::getCalledCe
- Replace php_get_callable_scope with php::getCallableScope
- Replace php_get_str with get_str for literal string access
- Update build script to handle TypePHP project module accessor
- Add test case for helper symbol collision prevention
- Modify module initialization from app_init/app_clean to module_init/module_clean
- Cache GLOBALS variable check result to avoid redundant parsing
- Implement fast paths for typed array and string reads using direct method calls
- Add optimized handling for simple shorthand ternary operations
- Generate efficient C++ code for array dimension fetch with proper type detection
- Preserve PHP semantics for reference handling in array operations
- Add comprehensive test coverage for typed read optimizations
- Improve namespace isolation for generated runtime symbols across build modes
- Add optimized array write operations that preserve references and avoid temporaries
- Implement direct array append and item assignment for known array types
- Optimize compound assignment operations like += for known array slots
- Add compile-time expansion for $this::CONST class constant fetches
- Cache process-stable class entry pointers at function level to avoid repeated lookups
- Optimize string concatenation with two scalar operands using efficient overload
- Skip unnecessary type conversions for arguments that already have exact types
- Add comprehensive test coverage for optimized array operations and object creation
- Implement lazy initialization of request-scoped array default templates
- Generate efficient
- Change temporary variable type to Variant for PHP arrays to prevent undefined C++ behavior
- Add methodNameToStr helper that treats 'self' and 'static' as ordinary member names
- Replace identifierToStr with methodNameToStr for method calls to respect lexical rules
- Update nullsafe access trait to use proper method name parsing
- Add test case for array element compound arithmetic overflow handling
- Add test case for nullsafe chain preserving typed method returns
- Rename canFoldLocalLiteralIntoDeclaration to canFoldLocalInitializerIntoDeclaration
- Replace isDeclarationLiteral with isHoistSafeDeclarationInitializer
- Add support for hoisting compile-time class constants in addition to literals
- Implement isHoistSafeClassConstFetch to determine safe class constant hoisting
- Add isHoistSafeConstFetch to validate constant fetch safety for hoisting
- Update documentation to reflect hoist-safe value requirements
- Add tests for native scalar literals, compile-time constants and class constants
- Move internal constant value generation to separate method
- Refine constant resolution logic with namespace fallback handling
- Add validation for runtime constant dependencies in class constants
- Implement ArrayDef attribute for compile-time array type contracts
- Add support for list and map type definitions with key/value constraints
- Create ArrayDefinition and ArrayDefWritePlan data structures
- Integrate ArrayDef validation into assignment operations
- Add documentation for ArrayDef usage and limitations
- Support class value types, subclasses, and dynamic type checking
- Enable static property array dimension assignments
- Optimize literal assignments into variable declarations
- Add comprehensive test cases for various ArrayDef scenarios