* optimize: convert for-loop post-inc/dec to prefix to avoid zval copy
For-loop post-expressions (third clause of `for (init; cond; post)`) always
discard the return value, so $i++ is semantically identical to ++$i in this
context. Converting to prefix increment/decrement avoids the zval copy +
destructor overhead from operator++(int)'s return value, saving ~6ns per
increment per loop iteration.
Changes:
- LoopControlTrait: convert PostInc→PreInc, PostDec→PreDec in for-loop
post-expressions only (while/do-while body untouched)
- Add phpunit test verifying generated C++ contains ++i/--i for for-loops
and retains i++ in while/do-while bodies
- Add test data file with 7 test functions covering basic, multi-post,
nested, mixed, and while/do-while cases
* fix: restrict for-loop post-expr rewrite to simple variables only
- Add support for coalescing assignment to ArrayAccess objects with nested dimensions
- Implement proper handling of magic property access in coalescing operations
- Create new test cases covering array access, ArrayAccess implementation and magic property scenarios
- Refactor coalesce array access target resolution logic for better performance
- Add proper null checking and object detection for ArrayAccess operations
- Update phpx dependency from ~2.6.11 to ~2.6.12 for compatibility fixes
* fix(gen_stub): register enum-case class constants as persistent AST constants
A class constant valued by an enum case was registered with the folded
scalar (the backing value, or the case name for pure cases), so
constant('K::CB'), $cls::CB and reflection observed an int/string where
PHP has the case object, and K::CB === E::B was false on every dynamic
path. Enum case objects have request lifetime and can never sit in the
persistent class-entry tables, in any request-init rebinding scheme
least of all: writing a request-owned object into the shared table is
unsafe under concurrent ZTS requests.
Reuse the engine's own mechanism for internal enums instead: the
constant is declared as a persistent IS_CONSTANT_AST holding the
Enum::Case fetch, so Zend separates the class constants table into
request-local mutable storage on first access, evaluates the fetch
there, and cleans it up at request shutdown. Identity is preserved for
static access, constant(), dynamic class access and reflection, with no
module-lifecycle hooks and no registration-order sensitivity.
Case identity flows through compile-time constant evaluation as an
EnumCaseRef value instead of a scalar, so it also survives constant
expressions (true ? E::A : E::B), constant chains, typed class
constants (declared type and AST value are registered together), and
internal enum cases such as RoundingMode::HalfEven, which previously
aborted stub generation. The runtime expression path (php::getEnumCase)
is unchanged.
The preprocessor also no longer reads the raw ->value property off
arbitrary case expressions (`case A = 1 + 1;` warned and was recorded
as a pure case): only literal backing values are recorded eagerly, and
no compile-time consumer needs the evaluated scalar - gen_stub
evaluates the registration value from the AST itself.
* fix(gen_stub): constrain enum-case AST registration to class constants
The persistent IS_CONSTANT_AST representation leaked into property and
parameter defaults, whose persistent tables reject refcounted zvals:
startup died with "Internal zvals cannot be refcounted". EvaluatedValue
now carries the case identity in a dedicated field while its value
degrades to what those consumers read before case identity existed
(the host case object for internal enums, the literal backing value or
case name for compiled ones), and only class-constant registration opts
into the AST. Property/parameter defaults keep flowing through their
existing runtime-restore machinery unchanged.
Also parenthesize a folded constant operand before appending a member
access: the C++ ternary of `const VALUE = cond ? E::A : E::B;` bound
`.attr("value")` to its else branch only, so `K::VALUE->value`
evaluated to the case object instead of its backing value.
* fix(gen_stub): give persistent AST constants a complete teardown lifecycle
destroy_zend_class() asserts (in debug builds) that every persistent
AST constant remaining on an internal class is CONST_ENUM_INIT, and its
teardown frees only the allocation referenced by Z_AST — the previous
representation left a CLASS_CONST root behind (assertion failure at
shutdown on 8.4/8.5 debug builds) and leaked the two separately
allocated children.
The AST is now built in one contiguous persistent allocation (ast_ref,
root, both zval children — mirroring Zend's own persistent enum AST
builder), and every generated file with AST constants emits a release
function that runs from the module's MSHUTDOWN, before Zend's class
teardown: it frees the single block and restores the constant slot to
null, so destroy_zend_class() never sees a foreign AST. Request-local
mutable copies are unaffected (no request is live at MSHUTDOWN).
CONST_ENUM_INIT itself is not usable here: that node constructs a new
case object rather than fetching the canonical registered one, which
would break case identity again.
* fix(codegen): reject lifecycles that cannot release AST constants
The typephp_release_ast_constants_*() teardown ran only from MSHUTDOWN,
which is not a general pre-class-destruction hook: for a
MODULE_TEMPORARY module loaded through dl(), module_destructor() runs
clean_module_classes() before the shutdown callback, so the foreign
ZEND_AST_CLASS_CONST reached destroy_zend_class() first and still
tripped the debug assertion; and a MINIT that fails after registering
such a constant never sets module_started, so MSHUTDOWN is not
guaranteed to run at all.
The generated module now enforces the lifecycle contract instead of
assuming it. When the module declares any enum-case AST constant, MINIT
opens with a guard that rejects MODULE_TEMPORARY (zend_error E_WARNING,
return FAILURE) before a single class is registered — with nothing in
the class table, teardown is trivially safe. MINIT is also restructured
so that every step that can return FAILURE precedes the first
register_class_*() call: the AST constants are installed by the
infallible tail (class registration, then symbol registration), so a
FAILURE return can never leave a foreign AST in the persistent tables.
The generator itself throws if a future change introduces a FAILURE
return after registration begins. The MSHUTDOWN release is unchanged
and remains the supported, persistent-module path.
EnumCaseAstConstantLifecycleTest asserts the guard exists exactly when
AST constants exist, that it precedes every registration step, that no
FAILURE return follows the first class registration, and that MSHUTDOWN
releases the constants before any other teardown. The enum-case phpt
gains a never-accessed constant so the full process shutdown it already
performs also covers a pristine persistent AST; a dl()-path phpt is not
feasible because the harness only builds standalone binaries whose
module is registered persistently (documented in the test file).
* fix(codegen): keep Zend operand read order around hoisted side effects
Lowering a later call argument or concat operand that materializes
captured statements (an assignment, a call result) appended them to the
enclosing statement, executing the side effect before earlier operands
were read: two($j, $j = 5) with $j = 1 produced "5,5" (Zend "1,5") and
$m . "," . ($m = 9) produced "9,9" (Zend "1,9").
Call arguments: Zend SENDs strictly left to right, so when a later
argument hoists statements, every earlier by-value plain-variable
argument is snapshotted into a temporary at its own argument position.
By-reference parameters, unpacked arguments, $this and $GLOBALS are
left alone.
Concat chains: Zend reads a CV operand when its CONCAT opcode executes,
so in the left-associated chain the first two items are read together
at the first op (after both items' side effects: $s . ($s = 'b') . $s
is "bbb") and each later item after the side effects of everything up
to itself. The flattened braced-list lowering now snapshots a
plain-variable item exactly at that read position, deferring the first
item's snapshot until the second item has been lowered.
Plain arithmetic is intentionally unchanged: Zend's ADD reads the CV at
op time, so $k + ($k = 5) is 10 in both worlds, and the existing
codegen already matches.
* test(codegen): platform-neutral integer-literal suffixes in eval-order assertions
* fix(codegen): detect wrapped side effects when ordering operand reads
Zend sends call arguments strictly left to right and reads a concat
operand's CV when its opcode executes: an assignment nested in a later
argument runs after every earlier by-value argument has been sent, so
pairValue($i, (int) ($i = 5)) passes the old value ("1,5"), and the
same holds when the assignment is wrapped in !, unary +/-, ~, or @.
shouldMaterializeOrderedOperand() only descended through BinaryOp
nodes, so an assignment inside any other expression wrapper was not
classified as side-effecting: no earlier operand was snapshotted and
the assignment could even stay inline in the C++ argument list
(php::toInt(i = 5LL)), mutating the variable at an unsequenced point.
Deriving the decision from the captured statements lowering produces
would miss exactly these inline cases, so the classifier now recurses
structurally through every sub-expression: any wrapper of a
side-effecting node is itself side-effecting. Closure and arrow
function bodies do not run at creation time and stop the walk; throw,
yield, yield from, and backtick expressions join the side-effecting
leaves.
Zend's ADD opcode still reads its CV at op time, so $k + ($k = 5)
keeps its existing codegen (10 in both worlds, no snapshot).
* fix(codegen): type materialized wrapper operands as dynamic temporaries
A wrapper expression around a side effect (-strlen($s), a cast, error
suppression) is now materialized for evaluation order, but the temp-type
fallback typed it by the detected PHP type. The lowered C++ can still be
dynamic — an unqualified namespaced call lowers to php::call(...), a
Variant — so a php::Int temporary failed to compile in the self-build.
Follow the call/binary-op policy: native scalar types only in
native-types mode, otherwise a dynamic temporary.
* fix(codegen): route typed division, int modulo and shifts through PHP operators
Typed int/int and float-typed division fell through to a raw C++ '/':
7 / 2 on zend_long operands truncated to 3 where PHP returns 3.5,
integer division by zero was undefined behavior and float division by
zero produced INF, while PHP raises a catchable DivisionByZeroError in
both cases; PHP_INT_MIN / -1 also has UB in C++ but promotes to float
in PHP. The '%' guard only routed through php::fn::mod when NOT both
operands were int, so both-int modulo kept raw C++ '%' (UB for a zero
divisor and for PHP_INT_MIN % -1, which PHP defines as 0). Dynamic int
shifts were raw C++ too: PHP defines counts >= the word size as 0 (or
-1 for negative right shifts) and raises ArithmeticError for negative
counts, both undefined in C++.
Route all of these through the encapsulated php::Var operators /
php::fn::mod in non-native mode, matching the existing +/-/* pattern.
Constant folds are untouched; constant shifts that C++ defines
identically to PHP still emit raw operators.
* fix(parser): defer literal zero divisors to the runtime DivisionByZeroError
A literal `/ 0` or `% 0` (including `/=` and `%=`) was a compile-time
fatal, rejecting valid PHP: Zend compiles it and raises a catchable
DivisionByZeroError only when the statement executes, so dead or
guarded code like `if ($cond) { $x = 1 % 0; }` must compile. The
equivalent spellings `1 % (1 - 1)` and `10 / ZERO` were already
accepted and lowered to the catchable runtime error.
Give the literal spelling the same lowering: route the operation
through the encapsulated Variant operators (compound assignments on
Variant slots already defer via operator/= and operator%=), keep a
compile-time warning in normal mode, and keep the fatal in native mode
where the C++ operation would be undefined behavior.
The six OperatorTest cases asserting the old compile-time fatal now
assert the runtime-error lowering instead.
* test(operator): platform-neutral literal suffixes, PHP division on typed ints
The literal-division assertions hardcoded the macOS zend_long suffix
(LL); Linux emits L, so they now match either. native-type.phpt
asserted the truncating int division this change removes: division on
typed int operands follows PHP semantics in non-native mode, consistent
with the pre-existing + - * routing (use native_types keeps raw
division), so std::int(10) / 4 is now float(2.5).
* fix(codegen): literal zero divisors on native scalar slots raise the runtime error
Downgrading the literal-zero compile fatal to a warning exposed the raw
C++ compound path on typed native slots: `int $value; $value /= 0`
compiled to `value /= php::toInt(0L)` and killed the process with
SIGFPE instead of the catchable DivisionByZeroError (`%= 0` likewise;
float `/= 0.0` produced INF). A proven zero divisor always throws
before any assignment happens, so the whole compound lowers to the
PHP-semantics binary operation through php::Var and the target is left
untouched. Native-types mode keeps the compile-time rejection.
* fix(codegen): exclude explicit native scalars from PHP arithmetic routing
std::int()/std::float() opt into native C++ arithmetic independently of
the file-wide native_types declaration, and the existing + - * routing
already honors that via isExplicitNativeArithmeticExpr(). The new
division, both-int modulo and dynamic shift branches, and the
literal-zero compound lowering, bypassed it: std::int(10) / 4 changed
from int(2) to float(2.5) and native-type.phpt was updated to encode
the regression.
Every new PHP-semantics branch now skips explicitly native operands,
native-type.phpt is restored to int(2), and a proven zero divisor on an
explicit native slot keeps the compile-time rejection used by
native_types mode instead of being silently rerouted to PHP semantics.
Boundary coverage added on both sides: ordinary typed parameters keep
PHP behavior (7 / 2 is 3.5, -7 % 2 is -1) while std::int()/std::float()
keep native division, modulo and shifts.
* 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