* fix(preprocessor): reject abstract method bodies and abstract private class methods
Two abstract-method rules Zend enforces at compile time were missing:
- an abstract method with a body was accepted and the body silently
dropped; Zend fatals with "Abstract function A::f() cannot contain
body" (applies to classes and traits alike, probed on 8.4.13)
- `abstract private function` in a class can never be implemented,
since private methods do not participate in overriding; Zend fatals
with "Abstract function A::f() cannot be declared private". Traits
keep accepting it (allowed since PHP 8.0: the consuming class
supplies the private implementation)
Zend reports the private-modifier error before the body error when both
apply; the checks are ordered to match.
* test(preprocessor): cover abstract method declaration rules
checkParentMethodCanBeOverridden()'s internal-parent branch only checked
the PRIVATE and FINAL modifiers via reflection and then stopped, so an
override of any Zend built-in method was never signature-checked:
narrowed parameters, static/instance mismatches, narrowed visibility and
incompatible real return types were all accepted (all fatal in Zend,
e.g. "Declaration of C::offsetGet(int $key): string must be compatible
with ArrayObject::offsetGet(mixed $key): mixed").
Add validateInternalMethodOverrideSignature(), mirroring Zend's
zend_do_perform_implementation_check on host ReflectionMethod data:
- visibility may widen but not narrow; staticness must match;
- a by-ref return may be added but not dropped;
- the child may not require more arguments; extra parameters must be
optional or variadic;
- parameters are contravariant with invariant by-ref-ness, and a
trailing child variadic absorbs remaining parent positions (a
variadic parent requires a variadic child);
- the return type is covariant, enforced ONLY for real return types:
ReflectionMethod::getReturnType() is null for TENTATIVE return
types, which Zend merely deprecates on mismatch, never fatals.
ReflectionType data (named/nullable/union/intersection, incl. self,
parent and static) is mapped into the existing accepted-types DNF so the
comparison reuses isReturnTypeCoveredBy()/isAcceptedTypeSubset().
* fix(translator): make typed class-constant overrides covariant
checkConstantOverride() required exact type equality between a child
constant and the parent's declared type, rejecting valid PHP 8.3
programs: class constant types are covariant, so a child may narrow
(parent `const int|string X` overridden by `const int X`, or
`?int` by `int`) but never widen or move to an unrelated type
(Zend: "Type of B::X must be compatible with A::X of type int").
Composite declared types (unions, nullables) were also collapsed to a
single variant type at parse time, making them unrepresentable in the
check. ConstantDef now records the accepted-types DNF of its declared
type (built by the existing buildTypeCheckFromNode machinery in a
parseClassConstDef override, while the declaration's name-resolution
context is still active), and the override check reuses the DNF
clause-subtyping used for covariant returns. Untyped parent constants
remain unchecked, and a typed parent still requires a typed child.
* fix(translator): validate interface constant contracts
Interface constants were never validated: checkInterfaceImplementation()
had no constants loop and checkConstantOverride() only walks the class
extends chain. Incompatible retypings, final-constant overrides,
narrowed visibility and ambiguous multi-interface inheritance were all
accepted (all fatal in Zend 8.4).
Model Zend's constants-table merge (zend_do_inheritance +
do_inherit_constant_check): a class-like's effective table is built from
the parent class's table (private constants are not inherited), its own
declarations, then its interfaces, each entry keeping the ORIGINAL
declaring class/interface. When a same-name constant arrives from a
different declaration:
- a FINAL inherited constant cannot be overridden — "C::X cannot
override final constant I::X" — including through an ancestor class
that implemented the interface (the origin travels with the entry);
- two different declarations are ambiguous unless the type declares
the constant itself — "Class C inherits both I1::X and I2::X,
which is ambiguous" (a diamond of one declaration is fine);
- an override of an interface constant must stay public — "Access
level to C::X must be public (as in interface I)";
- a typed interface constant requires a typed, covariant override; an
untyped one may be redefined freely.
The same validation runs for interfaces extending interfaces and for
enums implementing interfaces. Enum cases live in a separate table in
Zend and never conflict with inherited constants.
checkPropertyOverride() compared type, visibility, set-visibility,
readonly and final between a child property and the parent's, but never
Modifiers::STATIC. Redeclaring `public static int $x` as
`public int $x` (or the reverse) was accepted, while Zend fatals with
"Cannot redeclare static A::$x as non static B::$x" (and "Cannot
redeclare non static ... as static ..." in the other direction):
static and instance properties are different kinds of storage and can
never override one another.
Zend's zend_do_perform_implementation_check does not compare variadic-ness
per position. Its rules are:
- a variadic parent requires a variadic child (unbounded contract);
- a trailing child variadic stands in for every remaining parent
position (decorator pattern), with the variadic's type checked for
contravariance against each covered parent parameter and by-ref-ness
matched per position;
- when the parent is variadic, extra child parameters are validated
against the parent's variadic slot.
validateMethodOverrideSignature required an exact per-position variadic
match, rejecting valid programs such as parent f(int $a, int $b)
overridden by f(int ...$args). Rework the position loop per the Zend
rules; the required-argument-count and extra-optional-parameter checks
are unchanged.
The pre-existing testVariadicMismatch expectation (untyped f($x)
overridden by f(...$x) must fail) contradicts Zend 8.4, which accepts
it; the test now asserts the program compiles.
* 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.
* 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>
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.
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++.
doFoldKnownClass folds class_exists() whenever the name is a literal the
symbol table knows. That table also holds traits, so a trait name folded
to true while PHP answers false:
trait Helper {}
class_exists('Helper'); // folded to true
$name = 'Helper';
class_exists($name); // reaches php::fn::class_exists, answers false
The same program therefore gives two different answers for the same
trait, decided only by whether the argument is a literal.
The runtime side is already right, and deliberately so: traits are
compile-time AST templates in TypePHP, which is why
tests/compiler/stdlib/class_exists.phpt expects trait_exists() to be
false. Only the constant fold disagreed - with PHP and with the
compiler's own runtime.
A trait name now folds to false. Classes and enums keep folding to true,
which matches PHP: an enum is a class, a trait is not.
class_exists.phpt gains the literal and non-literal trait cases, and
ClassExistsTraitFoldTest pins the fold decision in the generated C++.
doFoldCountLiteral replaced `count([...])` with the number of AST items
and dropped the array literal entirely. That is only correct when the
item count equals the runtime element count and no element carries an
observable effect. Three common shapes break both assumptions:
count([bump(), bump()]); // folded to 2, bump() never ran
count(['a' => 1, 'a' => 2]); // folded to 2, PHP counts 1
count([...$rest, 9]); // folded to 2, PHP counts 6
The spread case is the most damaging: it silently yields a wrong number
in ordinary code that compiles without any diagnostic.
The fold now applies only when every item is unkeyed, is not a spread,
and holds an expression whose evaluation cannot be observed - a scalar,
a constant fetch, a unary sign over either, a nested literal that is
itself foldable, or a variable already known to be defined. An undefined
variable still reaches the dynamic path so it reports the same
diagnostic as PHP. Everything else keeps the runtime php::fn::count()
call, so `count([1, 2, 3])` and friends still fold as before.
Covered by tests/compiler/array/count-literal-fold.phpt for the runtime
semantics and by CountLiteralFoldTest for the fold/no-fold decision in
the generated C++.
- Add useImportAliases property to track import aliases by type domain
- Implement registerUseImportAlias method with duplicate detection logic
- Update use declaration processing to utilize import alias registration
- Make class and function aliases case-insensitive while constants remain case-sensitive
- Add comprehensive test coverage for import alias domain isolation
- Include negative test cases for duplicate alias detection across domains
- Move AST name resolution earlier in preprocessing steps
- Add proper error handling for PHP parser errors during traversal
- 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
- Add support for (int), (bool), (float), (string), (array), (object) casts
in constant expressions across all declaration contexts
- Implement proper evaluation of cast expressions during stub generation
- Add comprehensive test coverage for constant expression casts in various
contexts including global constants, class constants, properties, and
parameters
- Update documentation to reflect PHP 8.5 cast support
- Enhance RuntimeAttributeFactoryLowering to handle array cast nodes
correctly
- Add validation and error handling for unsupported cast types in
constant expressions
- 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
- Implement validation for promoted asymmetric properties requiring explicit types
- Add error handling for promoted asymmetric properties with wider set visibility
- Support constructor property promotion with asymmetric visibility modifiers
- Enable proper scope checking for promoted asymmetric properties in Zend-backed objects
- Preserve promoted/set visibility and implicit final reflection flags
- Add comprehensive test coverage for promoted asymmetric property scenarios
- Implement native object compilation support for asymmetric property access checks
- Remove pending status for ReflectionProperty::isPromoted() implementation
- Update documentation to reflect complete asymmetric property visibility support
- 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 DNF method signatures with parameter contravariance and return covariance
- Add new test cases for DNF inheritance variance scenarios
- Extend stub generation to handle DNF union and intersection types
- Create DNF type declaration code generation in gen_stub.php
- Add support for DNF properties and dynamic property writes
- Implement closure support with DNF parameters and return types
- Add error handling for invalid DNF type operations
- Update inheritance checks to validate DNF parameter and return type compatibility
- Introduce test cases for DNF inheritance errors
- Refactor type comparison logic to support DNF clause subtyping relations
- 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
- Introduce LibraryExportedNative class with #[Native] attribute
- Add LibraryHiddenNative class with #[NoExport] and #[Native] attributes
- Create stub file for native class definitions
- Implement StubNativePoint class with native annotation and basic properties
- Add length method to StubNativePoint for geometric calculations
- Enable compiler support for native class annotations
- 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
- Replace php::safeIndex with php::safeArrayIndex for consistent boundary checking
- Remove special case handling for property[count(property)] append operations
- Use zend_hash_next_free_element() based append position instead of length()
- Update tests to verify inclusive upper bound behavior for sparse arrays
- Add comprehensive test coverage for unset() hole scenarios
- Modify documentation to reflect new indexing behavior and boundaries
- 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