fix(codegen): PHP semantics for division, modulo, shifts and compound assignment on typed scalars (#45) --skip-tests
* fix(codegen): route typed division, int modulo and shifts through PHP operators
Typed int/int and float-typed division fell through to a raw C++ '/':
7 / 2 on zend_long operands truncated to 3 where PHP returns 3.5,
integer division by zero was undefined behavior and float division by
zero produced INF, while PHP raises a catchable DivisionByZeroError in
both cases; PHP_INT_MIN / -1 also has UB in C++ but promotes to float
in PHP. The '%' guard only routed through php::fn::mod when NOT both
operands were int, so both-int modulo kept raw C++ '%' (UB for a zero
divisor and for PHP_INT_MIN % -1, which PHP defines as 0). Dynamic int
shifts were raw C++ too: PHP defines counts >= the word size as 0 (or
-1 for negative right shifts) and raises ArithmeticError for negative
counts, both undefined in C++.
Route all of these through the encapsulated php::Var operators /
php::fn::mod in non-native mode, matching the existing +/-/* pattern.
Constant folds are untouched; constant shifts that C++ defines
identically to PHP still emit raw operators.
* fix(parser): defer literal zero divisors to the runtime DivisionByZeroError
A literal `/ 0` or `% 0` (including `/=` and `%=`) was a compile-time
fatal, rejecting valid PHP: Zend compiles it and raises a catchable
DivisionByZeroError only when the statement executes, so dead or
guarded code like `if ($cond) { $x = 1 % 0; }` must compile. The
equivalent spellings `1 % (1 - 1)` and `10 / ZERO` were already
accepted and lowered to the catchable runtime error.
Give the literal spelling the same lowering: route the operation
through the encapsulated Variant operators (compound assignments on
Variant slots already defer via operator/= and operator%=), keep a
compile-time warning in normal mode, and keep the fatal in native mode
where the C++ operation would be undefined behavior.
The six OperatorTest cases asserting the old compile-time fatal now
assert the runtime-error lowering instead.
* test(operator): platform-neutral literal suffixes, PHP division on typed ints
The literal-division assertions hardcoded the macOS zend_long suffix
(LL); Linux emits L, so they now match either. native-type.phpt
asserted the truncating int division this change removes: division on
typed int operands follows PHP semantics in non-native mode, consistent
with the pre-existing + - * routing (use native_types keeps raw
division), so std::int(10) / 4 is now float(2.5).
* fix(codegen): literal zero divisors on native scalar slots raise the runtime error
Downgrading the literal-zero compile fatal to a warning exposed the raw
C++ compound path on typed native slots: `int $value; $value /= 0`
compiled to `value /= php::toInt(0L)` and killed the process with
SIGFPE instead of the catchable DivisionByZeroError (`%= 0` likewise;
float `/= 0.0` produced INF). A proven zero divisor always throws
before any assignment happens, so the whole compound lowers to the
PHP-semantics binary operation through php::Var and the target is left
untouched. Native-types mode keeps the compile-time rejection.
* fix(codegen): exclude explicit native scalars from PHP arithmetic routing
std::int()/std::float() opt into native C++ arithmetic independently of
the file-wide native_types declaration, and the existing + - * routing
already honors that via isExplicitNativeArithmeticExpr(). The new
division, both-int modulo and dynamic shift branches, and the
literal-zero compound lowering, bypassed it: std::int(10) / 4 changed
from int(2) to float(2.5) and native-type.phpt was updated to encode
the regression.
Every new PHP-semantics branch now skips explicitly native operands,
native-type.phpt is restored to int(2), and a proven zero divisor on an
explicit native slot keeps the compile-time rejection used by
native_types mode instead of being silently rerouted to PHP semantics.
Boundary coverage added on both sides: ordinary typed parameters keep
PHP behavior (7 / 2 is 3.5, -7 % 2 is -1) while std::int()/std::float()
keep native division, modulo and shifts.
master
parent
48c95e9f9b
commit
8f8ae77ec8
12 changed files with 472 additions and 21 deletions
@ -0,0 +1,7 @@ |
||||
<?php |
||||
function main(): void |
||||
{ |
||||
$n = std::int(9); |
||||
$n /= 0; |
||||
var_dump($n); |
||||
} |
||||
@ -0,0 +1,26 @@ |
||||
<?php |
||||
|
||||
function divTypedInts(int $a, int $b): float |
||||
{ |
||||
return $a / $b; |
||||
} |
||||
|
||||
function divTypedFloats(float $a, float $b): float |
||||
{ |
||||
return $a / $b; |
||||
} |
||||
|
||||
function modTypedInts(int $a, int $b): int |
||||
{ |
||||
return $a % $b; |
||||
} |
||||
|
||||
function shiftLeftTypedInts(int $a, int $b): int |
||||
{ |
||||
return $a << $b; |
||||
} |
||||
|
||||
function shiftRightTypedInts(int $a, int $b): int |
||||
{ |
||||
return $a >> $b; |
||||
} |
||||
@ -0,0 +1,14 @@ |
||||
<?php |
||||
|
||||
/** |
||||
* std::int()/std::float() opt into native C++ arithmetic, so a proven zero |
||||
* divisor cannot be silently rerouted to PHP semantics; it keeps the |
||||
* compile-time rejection that native_types mode uses. |
||||
*/ |
||||
class NativeSlotZeroDivisorTest extends BaseTest |
||||
{ |
||||
public function testExplicitNativeSlotZeroDivisorIsRejected(): void |
||||
{ |
||||
$this->exec('Cannot divide or modulo by zero', 'native_slot_zero_divisor.php'); |
||||
} |
||||
} |
||||
@ -0,0 +1,55 @@ |
||||
<?php |
||||
|
||||
use TypePhp\CompilerTest; |
||||
|
||||
/** |
||||
* Typed int/float division, int modulo and int shifts must not be emitted as |
||||
* raw C++ operators: raw zend_long division truncates (PHP: 7 / 2 === 3.5), |
||||
* a zero divisor must raise the catchable DivisionByZeroError instead of |
||||
* being undefined behavior (int) or INF (float), and out-of-range shift |
||||
* counts are undefined behavior in C++ while PHP defines them. |
||||
*/ |
||||
final class TypedScalarArithmeticCodegenTest extends \BaseTest |
||||
{ |
||||
public function testTypedIntDivisionRoutesThroughVariant(): void |
||||
{ |
||||
$code = $this->compileFixture(); |
||||
|
||||
self::assertStringContainsString('((php::Var(a)) / (php::Var(b)))', $code); |
||||
self::assertStringNotContainsString('((a) / (b))', $code); |
||||
} |
||||
|
||||
public function testTypedIntModuloRoutesThroughPhpMod(): void |
||||
{ |
||||
$code = $this->compileFixture(); |
||||
|
||||
self::assertStringContainsString('php::fn::mod(a, b)', $code); |
||||
self::assertStringNotContainsString('((a) % (b))', $code); |
||||
} |
||||
|
||||
public function testTypedIntShiftsRouteThroughVariant(): void |
||||
{ |
||||
$code = $this->compileFixture(); |
||||
|
||||
self::assertStringContainsString('((php::Var(a)) << (php::Var(b)))', $code); |
||||
self::assertStringContainsString('((php::Var(a)) >> (php::Var(b)))', $code); |
||||
self::assertStringNotContainsString('((a) << (b))', $code); |
||||
self::assertStringNotContainsString('((a) >> (b))', $code); |
||||
} |
||||
|
||||
private function compileFixture(): string |
||||
{ |
||||
global $translator; |
||||
|
||||
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); |
||||
$translator = $compiler; |
||||
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/typed-scalar-arithmetic-codegen.php'; |
||||
$compiler->addFiles([$source]); |
||||
$compiler->prepareFile($source); |
||||
$generated = $compiler->convertFile($source); |
||||
$code = file_get_contents($generated); |
||||
|
||||
self::assertIsString($code); |
||||
return $code; |
||||
} |
||||
} |
||||
@ -0,0 +1,54 @@ |
||||
--TEST-- |
||||
Literal zero divisors compile and raise catchable DivisionByZeroError at runtime |
||||
--FILE-- |
||||
<?php |
||||
declare(strict_types=1); |
||||
|
||||
function main(): void |
||||
{ |
||||
$cond = false; |
||||
if ($cond) { |
||||
$x = 1 % 0; |
||||
var_dump($x); |
||||
} |
||||
echo "dead code ok\n"; |
||||
|
||||
try { |
||||
$y = 10 / 0; |
||||
var_dump($y); |
||||
} catch (DivisionByZeroError $e) { |
||||
echo "caught: " . $e->getMessage() . "\n"; |
||||
} |
||||
|
||||
try { |
||||
$f = 1.0 / 0.0; |
||||
var_dump($f); |
||||
} catch (DivisionByZeroError $e) { |
||||
echo "caught: " . $e->getMessage() . "\n"; |
||||
} |
||||
|
||||
$v = 10; |
||||
try { |
||||
$v /= 0; |
||||
} catch (DivisionByZeroError $e) { |
||||
echo "caught: " . $e->getMessage() . "\n"; |
||||
} |
||||
var_dump($v); |
||||
|
||||
$w = 10; |
||||
try { |
||||
$w %= 0; |
||||
} catch (DivisionByZeroError $e) { |
||||
echo "caught: " . $e->getMessage() . "\n"; |
||||
} |
||||
var_dump($w); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
dead code ok |
||||
caught: Division by zero |
||||
caught: Division by zero |
||||
caught: Division by zero |
||||
int(10) |
||||
caught: Modulo by zero |
||||
int(10) |
||||
@ -0,0 +1,45 @@ |
||||
--TEST-- |
||||
Literal zero divisors on typed native slots raise catchable DivisionByZeroError |
||||
--FILE-- |
||||
<?php |
||||
function divInt(int $value): mixed |
||||
{ |
||||
try { |
||||
$value /= 0; |
||||
} catch (DivisionByZeroError $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
return $value; |
||||
} |
||||
|
||||
function modInt(int $value): mixed |
||||
{ |
||||
try { |
||||
$value %= 0; |
||||
} catch (DivisionByZeroError $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
return $value; |
||||
} |
||||
|
||||
function divFloat(float $value): mixed |
||||
{ |
||||
try { |
||||
$value /= 0.0; |
||||
} catch (DivisionByZeroError $e) { |
||||
return $e->getMessage(); |
||||
} |
||||
return $value; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
var_dump(divInt(7)); |
||||
var_dump(modInt(7)); |
||||
var_dump(divFloat(1.5)); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
string(16) "Division by zero" |
||||
string(14) "Modulo by zero" |
||||
string(16) "Division by zero" |
||||
@ -0,0 +1,41 @@ |
||||
--TEST-- |
||||
Typed int and float division follows PHP semantics (fractional result, DivisionByZeroError) |
||||
--FILE-- |
||||
<?php |
||||
declare(strict_types=1); |
||||
|
||||
function divInts(int $a, int $b): float |
||||
{ |
||||
return $a / $b; |
||||
} |
||||
|
||||
function divFloats(float $a, float $b): float |
||||
{ |
||||
return $a / $b; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
var_dump(divInts(7, 2)); |
||||
var_dump(divInts(6, 3)); |
||||
var_dump(divInts(PHP_INT_MIN, -1)); |
||||
try { |
||||
divInts(7, 0); |
||||
} catch (DivisionByZeroError $e) { |
||||
echo "caught: " . $e->getMessage() . "\n"; |
||||
} |
||||
var_dump(divFloats(7.0, 2.0)); |
||||
try { |
||||
divFloats(1.5, 0.0); |
||||
} catch (DivisionByZeroError $e) { |
||||
echo "caught: " . $e->getMessage() . "\n"; |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
float(3.5) |
||||
float(2) |
||||
float(9.223372036854776E+18) |
||||
caught: Division by zero |
||||
float(3.5) |
||||
caught: Division by zero |
||||
@ -0,0 +1,62 @@ |
||||
--TEST-- |
||||
Typed int modulo and shifts follow PHP semantics (errors, boundaries) |
||||
--FILE-- |
||||
<?php |
||||
declare(strict_types=1); |
||||
|
||||
function modInts(int $a, int $b): int |
||||
{ |
||||
return $a % $b; |
||||
} |
||||
|
||||
function shiftLeft(int $a, int $b): int |
||||
{ |
||||
return $a << $b; |
||||
} |
||||
|
||||
function shiftRight(int $a, int $b): int |
||||
{ |
||||
return $a >> $b; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
var_dump(modInts(7, 3)); |
||||
var_dump(modInts(-7, 3)); |
||||
var_dump(modInts(PHP_INT_MIN, -1)); |
||||
try { |
||||
modInts(7, 0); |
||||
} catch (DivisionByZeroError $e) { |
||||
echo "caught: " . $e->getMessage() . "\n"; |
||||
} |
||||
var_dump(shiftLeft(1, 3)); |
||||
var_dump(shiftLeft(1, 63)); |
||||
var_dump(shiftLeft(1, 64)); |
||||
try { |
||||
shiftLeft(1, -1); |
||||
} catch (ArithmeticError $e) { |
||||
echo "caught: " . $e->getMessage() . "\n"; |
||||
} |
||||
var_dump(shiftRight(-8, 1)); |
||||
var_dump(shiftRight(-8, 65)); |
||||
var_dump(shiftRight(8, 65)); |
||||
try { |
||||
shiftRight(1, -1); |
||||
} catch (ArithmeticError $e) { |
||||
echo "caught: " . $e->getMessage() . "\n"; |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(1) |
||||
int(-1) |
||||
int(0) |
||||
caught: Modulo by zero |
||||
int(8) |
||||
int(-9223372036854775808) |
||||
int(0) |
||||
caught: Bit shift by negative number |
||||
int(-4) |
||||
int(-1) |
||||
int(0) |
||||
caught: Bit shift by negative number |
||||
@ -0,0 +1,25 @@ |
||||
--TEST-- |
||||
Ordinary typed scalars use PHP arithmetic; std::int()/std::float() stay native |
||||
--FILE-- |
||||
<?php |
||||
function phpDiv(int $a, int $b): mixed { return $a / $b; } |
||||
function phpMod(int $a, int $b): mixed { return $a % $b; } |
||||
|
||||
function main(): void |
||||
{ |
||||
// Ordinary typed parameters follow PHP semantics. |
||||
var_dump(phpDiv(7, 2)); |
||||
var_dump(phpMod(-7, 2)); |
||||
|
||||
// Explicit native scalars opt into C++ semantics. |
||||
$i = std::int(10); |
||||
var_dump($i / 4); |
||||
$f = std::float(10.0); |
||||
var_dump($f / 4); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
float(3.5) |
||||
int(-1) |
||||
int(2) |
||||
float(2.5) |
||||
Loading…
Reference in new issue