- Removed strtotime from the list of functions that use direct PHPX wrappers
- Updated test case to verify only supported datetime calls use direct wrappers
- Modified strtotime test to include absolute, relative, epoch 0 and invalid dates
- Added timezone setting and additional test cases for relative date handling
- Updated expected test output to reflect new strtotime behavior
- Adjusted assertions to check for correct persistent function calls
* fix(compiler): preserve runtime class dispatch and introspection for polymorphic objects
When an object variable has an upper-bound declared type (e.g. assigned from
a function or method return), SSA marks it as a stable object. However,
stableObjects does not guarantee the concrete runtime class: the instance
may be a subclass at runtime, or null if nullable.
1. FuncCallOptimizer::genGetClassOptimized checked isStableObject($obj->name)
and folded get_class($obj) to the declared type literal string. For
$a = getAnimal() returning a Dog, get_class($a) was incorrectly folded
to "Animal" at compile time.
2. MethodCallTrait::parseStaticCall checked isStableObject($class) and
jumped to _do_call with the declared base class. For $a::who(), this
statically invoked Base::who() instead of dispatching to Dog::who() on
the runtime object.
Require proven exact object instances (exactObjects, populated from direct
new ConcreteClass() definitions) before folding get_class() or devirtualizing
static calls on object variables. Non-exact variables fall back to runtime
class resolution (php::fn::get_class() and php::callStaticMethod()),
matching PHP semantics.
Add unit test coverage verifying exact objects continue to fold/devirtualize
while polymorphic variables retain dynamic runtime dispatch.
* fix(compiler): preserve lexical scope in dynamic static calls and runtime type checks for get_class
- Use php::callScoped with getCallableScopeExpr() for dynamic static calls inside class methods to preserve caller visibility on protected methods
- Fall back to runtime php::call for get_class on non-exact values so nullable objects throw TypeError instead of dereferencing null
- Add behavioral PHPT tests for protected polymorphic static calls and nullable get_class
- Update unit tests for scoped dispatch and runtime introspection
* test(compiler): remove stray top-level main call from PHPT tests
In TypePHP AOT compilation, function main(): void serves as the binary
entrypoint. Top-level function calls outside a function are rejected by
the preprocessor as stray code.
* fix(optimizer): round() must not lower a RoundingMode enum to an int
Since PHP 8.4 the third parameter of round() is a RoundingMode enum, but
php::fn::round() models the mode as an Int, which only covers the legacy
PHP_ROUND_* constants. genRound sent the argument through convertIntExpr
regardless, so the enum went through an object-to-int conversion that
yields 1 - PHP_ROUND_HALF_UP:
round(2.5, 0, RoundingMode::HalfEven);
// compiled: Warning: Object of class RoundingMode could not be
// converted to int
// compiled: 3
// PHP: 2
Banker's rounding silently became half away from zero. Code that spells
out HalfEven is usually money code, where that is the exact difference it
was avoiding.
A mode that is not statically an int now falls through to the dynamic
path, which passes the enum to the runtime function unchanged. The legacy
integer constants keep the native call - PHP_ROUND_HALF_DOWN still lowers
to a plain 2L - and the one and two argument forms are untouched.
Only the compile-time lowering is covered by a test here. A runtime PHPT
cannot pass yet: phpx resolves a class constant on an internal class by
reading the raw zval out of the constants table, so RoundingMode::HalfEven
does not materialise at all. That is reported separately; once it ships,
the runtime case can be added to type_conv-style coverage.
* fix(optimizer): send every explicit round() mode to the dynamic path
Checking only Type::INT was not enough. php::fn::round() calls
_php_math_round() directly and never runs Zend's validation of the mode,
so an integer outside 1-8 reaches php_round_helper and terminates the
process rather than raising ValueError:
round(2.5, 0, 99); // segmentation fault
A static int type does not prove the runtime value is a valid mode, so
an int variable reaches the same path. Since three-argument round() is
uncommon, take the conservative option and route every call with an
explicit mode to the dynamic Zend path, which validates the argument and
accepts both a RoundingMode enum and a legacy PHP_ROUND_* constant.
Reject unpacked and named arguments as well: they carry a single
Node\Arg whatever their runtime arity is, so genRound() was reading the
unpacked array as the number being rounded.
Add tests/compiler/stdlib/round-mode.phpt covering valid legacy modes,
out-of-range literal and variable modes, and full and partial unpacking.
The enum case still cannot produce PHP's result until the swoole/phpx
class-constant fix is part of the pinned dependency, so it stays out of
the runtime coverage for now.
---------
Co-authored-by: Giandonn <lucas_raineri@hotmail.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.
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++.
doFoldSsaType folded a statically type-known is_int()/is_float()/
is_bool() call to the literal `true`, discarding the argument entirely.
With `function f(): int`, `if (is_int(f()))` compiled to `if (true)`
and f() was never invoked — its side effects silently vanished.
Fold to a bare `true` only for plain variables and scalar literals;
for any other argument emit `((void)(expr), true)` so the operand is
still evaluated, mirroring how genIsNull already handles native scalar
operands.
- 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
- 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
- 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
- Updated BinaryOpTrait to use C++17 braced-list evaluation order for concat operations
- Modified CallArgumentGenerator to evaluate arguments in PHP source order before ABI arrangement
- Added proper handling of variadic arguments and named parameters in evaluation sequence
- Fixed array_key_exists parameter evaluation order to match PHP semantics
- Updated method call receiver evaluation to prevent statement hoisting issues
- Added comprehensive tests for argument evaluation order in various call scenarios
- Ensured captured beforeStmtLines are properly ordered relative to call expressions
- Added parseOrderedOperand() to handle operand evaluation in correct sequence
- Updated BinaryOpTrait to use ordered parsing for concat() operations
- Modified CallArgumentGenerator to materialize nested calls before argument lowering
- Enhanced CompilerBase to maintain evaluation order in string interpolations
- Updated MethodCallTrait to evaluate receiver before method arguments
- Added comprehensive test case for function/method call argument evaluation order
- Add support for precise BigFloat arithmetic beyond IEEE double precision
- Implement strict validation preventing unsupported operations like power operator on Decimal/BigFloat
- Add proper type conversion functions for BigInt, Decimal and BigFloat to int/float/bool
- Enhance documentation with accurate type specifications and usage guidelines
- Add comprehensive tests for boundary conditions and error handling
- Implement proper exception handling for arithmetic errors and invalid values
- Add support for mixed-type comparisons with explicit conversion requirements
- Fix precision issues in floating point representations and conversions
- Update type promotion rules to prevent unsafe implicit conversions between big types
- Remove static constant lookup logic from parseConstFetch method
- Add support for unqualified constants in namespaces with runtime fallback mechanism
- Implement UnqualifiedInNamespace constant lookup strategy for proper PHP behavior
- Update test cases to verify runtime constant resolution works correctly
- Add comprehensive test coverage for namespace constant fallback scenarios
- Remove isValidDefineName validation in favor of direct string checking
- Support define() calls with namespaced and non-identifier constant names
- Add proper error handling for undefined constants in namespaces
- Replaced all occurrences of self::TYPE_* with Type::* in AnonClassGenerator.php
- Updated ArrayExpressionTrait.php to use Type::ARRAY and Type::VAR instead of self::TYPE_*
- Modified AssignOpTrait.php to reference Type constants instead of self::TYPE_*
- Changed BinaryOpTrait.php to use Type::BIGINT, Type::DECIMAL, Type::BIGFLOAT constants
- Updated CallArgumentGenerator.php to use Type::ARRAY and Type::REF constants
- Added Type import statements to all modified files
- Maintained same functionality while improving code consistency with Type namespace