From 86c56ad1bfafa14b533174d302ebf8b5ca24e28b Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Sun, 6 Sep 2026 18:22:12 +0800 Subject: [PATCH] Remove use native_types, add use varint_types --- benchmark/bench.php | 2 - benchmark/micro_bench.php | 2 - examples/array-loop/main.php | 2 - examples/array-loop/std-array.php | 4 +- examples/debug/mem_leak.php | 2 - examples/error/unset-int-prop.php | 2 - examples/error/unset-int-var.php | 2 - examples/high-precision.php | 2 - examples/perf/class-static-prop.php | 4 +- examples/perf/code.php | 2 - examples/perf/empty_class_static_prop.php | 4 +- examples/pi.php | 2 - examples/wasm-hello/src/WasiDemo.php | 2 - phpunit/code/assign-divide-by-zero.php | 1 + phpunit/code/assign-modulo-by-zero.php | 1 + .../big-numeric/bigfloat-pow-operator.php | 2 - .../code/big-numeric/decimal-pow-operator.php | 2 - .../code/big-numeric/mixed-big-comparison.php | 2 - phpunit/code/bigfloat-unsupported-mod.php | 1 - phpunit/code/bigfloat-unsupported-shift.php | 1 - phpunit/code/bigint-post-inc.php | 1 - phpunit/code/bigint-pre-inc.php | 1 - phpunit/code/constant-overflow-native-add.php | 2 - .../constant-overflow-native-div-subtree.php | 2 - phpunit/code/constant-overflow-native-div.php | 2 - phpunit/code/constant-overflow-native-mod.php | 2 - phpunit/code/constant-overflow-native-mul.php | 2 - phpunit/code/constant-overflow-native-neg.php | 2 - .../constant-overflow-native-nested-add.php | 2 - .../constant-overflow-native-nested-div.php | 2 - .../constant-overflow-native-nested-mod.php | 2 - .../constant-overflow-native-nested-zero.php | 2 - .../constant-overflow-native-nonconstant.php | 2 - phpunit/code/constant-overflow-native-ok.php | 2 - phpunit/code/constant-overflow-native-sub.php | 2 - phpunit/code/constant-overflow-warning.php | 1 + .../control-flow/loop-switch-continue.php | 2 - .../standalone-switch-continue.php | 2 - phpunit/code/decimal-pre-dec.php | 1 - phpunit/code/decimal-unsupported-bitand.php | 1 - phpunit/code/decimal-unsupported-bitor.php | 1 - phpunit/code/divide-by-zero-any.php | 7 ++ phpunit/code/divide-by-zero-int.php | 1 + phpunit/code/divide-by-zero-string.php | 1 + .../code/dynamic-scalar-binary-operands.php | 2 + phpunit/code/hot-path-codegen.php | 2 - ...iteral-declaration-initializer-varint.php} | 4 +- .../local-literal-declaration-initializer.php | 2 + phpunit/code/modulo-by-zero-int.php | 1 + phpunit/code/native-bool-array-dim-write.php | 2 - phpunit/code/native-float-array-dim-write.php | 2 - phpunit/code/native-int-array-dim-write.php | 2 - ...native-method-unset-keeps-optimization.php | 2 - .../native-property-assign-op-class-const.php | 2 - .../code/native-property-assign-op-int.php | 2 - phpunit/code/native-property-full-name.php | 3 - .../native-property-private-other-class.php | 2 - ...ive-property-protected-unrelated-class.php | 2 - .../native-property-static-type-mismatch.php | 2 - .../native-property-this-write-conversion.php | 2 - .../native-property-unset-disables-hoist.php | 2 - .../code/native-property-write-conversion.php | 2 - .../code/native-scalar-binary-operands.php | 2 - .../code/nullsafe-nested-private-property.php | 2 - phpunit/code/nullsafe-private-property.php | 2 - .../code/object-prop-subclass-mismatch.php | 2 - phpunit/code/php-int-max-fold-global.php | 1 + phpunit/code/php-int-max-fold-namespace.php | 2 + phpunit/code/re-assign-array-to-bool.php | 1 - phpunit/code/re-assign-array-to-float.php | 1 - phpunit/code/re-assign-array-to-int.php | 1 - phpunit/code/re-assign-array-to-obj.php | 1 - phpunit/code/re-assign-bool-to-array.php | 1 - phpunit/code/re-assign-bool-to-obj.php | 1 - phpunit/code/re-assign-float-to-array.php | 1 - phpunit/code/re-assign-float-to-obj.php | 1 - phpunit/code/re-assign-int-to-array.php | 1 - phpunit/code/re-assign-int-to-obj.php | 1 - phpunit/code/re-assign-obj-to-array.php | 2 - phpunit/code/re-assign-obj-to-bool.php | 1 - phpunit/code/re-assign-obj-to-float.php | 1 - phpunit/code/re-assign-obj-to-int.php | 1 - phpunit/code/re-assign-obj-to-str.php | 1 - phpunit/code/re-assign-str-to-array.php | 1 - phpunit/code/re-assign-str-to-bool.php | 1 - phpunit/code/re-assign-str-to-float.php | 1 - phpunit/code/re-assign-str-to-int.php | 1 - .../code/readonly-property-no-native-ref.php | 2 - phpunit/code/removed-native-types.php | 7 ++ .../code/shift-boundary-native-neg-left.php | 2 - .../code/shift-boundary-native-neg-right.php | 2 - .../code/shift-boundary-native-negative.php | 2 - ...shift-boundary-native-nested-neg-right.php | 2 - .../shift-boundary-native-nested-overflow.php | 2 - phpunit/code/shift-boundary-native-ok.php | 2 - .../code/shift-boundary-native-overflow.php | 2 - .../code/shift-boundary-native-sign-bit.php | 2 - ...shift-boundary-native-wrapped-overflow.php | 2 - phpunit/code/shift-boundary-warning.php | 1 + .../code/typed-scalar-arithmetic-codegen.php | 1 + .../code/universal-method-int-undefined.php | 1 - .../code/universal-method-int-wrong-args.php | 1 - .../code/universal-method-literal-call.php | 1 - .../code/varint-types-case-insensitive.php | 11 +++ phpunit/src/ClassExistsTraitFoldTest.php | 4 +- phpunit/src/ConversionArityTest.php | 10 +-- phpunit/src/CtypeOptimizerTest.php | 5 +- .../src/EvalOrderSideEffectsCodegenTest.php | 12 +-- phpunit/src/FuncCallOptimizerTest.php | 4 +- phpunit/src/FuncCallOptimizerUnpackTest.php | 1 - phpunit/src/GeneratedCodeIndentationTest.php | 2 +- phpunit/src/LocalVariableInitializerTest.php | 35 ++++---- phpunit/src/LoopControlTest.php | 4 +- phpunit/src/OperatorTest.php | 23 ++++-- phpunit/src/VarIntTypesTest.php | 71 ++++++++++++++++ src/CompilerBase.php | 16 ++-- src/Optimizer/FuncCallOptimizer.php | 6 +- src/Optimizer/LoopVarOptimizer.php | 4 +- src/Optimizer/SsaTypeOptimizer.php | 44 ++++++++-- src/Parser/AssignOpTrait.php | 12 +-- src/Parser/BinaryOpTrait.php | 80 +++++++++++-------- src/Parser/PropertyAccessTrait.php | 4 +- src/Parser/TypeConversionTrait.php | 12 ++- src/Parser/UnaryExpressionTrait.php | 2 +- src/Resolver/DeclarationSymbolTrait.php | 7 +- src/Translator.php | 6 +- src/polyfills.php | 4 +- tests/compiler/array_method/misc.phpt | 2 - tests/compiler/basic/global-int.phpt | 2 - .../basic/pow-int-overflow-native-type.phpt | 1 - .../basic/pow-int-overflow-warning-php85.phpt | 1 - tests/compiler/basic/std_builtin.phpt | 2 - tests/compiler/bigint/arithmetic.phpt | 2 - tests/compiler/bigint/assign_op.phpt | 2 - tests/compiler/bigint/bitwise.phpt | 2 - tests/compiler/bigint/bitwise_shift.phpt | 2 - tests/compiler/bigint/compare.phpt | 2 - tests/compiler/bigint/divmod.phpt | 2 - tests/compiler/bigint/gcd.phpt | 2 - tests/compiler/bigint/literal.phpt | 2 - tests/compiler/bigint/math_func_opt.phpt | 2 - tests/compiler/bigint/mixed.phpt | 2 - tests/compiler/bigint/operator.phpt | 2 - tests/compiler/bigint/pow-right-bigint.phpt | 2 - tests/compiler/bigint/pow.phpt | 2 - tests/compiler/bigint/powmod.phpt | 2 - tests/compiler/bigint/sqrt.phpt | 2 - tests/compiler/bigint/toInt_toFloat.phpt | 2 - tests/compiler/bigint/toString_cast.phpt | 2 - .../bignumber/big_number_operators.phpt | 2 - .../bignumber/bigfloat-high-precision.phpt | 2 - .../bignumber/bigfloat_operators.phpt | 2 - .../bignumber/conversions-and-boundaries.phpt | 2 - .../logical-and-unary-operators.phpt | 2 - .../bignumber/operator-error-boundaries.phpt | 2 - .../reverse-and-compound-operators.phpt | 2 - ...rst-class-callable-property-optimizer.phpt | 2 - tests/compiler/decimal/arithmetic.phpt | 2 - tests/compiler/decimal/assign_op.phpt | 2 - tests/compiler/decimal/ceil.phpt | 2 - tests/compiler/decimal/compare.phpt | 2 - tests/compiler/decimal/divmod.phpt | 2 - tests/compiler/decimal/floor.phpt | 2 - tests/compiler/decimal/literal.phpt | 4 +- tests/compiler/decimal/operator.phpt | 2 - tests/compiler/decimal/pow.phpt | 2 - tests/compiler/decimal/powmod.phpt | 2 - .../precision-and-exception-boundaries.phpt | 2 - tests/compiler/decimal/round.phpt | 2 - tests/compiler/decimal/sqrt.phpt | 2 - tests/compiler/decimal/toFloat.phpt | 2 - tests/compiler/decimal/unary_minus.phpt | 2 - .../call-magic-fallback-typed-object.phpt | 2 - .../call-multilevel-override.phpt | 2 - .../call-parent-sibling-dispatch.phpt | 2 - .../dynamic_call/call-protected-override.phpt | 2 - tests/compiler/keyword_extension/001.phpt | 2 - tests/compiler/keyword_extension/camel.phpt | 2 - tests/compiler/object_property/001.phpt | 4 +- tests/compiler/object_property/003.phpt | 4 +- .../int-property-compound-assignment.phpt | 1 + .../native-scalar-property-assign-op-var.phpt | 2 - .../native-scalar-property-assign-var.phpt | 2 - ...scalar-property-namespaced-diagnostic.phpt | 2 - .../native-typed-read-in-loop.phpt | 2 - ...ypes-int-property-compound-assignment.phpt | 2 - .../readonly-clone-initialization.phpt | 2 - .../readonly-cross-class-direct.phpt | 2 - .../operator/assign-op-mixed-types.phpt | 6 +- .../constant-int-arithmetic-overflow.phpt | 1 + .../operator/local-var-compound-overflow.phpt | 1 + .../operator/runtime-int-division.phpt | 1 + .../operator/runtime-int-overflow-return.phpt | 1 + .../operator/typed-int-float-division.phpt | 1 + .../optimizations/int-narrow-overflow.phpt | 1 + .../loop-var-overflow-semantics.phpt | 3 +- .../optimizations/narrow-div-pow.phpt | 1 + .../optimizations/object-int-prop-02.phpt | 4 +- .../objprop-hoist-array-var-ref.phpt | 2 - .../optimizations/objprop-hoist-float.phpt | 2 - .../optimizations/objprop-hoist-int.phpt | 2 - .../objprop-hoist-loop-redef.phpt | 2 - .../optimizations/objprop-hoist-magic.phpt | 2 - .../objprop-hoist-multi-def.phpt | 2 - .../objprop-hoist-nested-std-ref.phpt | 2 - .../objprop-hoist-object-alias-escape.phpt | 2 - .../objprop-hoist-object-arg-escape.phpt | 2 - ...jprop-hoist-object-arg-reference-slot.phpt | 2 - .../objprop-hoist-ref-capture.phpt | 2 - .../objprop-hoist-this-object-arg-escape.phpt | 2 - .../objprop-nullable-union-default-null.phpt | 2 - .../objprop-typed-object-null-unset.phpt | 2 - .../objprop-unset-fixed-defaults.phpt | 2 - .../objprop-unset-this-typed.phpt | 2 - ...c-prop-assign-ref-late-static-binding.phpt | 2 - .../static/static-prop-assign-ref-native.phpt | 2 - .../static-prop-assign-ref-type-error.phpt | 2 - .../static-prop-dynamic-call-stable.phpt | 2 - .../static-prop-late-static-binding.phpt | 2 - .../static/static-prop-native-defaults.phpt | 2 - .../static/static-prop-ref-slot-crash.phpt | 2 - .../static-prop-ref-slot-float-crash.phpt | 2 - tests/compiler/std-vector/013.phpt | 1 - .../type_decl/is-null-native-scalar.phpt | 2 - .../universal_method/object_extension.phpt | 2 - .../object_extension_exact_name.phpt | 2 - .../universal_method_call.phpt | 2 - .../universal_method_extension.phpt | 2 - .../universal_method_extension_camel.phpt | 2 - .../universal_method_extension_chain.phpt | 2 - .../universal_method_internal.phpt | 2 - tests/compiler/var_convert/001.phpt | 2 - .../wasm/runtime/phpx-numeric-containers.phpt | 2 - 233 files changed, 321 insertions(+), 460 deletions(-) create mode 100644 phpunit/code/divide-by-zero-any.php rename phpunit/code/{local-literal-declaration-initializer-native.php => local-literal-declaration-initializer-varint.php} (74%) create mode 100644 phpunit/code/removed-native-types.php create mode 100644 phpunit/code/varint-types-case-insensitive.php create mode 100644 phpunit/src/VarIntTypesTest.php diff --git a/benchmark/bench.php b/benchmark/bench.php index b8ed3f9f..23407e34 100644 --- a/benchmark/bench.php +++ b/benchmark/bench.php @@ -1,6 +1,4 @@ add(99); diff --git a/phpunit/code/varint-types-case-insensitive.php b/phpunit/code/varint-types-case-insensitive.php new file mode 100644 index 00000000..b2d786ee --- /dev/null +++ b/phpunit/code/varint-types-case-insensitive.php @@ -0,0 +1,11 @@ +compileToCpp('class-exists-class-and-enum.php'); self::assertStringNotContainsString('php::fn::class_exists(', $cpp); - self::assertStringNotContainsString('= false;', $cpp); + self::assertStringNotContainsString('= php::toBool(false);', $cpp); } public function testExplicitAutoloadArgumentUsesNormalCallPath(): void diff --git a/phpunit/src/ConversionArityTest.php b/phpunit/src/ConversionArityTest.php index 885e0e62..a399ddb9 100644 --- a/phpunit/src/ConversionArityTest.php +++ b/phpunit/src/ConversionArityTest.php @@ -23,7 +23,7 @@ class ConversionArityTest extends TestCase // The Native cast carries no base, so both calls must keep the second // argument by going through the dynamic path. - self::assertStringNotContainsString('php::toInt(', $cpp); + self::assertSame(2, substr_count($cpp, 'php::call(get_persistent_func(PersistentFuncId{1}')); self::assertSame(2, substr_count($cpp, '16L')); } @@ -33,10 +33,10 @@ class ConversionArityTest extends TestCase // An unpacked argument is one Node\Arg whatever its runtime arity is, // so the array itself must never be handed to a Native cast. - self::assertStringNotContainsString('php::toInt(', $cpp); - self::assertStringNotContainsString('php::toString(', $cpp); - self::assertStringNotContainsString('php::toFloat(', $cpp); - self::assertStringNotContainsString('php::toBool(', $cpp); + self::assertStringNotContainsString('php::fn::intval(', $cpp); + self::assertStringNotContainsString('php::fn::strval(', $cpp); + self::assertStringNotContainsString('php::fn::floatval(', $cpp); + self::assertStringNotContainsString('php::fn::boolval(', $cpp); // Five full unpacks plus the partial intval('ff', ...[16]). self::assertSame(6, substr_count($cpp, 'appendUnpacked(')); diff --git a/phpunit/src/CtypeOptimizerTest.php b/phpunit/src/CtypeOptimizerTest.php index b680b273..db8b07ed 100644 --- a/phpunit/src/CtypeOptimizerTest.php +++ b/phpunit/src/CtypeOptimizerTest.php @@ -34,6 +34,9 @@ final class CtypeOptimizerTest extends BaseTest } self::assertStringNotContainsString('get_persistent_func', $code); self::assertStringNotContainsString('php::call(', $code); - self::assertStringContainsString('result = php::fn::ctype_alnum(_php__var__char);', $code); + self::assertStringContainsString( + 'result = php::toBool(php::fn::ctype_alnum(_php__var__char));', + $code, + ); } } diff --git a/phpunit/src/EvalOrderSideEffectsCodegenTest.php b/phpunit/src/EvalOrderSideEffectsCodegenTest.php index 69dac7ea..4eef98e8 100644 --- a/phpunit/src/EvalOrderSideEffectsCodegenTest.php +++ b/phpunit/src/EvalOrderSideEffectsCodegenTest.php @@ -19,7 +19,7 @@ final class EvalOrderSideEffectsCodegenTest extends \BaseTest $body = $this->extractFunctionBody($code, 'php_callargorder()'); self::assertMatchesRegularExpression( - '/(tmp_var_\d+) = j;\s*\n\s*(tmp_var_\d+) = j = 5L{1,2};/', + '/(tmp_var_\d+) = php::toInt\(j\);\s*\n\s*(tmp_var_\d+) = php::toInt\(j = php::toInt\(5L{1,2}\)\);/', $body, 'the old value of $j must be captured before $j = 5 executes', ); @@ -36,7 +36,7 @@ final class EvalOrderSideEffectsCodegenTest extends \BaseTest $body = $this->extractFunctionBody($code, 'php_concatorder()'); self::assertMatchesRegularExpression( - '/(tmp_var_\d+) = m;\s*\n\s*(tmp_var_\d+) = m = 9L{1,2};/', + '/(tmp_var_\d+) = php::toInt\(m\);\s*\n\s*(tmp_var_\d+) = php::toInt\(m = php::toInt\(9L{1,2}\)\);/', $body, 'the old value of $m must be captured before $m = 9 executes', ); @@ -53,7 +53,7 @@ final class EvalOrderSideEffectsCodegenTest extends \BaseTest $body = $this->extractFunctionBody($code, 'php_castwrappedcallargorder()'); self::assertMatchesRegularExpression( - '/(tmp_var_\d+) = i;\s*\n\s*(tmp_var_\d+) = php::toInt\(i = 5L{1,2}\);/', + '/(tmp_var_\d+) = php::toInt\(i\);\s*\n\s*(tmp_var_\d+) = php::toInt\(i = php::toInt\(5L{1,2}\)\);/', $body, 'the old value of $i must be captured before the cast-wrapped $i = 5 executes', ); @@ -70,7 +70,7 @@ final class EvalOrderSideEffectsCodegenTest extends \BaseTest $body = $this->extractFunctionBody($code, 'php_notwrappedcallargorder()'); self::assertMatchesRegularExpression( - '/(tmp_var_\d+) = k;\s*\n\s*(tmp_var_\d+) = !\(php::toBool\(k = 0L{1,2}\)\);/', + '/(tmp_var_\d+) = php::toInt\(k\);\s*\n\s*(tmp_var_\d+) = php::toBool\(!\(php::toBool\(k = php::toInt\(0L{1,2}\)\)\)\);/', $body, 'the old value of $k must be captured before the negated $k = 0 executes', ); @@ -89,10 +89,10 @@ final class EvalOrderSideEffectsCodegenTest extends \BaseTest // Zend reads the CV when the ADD executes, i.e. after the nested // assignment; the direct read of k matches that and must stay. self::assertMatchesRegularExpression( - '/(tmp_var_\d+) = k = 5L{1,2};\s*\n[^\n]*\(\(k\) \+ \(\1\)\)/', + '/(tmp_var_\d+) = php::toInt\(k = php::toInt\(5L{1,2}\)\);\s*\n[^\n]*\(\(k\) \+ \(\1\)\)/', $body, ); - self::assertStringNotContainsString('= k;', $body); + self::assertStringNotContainsString('= php::toInt(k);', $body); } private function extractFunctionBody(string $code, string $marker): string diff --git a/phpunit/src/FuncCallOptimizerTest.php b/phpunit/src/FuncCallOptimizerTest.php index 10de1f87..6002f225 100644 --- a/phpunit/src/FuncCallOptimizerTest.php +++ b/phpunit/src/FuncCallOptimizerTest.php @@ -27,8 +27,8 @@ final class FuncCallOptimizerTest extends BaseTest self::assertStringContainsString('php_optimizertypedint()', $code); self::assertStringContainsString('php_optimizertypedfloat()', $code); self::assertStringContainsString('php_optimizerdynamicbool()', $code); - self::assertMatchesRegularExpression('/php_optimizertypedbool\(\);\s*php::fn::in_array/', $code); - self::assertMatchesRegularExpression('/php_optimizertypedint\(\);\s*php::call/', $code); + self::assertMatchesRegularExpression('/php::toBool\(php_optimizertypedbool\(\)\);\s*php::fn::in_array/', $code); + self::assertMatchesRegularExpression('/php::toInt\(php_optimizertypedint\(\)\);\s*php::call/', $code); self::assertMatchesRegularExpression('/php_optimizerdynamicbool\(\);\s*php::call/', $code); self::assertStringContainsString('php::fn::hypot(php::toFloat(', $code); self::assertStringContainsString('php::VarList{php::null}', $code); diff --git a/phpunit/src/FuncCallOptimizerUnpackTest.php b/phpunit/src/FuncCallOptimizerUnpackTest.php index f366b07f..e0ef08a2 100644 --- a/phpunit/src/FuncCallOptimizerUnpackTest.php +++ b/phpunit/src/FuncCallOptimizerUnpackTest.php @@ -29,7 +29,6 @@ class FuncCallOptimizerUnpackTest extends TestCase $cpp = file_get_contents($compiler->convertFile($source)); self::assertSame(5, substr_count($cpp, '.appendUnpacked(')); - self::assertStringNotContainsString('php::toInt(', $cpp); self::assertStringNotContainsString('php::fn::round(', $cpp); self::assertStringNotContainsString('php::fn::array_keys(', $cpp); } diff --git a/phpunit/src/GeneratedCodeIndentationTest.php b/phpunit/src/GeneratedCodeIndentationTest.php index d85535c9..0723a7ed 100644 --- a/phpunit/src/GeneratedCodeIndentationTest.php +++ b/phpunit/src/GeneratedCodeIndentationTest.php @@ -41,7 +41,7 @@ class GeneratedCodeIndentationTest extends \PHPUnit\Framework\TestCase $code, ); $this->assertStringContainsString( - "\tphp::Var value = 1L;\n\n\treturn php::null;\n}", + "\tphp::Int value = php::toInt(1L);\n\n\treturn php::null;\n}", $code, ); $this->assertStringNotContainsString('return php::null;}', $code); diff --git a/phpunit/src/LocalVariableInitializerTest.php b/phpunit/src/LocalVariableInitializerTest.php index ec647869..23a2cead 100644 --- a/phpunit/src/LocalVariableInitializerTest.php +++ b/phpunit/src/LocalVariableInitializerTest.php @@ -17,40 +17,43 @@ final class LocalVariableInitializerTest extends \BaseTest $code = file_get_contents($generated); self::assertIsString($code); - self::assertStringContainsString('php::Var integer = 42L;', $code); - self::assertStringContainsString('php::Var negative = -7L;', $code); - self::assertStringContainsString('php::Var floating = 1.25;', $code); - self::assertStringContainsString('php::Var boolean = true;', $code); + self::assertStringContainsString('php::Int integer = php::toInt(42L);', $code); + self::assertStringContainsString('php::Int negative = php::toInt(-7L);', $code); + self::assertStringContainsString('php::Float floating = php::toFloat(1.25);', $code); + self::assertStringContainsString('php::Bool boolean = php::toBool(true);', $code); self::assertMatchesRegularExpression('/php::Str string = get_str\(\d+\);/', $code); self::assertStringContainsString('php::Var nullValue = php::null;', $code); + self::assertStringContainsString('php::Var explicitAny;', $code); + self::assertStringContainsString('explicitAny = 100L;', $code); - self::assertStringContainsString('php::Var nested;', $code); - self::assertStringContainsString('nested = 9L;', $code); - self::assertStringContainsString('php::Var computed;', $code); - self::assertStringContainsString('computed = ((40L) + (2L));', $code); + self::assertStringContainsString('php::Int nested = 0;', $code); + self::assertStringContainsString('nested = php::toInt(9L);', $code); + self::assertStringContainsString('php::Int computed = 0;', $code); + self::assertStringContainsString('computed = php::toInt(((40L) + (2L)));', $code); $afterDeclaration = substr( $code, - strpos($code, 'php::Var integer = 42L;') + strlen('php::Var integer = 42L;'), + strpos($code, 'php::Int integer = php::toInt(42L);') + + strlen('php::Int integer = php::toInt(42L);'), ); - self::assertStringNotContainsString('integer = 42L;', $afterDeclaration); + self::assertStringNotContainsString('integer = php::toInt(42L);', $afterDeclaration); } - public function testNativeScalarLiteralsInitializeNativeDeclarations(): void + public function testVarIntModeBoxesOnlyIntegerLiterals(): void { global $translator; $compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); $translator = $compiler; - $source = TYPEPHP_ROOT_PATH . '/phpunit/code/local-literal-declaration-initializer-native.php'; + $source = TYPEPHP_ROOT_PATH . '/phpunit/code/local-literal-declaration-initializer-varint.php'; $compiler->addFiles([$source]); $compiler->prepareFile($source); $generated = $compiler->convertFile($source); $code = file_get_contents($generated); self::assertIsString($code); - self::assertStringContainsString('php::Int integer = php::toInt(42L);', $code); - self::assertStringContainsString('php::Int negative = php::toInt(-7L);', $code); + self::assertStringContainsString('php::Var integer = 42L;', $code); + self::assertStringContainsString('php::Var negative = -7L;', $code); self::assertStringContainsString('php::Float floating = php::toFloat(1.25);', $code); self::assertStringContainsString('php::Bool boolean = php::toBool(true);', $code); self::assertMatchesRegularExpression('/php::Str string = get_str\(\d+\);/', $code); @@ -79,10 +82,10 @@ final class LocalVariableInitializerTest extends \BaseTest $code, ); self::assertStringContainsString( - 'php::Var namespaced = _const_var_LocalConstantInitializer__Consumer__ENABLED;', + 'php::Bool namespaced = php::toBool(_const_var_LocalConstantInitializer__Consumer__ENABLED);', $code, ); - self::assertStringContainsString('php::Var internal = ZEND_LONG_MAX;', $code); + self::assertStringContainsString('php::Int internal = php::toInt(ZEND_LONG_MAX);', $code); self::assertStringContainsString('php::Var runtime;', $code); self::assertStringContainsString('runtime = php::constant(', $code); diff --git a/phpunit/src/LoopControlTest.php b/phpunit/src/LoopControlTest.php index 0621967f..ba7da8d2 100644 --- a/phpunit/src/LoopControlTest.php +++ b/phpunit/src/LoopControlTest.php @@ -54,8 +54,8 @@ class LoopControlTest extends \BaseTest $this->assertMatchesRegularExpression('/\.attr\([^)]+\)[^;]*--/', $cpp); // static-property postfix must NOT be rewritten - $this->assertMatchesRegularExpression('/_typephp_static_property_\d+\(\)\+\+/', $cpp); - $this->assertMatchesRegularExpression('/_typephp_static_property_\d+\(\)--/', $cpp); + $this->assertMatchesRegularExpression('/_typephp_static_property_\d+\(\)\.direct_ptr\(\)\)\+\+/', $cpp); + $this->assertMatchesRegularExpression('/_typephp_static_property_\d+\(\)\.direct_ptr\(\)\)--/', $cpp); // array-element postfix must NOT be rewritten $this->assertMatchesRegularExpression('/\.item\([^)]+\)[^;]*\+\+/', $cpp); diff --git a/phpunit/src/OperatorTest.php b/phpunit/src/OperatorTest.php index 82f59dfe..c37b9c89 100644 --- a/phpunit/src/OperatorTest.php +++ b/phpunit/src/OperatorTest.php @@ -57,10 +57,9 @@ class OperatorTest extends \BaseTest } /** - * A literal zero divisor is valid PHP: it raises a catchable - * DivisionByZeroError only when the statement executes, so it must - * compile (with a warning) and defer to the runtime error, exactly like - * the already-accepted `1 % (1 - 1)` and `10 / ZERO` spellings. + * varint_types preserves PHP's catchable integer division errors. Native + * float storage remains the default and rejects a statically known zero + * divisor instead of emitting C++ infinity/NaN behavior. */ public function testLiteralIntDivideByZeroCompilesToRuntimeError(): void { @@ -68,10 +67,20 @@ class OperatorTest extends \BaseTest $this->assertMatchesRegularExpression('/\(\(php::Var\(10L{1,2}\)\) \/ \(php::Var\(0L{1,2}\)\)\)/', $cpp); } - public function testLiteralFloatDivideByZeroCompilesToRuntimeError(): void + public function testNativeFloatLiteralDivideByZeroIsRejected(): void { - $cpp = $this->compileToCpp('divide-by-zero-float.php'); - $this->assertStringContainsString('((php::Var(1.0)) / (php::Var(0.0)))', $cpp); + $this->expectException(\TypePhp\Exception\TestError::class); + $this->expectExceptionMessage('Cannot divide or modulo by zero'); + $this->compileToCpp('divide-by-zero-float.php'); + } + + public function testExplicitAnyIntegerKeepsRuntimeDivisionErrorInDefaultMode(): void + { + $cpp = $this->compileToCpp('divide-by-zero-any.php'); + $this->assertMatchesRegularExpression( + '/\(\(php::Var\(value\)\) \/ \(php::Var\(0L{1,2}\)\)\)/', + $cpp, + ); } public function testLiteralStringDivideByZeroCompilesToRuntimeError(): void diff --git a/phpunit/src/VarIntTypesTest.php b/phpunit/src/VarIntTypesTest.php new file mode 100644 index 00000000..f8334457 --- /dev/null +++ b/phpunit/src/VarIntTypesTest.php @@ -0,0 +1,71 @@ +expectException(TestError::class); + $this->expectExceptionMessage( + '`use native_types` has been removed; native scalar types are now the default', + ); + + $compiler = $this->createCompiler(); + $source = TYPEPHP_ROOT_PATH . '/phpunit/code/removed-native-types.php'; + $compiler->addFiles([$source]); + $compiler->prepareFile($source); + } + + public function testVarIntTypesDirectiveIsCaseInsensitiveAndOnlyBoxesIntegers(): void + { + $code = $this->compileSource( + $this->createCompiler(), + TYPEPHP_ROOT_PATH . '/phpunit/code/varint-types-case-insensitive.php', + ); + + self::assertStringContainsString('php::Var integer = 42L;', $code); + self::assertStringContainsString('php::Float floating = php::toFloat(1.5);', $code); + self::assertStringContainsString('php::Bool boolean = php::toBool(true);', $code); + } + + public function testVarIntTypesModeDoesNotLeakIntoTheNextFile(): void + { + $compiler = $this->createCompiler(); + $varIntSource = TYPEPHP_ROOT_PATH . '/phpunit/code/local-literal-declaration-initializer-varint.php'; + $defaultSource = TYPEPHP_ROOT_PATH . '/phpunit/code/local-literal-declaration-initializer.php'; + $compiler->addFiles([$varIntSource, $defaultSource]); + + $compiler->prepareFile($varIntSource); + $compiler->prepareFile($defaultSource); + $this->compilePrepared($compiler, $varIntSource); + $defaultCode = $this->compilePrepared($compiler, $defaultSource); + + self::assertStringContainsString('php::Int integer = php::toInt(42L);', $defaultCode); + self::assertStringNotContainsString('php::Var integer = 42L;', $defaultCode); + } + + private function createCompiler(): CompilerTest + { + global $translator; + $compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); + $translator = $compiler; + return $compiler; + } + + private function compileSource(CompilerTest $compiler, string $source): string + { + $compiler->addFiles([$source]); + $compiler->prepareFile($source); + return $this->compilePrepared($compiler, $source); + } + + private function compilePrepared(CompilerTest $compiler, string $source): string + { + $generated = $compiler->convertFile($source); + $code = file_get_contents($generated); + self::assertIsString($code); + return $code; + } +} diff --git a/src/CompilerBase.php b/src/CompilerBase.php index ad493ce4..13830967 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -488,7 +488,8 @@ class CompilerBase implements PropertyAccessContext protected array $nativeClassDeclarations = []; /** @var array Request-reset initialization flags for Native static locals. */ protected array $nativeStaticInitializers = []; - protected bool $nativeTypes = false; + /** Box inferred integer locals in php::Var to retain Zend integer widening semantics. */ + protected bool $varIntTypes = false; protected bool $decimalTypes = false; protected bool $bigintTypes = false; protected string $rootPath; @@ -1050,7 +1051,7 @@ class CompilerBase implements PropertyAccessContext protected function resetFile(): void { $this->indentLevel = 0; - $this->nativeTypes = false; + $this->varIntTypes = false; $this->decimalTypes = false; $this->bigintTypes = false; $this->classesDefineInFile = []; @@ -2489,13 +2490,12 @@ class CompilerBase implements PropertyAccessContext } // The return value of the actual function. $type = $this->detectTypeOfExpr($v->expr); - // In ordinary PHP mode, int +/−/* int is only conditionally an int: + // In varint mode, int +/−/* int is only conditionally an int: // runtime overflow promotes the result to float. Keep the Variant // representation through the return boundary so a declared scalar // return type observes and rejects that float exactly as PHP does. - // `use native_types` intentionally opts into native C++ arithmetic - // semantics and is therefore excluded from this check. - if (!$this->nativeTypes && $type === Type::INT && $this->exprCanOverflowInt($v->expr)) { + // Native C++ arithmetic is the default and is therefore excluded. + if ($this->varIntTypes && $type === Type::INT && $this->exprCanOverflowInt($v->expr)) { $type = Type::VAR; } $nativeExpressionClass = $this->detectClassOfExpr($v->expr); @@ -3009,7 +3009,7 @@ class CompilerBase implements PropertyAccessContext case 'Expr_UnaryPlus': $innerType = $this->detectTypeOfExpr($expr->expr); if ( - !$this->nativeTypes + $this->varIntTypes && $exprType === 'Expr_UnaryMinus' && $innerType === Type::INT && $this->constantIntValue($expr->expr) === PHP_INT_MIN @@ -3097,7 +3097,7 @@ class CompilerBase implements PropertyAccessContext if ($leftType === Type::FLOAT || $rightType === Type::FLOAT) { return Type::FLOAT; } - if (!$this->nativeTypes && $leftType === Type::INT && $rightType === Type::INT) { + if ($this->varIntTypes && $leftType === Type::INT && $rightType === Type::INT) { $op = match ($exprType) { 'Expr_BinaryOp_Plus' => '+', 'Expr_BinaryOp_Minus' => '-', diff --git a/src/Optimizer/FuncCallOptimizer.php b/src/Optimizer/FuncCallOptimizer.php index 89b4fe8a..25d1fc02 100644 --- a/src/Optimizer/FuncCallOptimizer.php +++ b/src/Optimizer/FuncCallOptimizer.php @@ -478,7 +478,11 @@ trait FuncCallOptimizer // time()). Reflection lookup failure also uses zero as its // unknown sentinel, so only enforce that implicit value when it // is greater than zero. - if ((array_key_exists('maxArgs', $config) || $maxArgs > 0) && $argCount > $maxArgs) { + $variadic = !empty($config['variadic']) || ($refInfo['variadic'] ?? false); + if (!$variadic + && (array_key_exists('maxArgs', $config) || $maxArgs > 0) + && $argCount > $maxArgs + ) { $this->fatalError($expr, "{$name}() expects at most {$maxArgs} argument(s), {$argCount} given"); } } diff --git a/src/Optimizer/LoopVarOptimizer.php b/src/Optimizer/LoopVarOptimizer.php index 7cd0e278..e56b4bbd 100644 --- a/src/Optimizer/LoopVarOptimizer.php +++ b/src/Optimizer/LoopVarOptimizer.php @@ -2,8 +2,8 @@ /** * Range-proven loop variable optimizer. * - * This pass narrows common PHP loop counters to php::Int without requiring - * `use native_types`. It is intentionally pattern-based: PHP arithmetic can + * This pass narrows common PHP loop counters to php::Int even under + * `use varint_types`. It is intentionally pattern-based: PHP arithmetic can * widen integers to floats on overflow, so only monotonic counters with a * statically bounded range are accepted. */ diff --git a/src/Optimizer/SsaTypeOptimizer.php b/src/Optimizer/SsaTypeOptimizer.php index f8eb9afb..758af384 100644 --- a/src/Optimizer/SsaTypeOptimizer.php +++ b/src/Optimizer/SsaTypeOptimizer.php @@ -16,6 +16,7 @@ use TypePhp\Analysis\SsaFlags; use TypePhp\Analysis\SsaVar; use PhpParser\Node; use PhpParser\NodeAbstract; +use PhpParser\NodeFinder; trait SsaTypeOptimizer { @@ -94,11 +95,42 @@ trait SsaTypeOptimizer $groups[$name][] = $ssaVar; } - // Type detection may inspect RHS expressions that read variables - // defined by earlier assignments. Code generation normally registers - // those locals while parsing assignments, but SSA optimization runs - // before that parse pass, so seed the optimization context with the - // SSA-defined local names as generic Vars. + // Type detection may inspect RHS expressions before the normal parse + // pass has registered their inputs. This includes variables introduced + // by destructuring foreach targets, which are deliberately not always + // represented as standalone SSA definitions. Seed every statically + // named variable read as Var for analysis only; resetAnalysisTemporaries() + // restores the real symbol table before code generation, where genuine + // undefined-variable diagnostics still run normally. + $nodeFinder = new NodeFinder(); + foreach ($nodeFinder->findInstanceOf($ssa->getStmts(), Node\Expr\Variable::class) as $variable) { + if (!is_string($variable->name)) { + continue; + } + $varName = $this->escapeVarName($variable->name); + if (!isset($this->context->arguments[$varName]) && !$this->hasVar($varName)) { + $this->context->localVars[$varName] = Type::VAR; + } + } + + // A foreach key may be int or string and a foreach value is entirely + // runtime-defined. Even when the same local has an earlier integer + // assignment, neither target can be narrowed safely. Include list + // destructuring targets as well. + $foreachTargets = []; + foreach ($nodeFinder->findInstanceOf($ssa->getStmts(), Node\Stmt\Foreach_::class) as $foreach) { + foreach ([$foreach->keyVar, $foreach->valueVar] as $target) { + if (!$target instanceof Node) { + continue; + } + foreach ($nodeFinder->findInstanceOf($target, Node\Expr\Variable::class) as $variable) { + if (is_string($variable->name)) { + $foreachTargets[$variable->name] = true; + } + } + } + } + foreach (array_keys($groups) as $name) { $varName = $this->escapeVarName($name); if (!isset($this->context->arguments[$varName]) && !$this->hasVar($varName)) { @@ -109,7 +141,7 @@ trait SsaTypeOptimizer foreach ($groups as $groupName => $varList) { $varName = $this->escapeVarName($groupName); // Skip parameters — they already have declared types - if (isset($this->context->arguments[$varName])) { + if (isset($this->context->arguments[$varName]) || isset($foreachTargets[$groupName])) { continue; } diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 8c7510a1..08358029 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -867,7 +867,7 @@ trait AssignOpTrait return $pythonOperator; } $propertyWriteTarget = $this->preparePropertyWriteTarget($node->var); - $this->guardLiteralDivisionByZero($node->expr, $op); + $this->guardLiteralDivisionByZero($node->var, $node->expr, $op); // A compound division/modulo on a NATIVE scalar slot with a proven // zero divisor cannot fall through to the raw C++ operator (SIGFPE @@ -876,16 +876,16 @@ trait AssignOpTrait // lower the whole expression to the PHP-semantics binary operation // and leave the target untouched. if (($op === '/=' || $op === '%=') - && !$this->nativeTypes + && $this->varIntTypes && $this->isZeroLiteral($node->expr) && $this->isVarExpr($node->var) && $this->hasVar((string) $this->parseIdentifier($node->var)) - && in_array($this->detectVarType($node->var), [Type::INT, Type::FLOAT], true) + && $this->detectVarType($node->var) === Type::INT ) { // std::int()/std::float() values are an explicit opt-in to native // C++ arithmetic; changing them to PHP semantics here would be as // wrong as the undefined raw operation. Keep the compile-time - // rejection native_types mode uses. + // rejection used by the default native mode. if ($this->isExplicitNativeArithmeticExpr($node->var)) { $this->fatalError($node->expr, 'Cannot divide or modulo by zero'); } @@ -1176,14 +1176,14 @@ trait AssignOpTrait $rightType = $this->detectTypeOfExpr($node->expr); - // In ordinary PHP mode, an int compound assignment must perform the + // In varint mode, an int compound assignment must perform the // arithmetic before the typed-property write is validated. The result // may therefore be a float (division or integer overflow), in which // case Zend rejects the write and leaves the old property value intact. // A direct zend_long reference would bypass that behavior completely. // Native objects cannot cross the Variant boundary and retain their // native C++ property access path. - if (!$this->nativeTypes + if ($this->varIntTypes && $def->type === Type::INT && !$this->isNativeObjectClass($this->detectClassOfExpr($node->var->var)) && in_array($op, ['+=', '-=', '*=', '/=', '%=', '**=', '<<=', '>>=', '&=', '|=', '^='], true) diff --git a/src/Parser/BinaryOpTrait.php b/src/Parser/BinaryOpTrait.php index a6cb87ad..89a6aca0 100644 --- a/src/Parser/BinaryOpTrait.php +++ b/src/Parser/BinaryOpTrait.php @@ -129,9 +129,10 @@ trait BinaryOpTrait $leftExpr = $this->convertExprType($leftExpr, $leftType, Type::FLOAT); } - $this->guardLiteralDivisionByZero($right, $op); + $this->guardLiteralDivisionByZero($left, $right, $op); $constantDivisionByZero = $this->handleNestedConstantDivisionByZero( + $left, $right, $op, $leftExpr, @@ -145,12 +146,9 @@ trait BinaryOpTrait if (!($leftType === Type::INT and $rightType === Type::INT)) { return 'php::fn::mod(' . $leftExpr . ', ' . $rightExpr . ')'; } - // PHP int modulo raises a catchable DivisionByZeroError for a - // zero divisor and defines PHP_INT_MIN % -1 as 0; the raw C++ '%' - // is undefined behavior for both. Route dynamic int modulo through - // the PHP mod function unless the user explicitly selected - // `use native_types`. Constant operands are folded below. - if (!$this->nativeTypes + // varint_types retains PHP's catchable modulo errors and + // PHP_INT_MIN % -1 behavior. Native integers use raw C++ rules. + if ($this->varIntTypes && !$this->isExplicitNativeArithmeticExpr($left) && !$this->isExplicitNativeArithmeticExpr($right) && $this->evaluateConstantIntArithmetic($left, $right, '%') === null @@ -169,10 +167,9 @@ trait BinaryOpTrait // right-shifted value) and negative shift counts raise a catchable // ArithmeticError, while the raw C++ shift is undefined behavior // for both; a raw left shift into the sign bit is also undefined. - // Route dynamic int shifts through the encapsulated Variant - // operators unless the user explicitly selected `use native_types`. - // Constant shifts that C++ defines identically to PHP stay raw. - if (!$this->nativeTypes + // In varint mode, route dynamic shifts through Variant. Constant + // shifts that C++ defines identically to PHP stay raw. + if ($this->varIntTypes && !$this->isExplicitNativeArithmeticExpr($left) && !$this->isExplicitNativeArithmeticExpr($right) && $leftType === Type::INT @@ -200,10 +197,9 @@ trait BinaryOpTrait // Declared int parameters use the native Int ABI even in ordinary PHP // mode. Direct C++ +/−/* can overflow, while C++ integer division // truncates and cannot raise PHP's DivisionByZeroError. Route dynamic - // integer arithmetic through the encapsulated Variant operators unless - // the user explicitly selected `use native_types`. Fully constant - // expressions remain safe to emit directly after the checks above. - if (!$this->nativeTypes + // integer arithmetic through the encapsulated Variant operators only + // when varint_types explicitly requests PHP widening semantics. + if ($this->varIntTypes && $leftType === Type::INT && $rightType === Type::INT && in_array($op, ['+', '-', '*', '/'], true) @@ -221,11 +217,9 @@ trait BinaryOpTrait // C++ '/': zend_long division truncates (7 / 2 is 3.5 in PHP, 3 in // C++), division by zero must raise the catchable DivisionByZeroError // (raw integer division is UB, raw double division yields INF/NAN), - // and PHP_INT_MIN / -1 promotes to float. Route dynamic division - // through the encapsulated Variant operator unless the user explicitly - // selected `use native_types`. Fully constant operands are folded - // above or are exact when emitted directly. - if (!$this->nativeTypes + // and PHP_INT_MIN / -1 promotes to float. varint_types explicitly + // selects this Variant path; native scalar division is the default. + if ($this->varIntTypes && $op === '/' && !$this->isExplicitNativeArithmeticExpr($left) && !$this->isExplicitNativeArithmeticExpr($right) @@ -241,8 +235,8 @@ trait BinaryOpTrait } /** - * std::int/float/bool explicitly opt a value into native C++ arithmetic, - * independently of the file-wide `use native_types` declaration. + * std::int/float/bool explicitly retain native C++ arithmetic inside a + * file that selected `use varint_types`. */ protected function isExplicitNativeArithmeticExpr(NodeAbstract $expr): bool { @@ -277,7 +271,7 @@ trait BinaryOpTrait $wordSize = PHP_INT_SIZE * 8; - if ($this->nativeTypes) { + if (!$this->varIntTypes) { if ($shiftValue >= $wordSize) { $this->fatalError( $right, @@ -371,7 +365,7 @@ trait BinaryOpTrait return null; } - if ($this->nativeTypes) { + if (!$this->varIntTypes) { if ($evaluation['cppUndefined']) { $this->fatalError( $left, @@ -448,7 +442,7 @@ trait BinaryOpTrait */ protected function constantIntValue(NodeAbstract $expr): ?int { - $value = $this->constantNumericValue($expr, $this->nativeTypes); + $value = $this->constantNumericValue($expr, !$this->varIntTypes); return is_int($value) ? $value : null; } @@ -578,6 +572,7 @@ trait BinaryOpTrait } protected function handleNestedConstantDivisionByZero( + NodeAbstract $left, NodeAbstract $right, string $op, string $leftExpr, @@ -588,13 +583,13 @@ trait BinaryOpTrait } if (!$this->isZeroLiteral($right)) { - $rightValue = $this->constantNumericValue($right, $this->nativeTypes); + $rightValue = $this->constantNumericValue($right, !$this->varIntTypes); if ($rightValue === null || $rightValue != 0) { return null; } } - if ($this->nativeTypes) { + if (!$this->usesPhpArithmeticForZeroDivisor($left, $right)) { $this->fatalError($right, 'Constant division or modulo by zero has undefined behavior in C++ native mode'); } @@ -689,7 +684,7 @@ trait BinaryOpTrait $type = $this->getOrderedOperandTmpType($expr, (string) $value); $tmpVar = $this->addTmpVar($type); } - if ($this->nativeTypes && $this->isNativeType($type)) { + if ($this->usesNativeScalarStorage($type)) { // A native temporary has a fixed C++ scalar ABI. The expression // can still contain a dynamic operand (for example, an array // element), so normalize it at the materialization boundary. @@ -721,7 +716,7 @@ trait BinaryOpTrait && $this->isVarExpr($expr->var) ) { $type = $this->getVarType($this->parseIdentifier($expr->var)); - if ($this->nativeTypes && $this->isNativeType($type)) { + if ($this->usesNativeScalarStorage($type)) { return $type; } return Type::VAR; @@ -736,7 +731,7 @@ trait BinaryOpTrait $type = $this->detectTypeOfExpr($expr); if ( in_array($type, [Type::BIGINT, Type::DECIMAL, Type::BIGFLOAT], true) - || ($this->nativeTypes && $this->isNativeType($type)) + || $this->usesNativeScalarStorage($type) ) { // Calls and nested binary operands are materialized to preserve // PHP's left-to-right evaluation order. In native-types mode @@ -778,7 +773,7 @@ trait BinaryOpTrait $type = $this->detectTypeOfExpr($expr); if ($expr instanceof Expr\Variable || in_array($type, [Type::BIGINT, Type::DECIMAL, Type::BIGFLOAT], true) - || ($this->nativeTypes && $this->isNativeType($type)) + || $this->usesNativeScalarStorage($type) ) { return $type; } @@ -821,7 +816,7 @@ trait BinaryOpTrait */ protected function tryParseFinalIntPropertyAddChain(Expr\BinaryOp\Plus $expr): ?string { - if ($this->nativeTypes) { + if (!$this->varIntTypes) { return null; } @@ -1424,10 +1419,14 @@ trait BinaryOpTrait ?? $this->parseBinaryOp($expr->left, $expr->right, '/'); } - protected function guardLiteralDivisionByZero(NodeAbstract $right, string $op): void + protected function guardLiteralDivisionByZero( + NodeAbstract $left, + NodeAbstract $right, + string $op + ): void { if (($op === '/' or $op === '%' or $op === '/=' or $op === '%=') and $this->isZeroLiteral($right)) { - if ($this->nativeTypes) { + if (!$this->usesPhpArithmeticForZeroDivisor($left, $right)) { $this->fatalError($right, 'Cannot divide or modulo by zero'); } // PHP raises a catchable DivisionByZeroError at runtime, and only @@ -1437,6 +1436,19 @@ trait BinaryOpTrait } } + /** + * A boxed operand already carries Zend arithmetic semantics regardless of + * the file mode. varint_types additionally opts integer arithmetic into + * that path; native scalar expressions reject statically known zero + * divisors before raw C++ arithmetic can produce UB or infinity. + */ + protected function usesPhpArithmeticForZeroDivisor(NodeAbstract $left, NodeAbstract $right): bool + { + return $this->varIntTypes + || $this->detectTypeOfExpr($left) === Type::VAR + || $this->detectTypeOfExpr($right) === Type::VAR; + } + protected function parseBinaryOpMinus(Expr\BinaryOp\Minus $expr): string { return $this->parsePythonBinaryOperator($expr) diff --git a/src/Parser/PropertyAccessTrait.php b/src/Parser/PropertyAccessTrait.php index c3e32da3..9cca3ba8 100644 --- a/src/Parser/PropertyAccessTrait.php +++ b/src/Parser/PropertyAccessTrait.php @@ -512,7 +512,7 @@ trait PropertyAccessTrait $nativeProp = $resolution->expression; $def = $this->getNativePropertyDef($expr); $class = $resolution->class; - if ($this->nativeTypes && $def && $class !== null) { + if ($def && $class !== null && $this->usesNativeScalarStorage($def->type)) { $this->setNativePropertyValueSource($expr, self::NATIVE_PROPERTY_VALUE_VAR); return $this->emitNativeStaticPropertyTypedFetch($expr, $class, $def, $nativeProp); } @@ -1244,7 +1244,7 @@ trait PropertyAccessTrait $getProperty = $objectVar . '.attr(' . $id . ', ' . $this->escapeAttrMode($update) . ')'; } $def = $this->getNativePropertyDef($expr); - if ($def and $this->nativeTypes) { + if ($def && $this->usesNativeScalarStorage($def->type)) { $propName = $this->parseIdentifier($property); $typedFetch = $this->emitNativeInstancePropertyTypedFetch( $expr, diff --git a/src/Parser/TypeConversionTrait.php b/src/Parser/TypeConversionTrait.php index 5e7da7fc..5db48a9b 100644 --- a/src/Parser/TypeConversionTrait.php +++ b/src/Parser/TypeConversionTrait.php @@ -226,7 +226,17 @@ trait TypeConversionTrait if ($type === Type::FLOAT && $this->decimalTypes) { return Type::DECIMAL; } - return $this->nativeTypes ? $type : Type::VAR; + return $type === Type::INT && $this->varIntTypes ? Type::VAR : $type; + } + + /** + * Whether an inferred scalar has fixed C++ storage in the current file. + * Float and bool are always native; varint_types only boxes integers. + */ + protected function usesNativeScalarStorage(string $type): bool + { + return $this->isNativeType($type) + && !($type === Type::INT && $this->varIntTypes); } protected function convertExprFromType(string $type, string $expr): string diff --git a/src/Parser/UnaryExpressionTrait.php b/src/Parser/UnaryExpressionTrait.php index 9fd45e5a..6fbe1f62 100644 --- a/src/Parser/UnaryExpressionTrait.php +++ b/src/Parser/UnaryExpressionTrait.php @@ -113,7 +113,7 @@ trait UnaryExpressionTrait if ($type === Type::INT) { $value = $this->constantIntValue($expr->expr); if ($value === PHP_INT_MIN) { - if ($this->nativeTypes) { + if (!$this->varIntTypes) { $this->fatalError( $expr, 'Negating PHP_INT_MIN has undefined behavior in C++ native mode' diff --git a/src/Resolver/DeclarationSymbolTrait.php b/src/Resolver/DeclarationSymbolTrait.php index 52e9a36d..c2987cf0 100644 --- a/src/Resolver/DeclarationSymbolTrait.php +++ b/src/Resolver/DeclarationSymbolTrait.php @@ -88,7 +88,12 @@ trait DeclarationSymbolTrait } else { $idLower = strtolower($id); if ($idLower === 'native_types') { - $this->nativeTypes = true; + $this->fatalError( + $use, + '`use native_types` has been removed; native scalar types are now the default', + ); + } elseif ($idLower === 'varint_types') { + $this->varIntTypes = true; } elseif ($idLower === 'decimal_types') { $this->decimalTypes = true; } elseif ($idLower === 'bigint_types') { diff --git a/src/Translator.php b/src/Translator.php index 433faeb8..53df8acc 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -5026,11 +5026,11 @@ CODE; $ssaBuilder->build(); $this->context->ssaBuilder = $ssaBuilder; $this->analyzeStableObjects($ssaBuilder); - // Range-proven loop counters are safe to narrow even without - // `use native_types`: the optimizer rejects counters whose PHP + // Range-proven loop counters are safe to narrow even with + // `use varint_types`: the optimizer rejects counters whose PHP // integer semantics could widen to float or otherwise escape. $optimizedLoopVars = $this->optimizeLoopVars($ssaBuilder); - if ($this->nativeTypes) { + if (!$this->varIntTypes) { // Narrow local variable types based on SSA analysis. $this->optimizeVarTypes($ssaBuilder); // Narrow native property accesses. diff --git a/src/polyfills.php b/src/polyfills.php index 1687f13e..cbd59a7e 100644 --- a/src/polyfills.php +++ b/src/polyfills.php @@ -143,8 +143,8 @@ final class Type public const string Box = 'box'; } -/** @deprecated Compiler directive retained independently of public type symbols. */ -class native_types +/** Compiler directive that opts inferred integer locals into Variant storage. */ +class varint_types { } diff --git a/tests/compiler/array_method/misc.phpt b/tests/compiler/array_method/misc.phpt index 00d4d0ea..8bb65b7c 100644 --- a/tests/compiler/array_method/misc.phpt +++ b/tests/compiler/array_method/misc.phpt @@ -2,8 +2,6 @@ Universal method call on native typed variables --FILE-- --FILE-- >, <<=, >>=, bitShiftLeft, bitShiftRight) --FILE-- toString(); diff --git a/tests/compiler/bigint/math_func_opt.phpt b/tests/compiler/bigint/math_func_opt.phpt index 70526db9..6663433e 100644 --- a/tests/compiler/bigint/math_func_opt.phpt +++ b/tests/compiler/bigint/math_func_opt.phpt @@ -3,8 +3,6 @@ Big* types: math function optimization (abs/pow/sqrt/floor/ceil/round) --FILE-- toString(), "\n"; diff --git a/tests/compiler/bigint/pow.phpt b/tests/compiler/bigint/pow.phpt index d4869c8b..91377943 100644 --- a/tests/compiler/bigint/pow.phpt +++ b/tests/compiler/bigint/pow.phpt @@ -2,8 +2,6 @@ BigInt: pow --FILE-- sqrt()->toString(); echo "\n"; diff --git a/tests/compiler/bigint/toInt_toFloat.phpt b/tests/compiler/bigint/toInt_toFloat.phpt index 6dc2e37d..4827ed68 100644 --- a/tests/compiler/bigint/toInt_toFloat.phpt +++ b/tests/compiler/bigint/toInt_toFloat.phpt @@ -3,8 +3,6 @@ BigInt: toInt / toFloat --FILE-- toInt()); diff --git a/tests/compiler/bigint/toString_cast.phpt b/tests/compiler/bigint/toString_cast.phpt index baf65493..a835c398 100644 --- a/tests/compiler/bigint/toString_cast.phpt +++ b/tests/compiler/bigint/toString_cast.phpt @@ -3,8 +3,6 @@ Big* types: (string) cast, strval() and echo → toString() --FILE-- , <=, >=, ==, != --FILE-- , <=, >=, ==, != --FILE-- toString(), "\n"; diff --git a/tests/compiler/callable/first-class-callable-property-optimizer.phpt b/tests/compiler/callable/first-class-callable-property-optimizer.phpt index 7e009fb0..dbbe3be7 100644 --- a/tests/compiler/callable/first-class-callable-property-optimizer.phpt +++ b/tests/compiler/callable/first-class-callable-property-optimizer.phpt @@ -3,8 +3,6 @@ First-class callable placeholder is not treated as a call argument by property o --FILE-- ceil()->toString()); diff --git a/tests/compiler/decimal/compare.phpt b/tests/compiler/decimal/compare.phpt index 1b348c20..d92bfc20 100644 --- a/tests/compiler/decimal/compare.phpt +++ b/tests/compiler/decimal/compare.phpt @@ -3,8 +3,6 @@ Decimal comparison and conversions --FILE-- floor()->toString()); diff --git a/tests/compiler/decimal/literal.phpt b/tests/compiler/decimal/literal.phpt index 69064614..b0740fe6 100644 --- a/tests/compiler/decimal/literal.phpt +++ b/tests/compiler/decimal/literal.phpt @@ -2,8 +2,6 @@ decimal: literal --FILE-- --EXPECT-- -string(3) "4.3" \ No newline at end of file +string(3) "4.3" diff --git a/tests/compiler/decimal/operator.phpt b/tests/compiler/decimal/operator.phpt index 407d5359..364a190e 100644 --- a/tests/compiler/decimal/operator.phpt +++ b/tests/compiler/decimal/operator.phpt @@ -3,8 +3,6 @@ Decimal operator overloading (+, -, *, /, %) and comparisons --FILE-- pow(std::decimal("3"))->toString()); diff --git a/tests/compiler/decimal/powmod.phpt b/tests/compiler/decimal/powmod.phpt index a6369d0a..8316b9c1 100644 --- a/tests/compiler/decimal/powmod.phpt +++ b/tests/compiler/decimal/powmod.phpt @@ -3,8 +3,6 @@ Decimal: powmod --FILE-- toString(), "\n"; diff --git a/tests/compiler/decimal/round.phpt b/tests/compiler/decimal/round.phpt index ba4ccc61..902b0618 100644 --- a/tests/compiler/decimal/round.phpt +++ b/tests/compiler/decimal/round.phpt @@ -3,8 +3,6 @@ Decimal: round --FILE-- sqrt()->toString()); diff --git a/tests/compiler/decimal/toFloat.phpt b/tests/compiler/decimal/toFloat.phpt index 70e3f548..54462bd4 100644 --- a/tests/compiler/decimal/toFloat.phpt +++ b/tests/compiler/decimal/toFloat.phpt @@ -3,8 +3,6 @@ Decimal: toFloat --FILE-- toFloat()); diff --git a/tests/compiler/decimal/unary_minus.phpt b/tests/compiler/decimal/unary_minus.phpt index 903b5fb3..a5ab7151 100644 --- a/tests/compiler/decimal/unary_minus.phpt +++ b/tests/compiler/decimal/unary_minus.phpt @@ -3,8 +3,6 @@ Decimal unary minus operator --FILE-- --EXPECT-- -int(76193440) \ No newline at end of file +int(76193440) diff --git a/tests/compiler/object_property/003.phpt b/tests/compiler/object_property/003.phpt index 0e605843..4d389a64 100644 --- a/tests/compiler/object_property/003.phpt +++ b/tests/compiler/object_property/003.phpt @@ -2,8 +2,6 @@ default array property --FILE-- --EXPECT-- int(123) -int(112) \ No newline at end of file +int(112) diff --git a/tests/compiler/object_property/int-property-compound-assignment.phpt b/tests/compiler/object_property/int-property-compound-assignment.phpt index 2631fd9b..aba96679 100644 --- a/tests/compiler/object_property/int-property-compound-assignment.phpt +++ b/tests/compiler/object_property/int-property-compound-assignment.phpt @@ -3,6 +3,7 @@ Typed int property compound assignments use PHP arithmetic and checked writes --FILE-- b += 2; @@ -19,4 +17,4 @@ function main(): void { } ?> --EXPECT-- -int(200000) \ No newline at end of file +int(200000) diff --git a/tests/compiler/optimizations/objprop-hoist-array-var-ref.phpt b/tests/compiler/optimizations/objprop-hoist-array-var-ref.phpt index 9388a125..b0569fd8 100644 --- a/tests/compiler/optimizations/objprop-hoist-array-var-ref.phpt +++ b/tests/compiler/optimizations/objprop-hoist-array-var-ref.phpt @@ -2,8 +2,6 @@ SSA object prop: hoist array property through indirect Var handle --FILE-- a = 1.5; - $n = 1024; while($n--) { $o->a += 0.5; diff --git a/tests/compiler/optimizations/objprop-hoist-int.phpt b/tests/compiler/optimizations/objprop-hoist-int.phpt index 7aa77260..eb50f15f 100644 --- a/tests/compiler/optimizations/objprop-hoist-int.phpt +++ b/tests/compiler/optimizations/objprop-hoist-int.phpt @@ -2,14 +2,12 @@ SSA object prop: hoist int property to reference (NativeTypes) --FILE-- a = 12; - $n = 1024; while($n--) { $o->a += 13; diff --git a/tests/compiler/optimizations/objprop-hoist-loop-redef.phpt b/tests/compiler/optimizations/objprop-hoist-loop-redef.phpt index 2acadb58..21963241 100644 --- a/tests/compiler/optimizations/objprop-hoist-loop-redef.phpt +++ b/tests/compiler/optimizations/objprop-hoist-loop-redef.phpt @@ -2,8 +2,6 @@ SSA object prop: loop body object redefinition prevents hoisting --FILE-- $name; } @@ -11,7 +10,6 @@ class Foo { function main(): void { $o = new Foo(); $o->a = 10; - $n = 5; while($n--) { $o->a += 2; diff --git a/tests/compiler/optimizations/objprop-hoist-multi-def.phpt b/tests/compiler/optimizations/objprop-hoist-multi-def.phpt index 86196421..d243dde7 100644 --- a/tests/compiler/optimizations/objprop-hoist-multi-def.phpt +++ b/tests/compiler/optimizations/objprop-hoist-multi-def.phpt @@ -2,7 +2,6 @@ SSA object prop: multiple definitions prevent hoisting --FILE-- a = 100; $o = new Foo(); $o->a = 200; - $n = 10; while($n--) { $o->a += 5; diff --git a/tests/compiler/optimizations/objprop-hoist-nested-std-ref.phpt b/tests/compiler/optimizations/objprop-hoist-nested-std-ref.phpt index ea3ea55b..2fabaaec 100644 --- a/tests/compiler/optimizations/objprop-hoist-nested-std-ref.phpt +++ b/tests/compiler/optimizations/objprop-hoist-nested-std-ref.phpt @@ -2,8 +2,6 @@ SSA object prop: nested std::ref property use prevents hoisting --FILE-- a = 10; - $ref = &$o->a; $ref = 20; diff --git a/tests/compiler/optimizations/objprop-hoist-this-object-arg-escape.phpt b/tests/compiler/optimizations/objprop-hoist-this-object-arg-escape.phpt index a28036fd..8540e552 100644 --- a/tests/compiler/optimizations/objprop-hoist-this-object-arg-escape.phpt +++ b/tests/compiler/optimizations/objprop-hoist-this-object-arg-escape.phpt @@ -2,8 +2,6 @@ SSA object prop: this object argument escape prevents property hoisting --FILE-- toString()); var_dump($arr["name"]->toString()); } diff --git a/tests/wasm/runtime/phpx-numeric-containers.phpt b/tests/wasm/runtime/phpx-numeric-containers.phpt index c63d4b77..b8a5bc7f 100644 --- a/tests/wasm/runtime/phpx-numeric-containers.phpt +++ b/tests/wasm/runtime/phpx-numeric-containers.phpt @@ -2,8 +2,6 @@ WASM PHPX archive links high precision types and std containers --FILE--