- Add test cases for forbidden magic methods in enums (__construct, __destruct, __clone, etc.)
- Create tests for trait-injected forbidden methods failing during composition
- Add tests for trait aliases to forbidden methods detection
- Verify __call, __callStatic and __invoke remain allowed in enums
- Include proper setup and teardown for temporary test directories
- Provide data provider for forbidden magic method testing scenarios
feat(enum): enforce forbidden magic method restrictions on enums
- Add ENUM_FORBIDDEN_MAGIC_METHODS constant with restricted methods list
- Implement assertEnumMayIncludeMethod to validate enum method declarations
- Prevent forbidden magic methods from being added to enums during preprocessing
- Share validation logic between direct method declarations and trait composition
- Add
* fix(preprocessor): reject variadic promoted properties and callable property/constant types
Two promotion/type gaps against Zend (probed on 8.4.13):
- `__construct(public int ...$x)` was accepted and even registered
the property before the variadic-position check ran. A variadic
parameter collects its arguments into an array, so there is no single
value to promote; Zend fatals with "Cannot declare variadic promoted
property". The check now precedes the property registration.
- `callable` is a calling-scope-dependent type, so Zend forbids it in
property types (declared, promoted, interface hooked) and class
constant types (class and interface), bare or as a nullable/union
member: "Property A::$x cannot have type ?callable" /
"Class constant A::X cannot have type callable". Intersection
members are left to the compound-type validation, which rejects
every non-class standard type there.
`void`/`never` property and parameter types were already rejected by
parseTypeDecl ("The type `void`/`never` is allowed only for return
type") - verified, no change needed; union members are covered by the
compound-type validation.
* test(preprocessor): cover promotion and property/constant type rules
* fix(preprocessor): reject callable inside intersection and DNF types
typeDeclContainsCallable() deliberately skipped IntersectionType, so a
DNF-nested callable such as `public (Traversable&callable)|stdClass
$value;` sailed past the property checks and died in gen_stub on
assert(!$type->isBuiltin); a bare `Traversable&callable` property
compiled outright.
Zend rejects callable while compiling the intersection type itself,
with its own diagnostic ("Type callable cannot be part of an
intersection type", probed on 8.4.13), in every declaration context and
ahead of the property/constant-specific bans — `callable|(Traversable&
callable)` reports the intersection conflict, not the property one.
A dedicated assertTypeDeclIntersectionsHaveNoCallable() walk (nullable,
union, intersection members) now runs before the existing
typeDeclContainsCallable() checks in all contexts this branch guards:
class properties, promoted properties (both via addClassProperty),
typed class constants, and interface properties/constants. Tests cover
the bare intersection member, DNF in first and second union member,
the promoted and constant/interface variants, and a callable-free DNF
property that must keep compiling.
* fix(preprocessor): validate callable intersections on the common type-declaration path
assertTypeDeclIntersectionsHaveNoCallable() was invoked only from the
property and class/interface-constant paths, so the same invalid type
in a function parameter or return declaration bypassed the check and
reached the later generator path:
function consume(Traversable&callable $value): void {}
function produce(): Traversable&callable {}
Zend rejects both while compiling the type itself ("Type callable
cannot be part of an intersection type", probed on 8.4.13), in every
declaration context.
The walk now lives in parseTypeDecl(), the declaration funnel behind
resolveTypeDecl() that parameters, returns, properties, promoted
properties, class and interface constants, and interface hooked
properties already flow through; the per-context calls are gone, and
the diagnostic points at the offending intersection member. Closure
and arrow-function signatures resolved no full type node anywhere, so
doGenClosure() now routes them through the same funnel - except bare
class names, which the native-object walk there already resolves (and,
inside trait methods, rewrites) via parseTypeDecl().
The property and constant paths resolve the declaration before
applying their own bare/nullable/union callable bans, so a type like
`callable|(Traversable&callable)` keeps reporting the intersection
conflict first, as Zend does.
New negative tests: parameter and return intersections, callable in a
DNF parameter member, and closure parameter and return intersections
(each probed against Zend 8.4.13); positive tests keep bare `callable`
parameters and callable-free DNF properties compiling.
* 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(preprocessor): enforce Zend interface member declaration rules
parseInterface accepted several declarations Zend rejects at compile
time (all wordings probed on 8.4.13, which renamed the modifier errors
to "must not be abstract/final"):
- interface method with a body ("Interface function I::f() cannot
contain body")
- private/protected interface method ("Access type for interface
method I::f() must be public")
- explicit `abstract` modifier on an interface method ("Interface
method I::f() must not be abstract")
- `final` interface method ("Interface method I::f() must not be
final")
- private/protected interface constant ("Access type for interface
constant I::X must be public"); `final` interface constants remain
legal per PHP 8.1
- explicit `abstract` on an interface hooked property ("Property in
interface cannot be explicitly abstract...")
- `interface I extends A` where A is a known class, enum, or trait
("I cannot implement A - it is not an interface"); only checked when
A's declaration has already been prepared - a parent declared later
is left to the Translator (deferred to integrator)
- the same interface listed twice in extends ("Interface I cannot
implement previously implemented interface A")
Zend's precedence for combined modifier violations (visibility, then
abstract, then final, then body) is preserved.
* fix(translator): validate same-name methods when interfaces merge
Two interfaces declaring the same method were never cross-checked:
`interface J extends I1, I2` and a class implementing both compiled
even when the declarations were mutually incompatible (Zend:
"Declaration of I1::f(): int must be compatible with I2::f(): string").
The first-seen declaration is now validated as an override of every
later one, mirroring Zend's merge order; diamond inheritance of one
original declaration never conflicts, and a method the class chain
defines silences the pairwise check (it is validated against each
interface individually instead) — all probed against Zend 8.4.
* fix(translator): reject an interface extending a class, validate merged methods
An interface can only extend other interfaces: naming a class either
fataled with a misleading missing-symbol message (declaration seen
earlier) or compiled silently (declaration appearing later). The
translation phase now rejects both with Zend's wording.
Same-name methods arriving from several extended interfaces (or from
several interfaces a class implements without defining the method) were
never cross-checked; the first-seen declaration is now validated as an
override of every later one, matching Zend's merge order, with diamond
inheritance of one original declaration exempt.
* fix(translator): guard the interface method table against extends cycles
getEffectiveInterfaceMethodTable() recursed forever on a cyclic extends
graph (interface A extends B; interface B extends A). Zend never reaches
this state - declarations are linked one at a time, so the first one
already fails with 'Interface "B" not found' - but ahead-of-time the
whole graph exists before linking, so the cycle must be detected.
Track the tables being built in a visiting set (cleared with
try/finally) and fail promptly with the same stable diagnostic the
constants table uses ('Interface inheritance cycle detected at ...'),
so the helper is safe regardless of which validation pass reaches the
cycle first. Diamond (non-cyclic) graphs still converge through the
memoized table.
Covered by a negative test on the two-interface cycle; the diamond case
is already exercised by interface_collision_valid.php.
* fix(preprocessor): enforce Zend readonly property declaration rules
The readonly checks previously lived only in the Native-class branch;
ZendVM-backed classes accepted declarations Zend rejects at compile
time. addClassProperty now enforces, for declared and promoted
properties alike (probed against Zend 8.4.13):
- readonly property with a default value ("Readonly property A::$x
cannot have default value") - a readonly property carries runtime
initialization state, so a compile-time default is meaningless
- untyped readonly property, including untyped promoted readonly ctor
params ("Readonly property A::$x must have type")
- static readonly ("Static property A::$x cannot be readonly")
- a `readonly class` applies the same three rules to every property:
the class-level Modifiers::READONLY flag (already recorded on
ClassDef->flags for the Translator-side inheritance check) is OR-ed
into the per-property check
Promoted readonly params keep accepting parameter defaults: the default
belongs to the constructor argument, not the property (Zend-verified).
The inheritance_error_prop_readonly fixture used `readonly int $x = 2`,
which Zend itself rejects with the default-value error before ever
reaching the readonly-mismatch link error; the default is dropped so the
fixture still exercises the inheritance mismatch.
* fix(translator): enforce readonly-class inheritance in both directions
Zend seals readonly-ness across a hierarchy: a non-readonly class cannot
extend a readonly one and vice versa. Both directions compiled silently.
* fix(translator): complete readonly-class contracts for traits, internal parents and attributes
Three readonly-class rules Zend enforces at compile time were still
accepted (all probed on 8.4.13):
- A trait property keeps its own declaration; the consuming class's
readonly modifier does not upgrade it, so composing a non-readonly
(or static, which can never be readonly) trait property into a
readonly class fails: "Readonly class C cannot use trait with a
non-readonly property T::$value". The check runs in composeTraitAst's
property pass, which also matches Zend's naming of the directly used
trait when the property originates in a nested trait. A trait property
declared readonly composes fine.
- The readonly inheritance check only covered compiled parents; classes
extending internal ones skipped it entirely, so `readonly class C
extends ArrayObject {}` compiled. Internal parents now consult host
reflection (ReflectionClass::isReadOnly), keeping the contract
two-directional: the host runtime also knows internal readonly classes
(BcMath\Number, Dom\NamespaceInfo — both final in 8.4, so only the
readonly-child direction is reachable today).
- #[AllowDynamicProperties] contradicts readonly semantics (every
property is readonly and declared); Zend rejects the combination:
"Cannot apply #[AllowDynamicProperties] to readonly class C". The
pre-existing readonly-class.phpt carried exactly this invalid
combination and is adjusted to stay a valid positive test.
* fix(preprocessor): enforce property-hook placement rules for class properties
The interface path already validated hook placement; class and trait
properties accepted every combination. parseClassPropertyDef now
mirrors Zend's compile-time rules (probed on 8.4.13, including the
precedence order static -> readonly -> abstract rules):
- hooks on a static property ("Cannot declare hooks for static
property")
- hooks on a readonly property, including properties made readonly by
a `readonly class` ("Hooked properties cannot be readonly")
- `abstract` on a hook-less property ("Only hooked properties may be
declared abstract")
- abstract hooked property with a default value ("Cannot specify
default value for virtual hooked property A::$x")
- abstract hooked property whose hooks all have bodies ("Abstract
property A::$x must specify at least one abstract hook")
- abstract hooked property in a non-abstract class; traits stay exempt
(the consuming class satisfies the hook) and enums are already
rejected by the property ban
- bodiless hook on a non-abstract property, in classes and traits
("Non-abstract property hook must have a body"); previously the
lowering fabricated a concrete backing-store accessor for it
* test(preprocessor): cover property-hook placement rules
* fix(preprocessor): reject abstract-private, abstract-final and final-private hooks
Three hook-level modifier conflicts Zend rejects at compile time were
still accepted by the class/trait property path (all probed on 8.4.13):
- `abstract private int $x { get; }` — an abstract (bodiless) hook must
be implemented by a subclass, which private visibility forbids
("Property hook cannot be both abstract and private"). Unlike abstract
private trait methods, Zend does NOT exempt traits from this rule.
- `abstract public int $x { final get; }` — a bodiless hook must stay
overridable to ever gain a body ("Property hook cannot be both
abstract and final").
- `private int $x { final get => 1; }` — a final hook on a private
property is meaningless because private members cannot be overridden
("Property hook cannot be both final and private").
Diagnostic precedence follows Zend: static, then readonly, then the
per-hook final+private conflict (which wins over both abstract
conflicts: `abstract private int $x { final get; }` reports
final+private), then per bodiless hook abstract+private before
abstract+final, all ahead of the default-value and
at-least-one-abstract-hook rules. Protected abstract hooks remain
legal in classes and traits.
* 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
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
- 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
- Remove TYPEPHP_PROJECT_NAME and TYPEPHP_RUNTIME_EXPORTS from common compile options
- Add dedicated getProjectRuntimeEntryCompileCommandOptions method for project-specific defines
- Update testEmbeddedCompileOptionsPassProjectNameForModuleAccessor to reflect new behavior
- Create new testProjectRuntimeEntryHasTargetDefineWithoutPchOrObjectCache
- Add testProjectIndependentMiscObjectsUseSharedCacheScope and testProjectIndependentMiscObjectCacheSurvivesTargetNameChange
- Update composer.json to use swoole/phpx ~2.6.4
- Modify object file caching logic to handle project-specific vs shared misc objects
- Add isProjectRuntimeEntryFile helper method for determining cache scope
- Update compileFile method to use source-specific compile options
- Handle different PHPX runtime source file layouts during embed mode builds
- 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
- 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
- Prevent native classes from being exported through library stubs with proper error messages
- Add test cases to verify rejection of exported native classes in library builds
- Validate that NoExport native classes are omitted from library stub generation
- Implement check for native class usage in stub files with appropriate error handling
- Update hot path codegen to use appendValue method instead of append
- Add comprehensive test coverage for native class validation scenarios
- Introduce $allowLocalClassEntryHoisting flag to control class entry hoisting behavior
- Add withoutLocalClassEntryHoisting method to temporarily disable hoisting during callback execution
- Modify getLocalClassEntryPtr to respect hoisting allowance when checking process stable class
- Wrap parameter default parsing in withoutLocalClassEntryHoisting to prevent unwanted hoisting
- Update array initialization in Preprocessor to use hoisting control for literal arrays
- Add support for new expressions in array parameter defaults with proper class resolution
- Include tests for array parameter defaults containing new expressions
- Add tests for runtime array property defaults with copy-on-write behavior
- Rename generated request array default symbols with 'typephp_' prefix for consistency
- Update documentation to reflect implemented optimizations and remaining costs
- 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
- 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