From 04507368c1c12820e2f157fd23cb8a30126fee24 Mon Sep 17 00:00:00 2001 From: PurHur Date: Sun, 17 May 2026 12:31:22 +0000 Subject: [PATCH] Stdlib: pi, angle conversion, log/exp, and float checks Add VM/JIT implementations and PHPT compliance specs for basic math stdlib functions (pi, deg2rad, rad2deg, log, exp, is_nan, is_finite, is_infinite) plus an integration test covering angle conversion and transcendental helpers. Co-authored-by: Cursor --- ext/standard/Module.php | 20 +++++- ext/standard/deg2rad.php | 65 +++++++++++++++++ ext/standard/exp.php | 63 +++++++++++++++++ ext/standard/is_finite.php | 70 +++++++++++++++++++ ext/standard/is_infinite.php | 70 +++++++++++++++++++ ext/standard/is_nan.php | 70 +++++++++++++++++++ ext/standard/log.php | 63 +++++++++++++++++ ext/standard/pi.php | 48 +++++++++++++ ext/standard/rad2deg.php | 65 +++++++++++++++++ test/compliance/cases/stdlib/deg2rad.phpt | 11 +++ test/compliance/cases/stdlib/exp.phpt | 11 +++ test/compliance/cases/stdlib/is_finite.phpt | 13 ++++ test/compliance/cases/stdlib/is_infinite.phpt | 13 ++++ test/compliance/cases/stdlib/is_nan.phpt | 11 +++ test/compliance/cases/stdlib/log.phpt | 11 +++ test/compliance/cases/stdlib/pi.phpt | 7 ++ test/compliance/cases/stdlib/rad2deg.phpt | 11 +++ test/real/cases/stdlib_trig_math.phpt | 13 ++++ 18 files changed, 634 insertions(+), 1 deletion(-) create mode 100644 ext/standard/deg2rad.php create mode 100644 ext/standard/exp.php create mode 100644 ext/standard/is_finite.php create mode 100644 ext/standard/is_infinite.php create mode 100644 ext/standard/is_nan.php create mode 100644 ext/standard/log.php create mode 100644 ext/standard/pi.php create mode 100644 ext/standard/rad2deg.php create mode 100644 test/compliance/cases/stdlib/deg2rad.phpt create mode 100644 test/compliance/cases/stdlib/exp.phpt create mode 100644 test/compliance/cases/stdlib/is_finite.phpt create mode 100644 test/compliance/cases/stdlib/is_infinite.phpt create mode 100644 test/compliance/cases/stdlib/is_nan.phpt create mode 100644 test/compliance/cases/stdlib/log.phpt create mode 100644 test/compliance/cases/stdlib/pi.phpt create mode 100644 test/compliance/cases/stdlib/rad2deg.phpt create mode 100644 test/real/cases/stdlib_trig_math.phpt diff --git a/ext/standard/Module.php b/ext/standard/Module.php index bfcb897dc5b..beb0800947a 100755 --- a/ext/standard/Module.php +++ b/ext/standard/Module.php @@ -26,6 +26,14 @@ public function getFunctions(): array new floor(), new round(), new sqrt(), + new pi(), + new deg2rad(), + new rad2deg(), + new log(), + new exp(), + new is_nan(), + new is_finite(), + new is_infinite(), new pow(), new fmod(), new intval(), @@ -70,7 +78,7 @@ public function jitInit(JIT\Context $context): void $context->registerFunction('strtol', $fn); } $double = $context->getTypeFromString('double'); - foreach (['ceil', 'floor', 'round', 'sqrt', 'pow', 'fmod'] as $name) { + foreach (['ceil', 'floor', 'round', 'sqrt', 'log', 'exp', 'pow', 'fmod'] as $name) { try { $context->lookupFunction($name); } catch (\Throwable $e) { @@ -80,5 +88,15 @@ public function jitInit(JIT\Context $context): void $context->registerFunction($name, $fn); } } + $i32 = $context->getTypeFromString('int32'); + foreach (['isnan', 'isfinite', 'isinf'] as $name) { + try { + $context->lookupFunction($name); + } catch (\Throwable $e) { + $ft = $context->context->functionType($i32, false, $double); + $fn = $context->module->addFunction($name, $ft); + $context->registerFunction($name, $fn); + } + } } } diff --git a/ext/standard/deg2rad.php b/ext/standard/deg2rad.php new file mode 100644 index 00000000000..32149dfaead --- /dev/null +++ b/ext/standard/deg2rad.php @@ -0,0 +1,65 @@ +calledArgs)) { + throw new \LogicException('deg2rad() requires exactly one argument'); + } + $v = $frame->calledArgs[0]->resolveIndirect(); + if (null === $frame->returnVar) { + return; + } + $frame->returnVar->float(self::FACTOR * self::toFloat($v)); + } + + public Context $context; + + public function call(Context $context, JITVariable ...$args): Value + { + $this->context = $context; + if (1 !== count($args)) { + throw new \LogicException('deg2rad() requires exactly one argument'); + } + $double = $context->getTypeFromString('double'); + $asFloat = pow::toJitDouble($context, $args[0], $double); + $factor = $double->constReal(self::FACTOR); + + return $context->builder->fMul($asFloat, $factor); + } + + private static function toFloat(Variable $v): float + { + if (Variable::TYPE_INTEGER === $v->type) { + return (float) $v->toInt(); + } + if (Variable::TYPE_FLOAT === $v->type) { + return $v->toFloat(); + } + throw new \LogicException('deg2rad() only supports integers and floats in this compiler build'); + } +} diff --git a/ext/standard/exp.php b/ext/standard/exp.php new file mode 100644 index 00000000000..01eca8f9452 --- /dev/null +++ b/ext/standard/exp.php @@ -0,0 +1,63 @@ +calledArgs)) { + throw new \LogicException('exp() requires exactly one argument'); + } + $v = $frame->calledArgs[0]->resolveIndirect(); + if (null === $frame->returnVar) { + return; + } + $frame->returnVar->float(\exp(self::toFloat($v))); + } + + public Context $context; + + public function call(Context $context, JITVariable ...$args): Value + { + $this->context = $context; + if (1 !== count($args)) { + throw new \LogicException('exp() requires exactly one argument'); + } + $double = $context->getTypeFromString('double'); + $asFloat = pow::toJitDouble($context, $args[0], $double); + $fn = $context->lookupFunction('exp'); + + return $context->builder->call($fn, $asFloat); + } + + private static function toFloat(Variable $v): float + { + if (Variable::TYPE_INTEGER === $v->type) { + return (float) $v->toInt(); + } + if (Variable::TYPE_FLOAT === $v->type) { + return $v->toFloat(); + } + throw new \LogicException('exp() only supports integers and floats in this compiler build'); + } +} diff --git a/ext/standard/is_finite.php b/ext/standard/is_finite.php new file mode 100644 index 00000000000..d579227b467 --- /dev/null +++ b/ext/standard/is_finite.php @@ -0,0 +1,70 @@ +calledArgs)) { + throw new \LogicException('is_finite() requires exactly one argument'); + } + $v = $frame->calledArgs[0]->resolveIndirect(); + if (null === $frame->returnVar) { + return; + } + if (Variable::TYPE_INTEGER === $v->type) { + $frame->returnVar->bool(true); + + return; + } + if (Variable::TYPE_FLOAT === $v->type) { + $frame->returnVar->bool(\is_finite($v->toFloat())); + + return; + } + throw new \LogicException('is_finite() only supports integers and floats in this compiler build'); + } + + public Context $context; + + public function call(Context $context, JITVariable ...$args): Value + { + $this->context = $context; + if (1 !== count($args)) { + throw new \LogicException('is_finite() requires exactly one argument'); + } + if (JITVariable::TYPE_NATIVE_LONG === $args[0]->type) { + return $context->constantFromBool(true); + } + if (JITVariable::TYPE_NATIVE_DOUBLE !== $args[0]->type) { + throw new \LogicException('is_finite() only supports integers and floats in this compiler build'); + } + $asFloat = $context->helper->loadValue($args[0]); + $fn = $context->lookupFunction('isfinite'); + $raw = $context->builder->call($fn, $asFloat); + $zero = $raw->typeOf()->constInt(0, false); + + return $context->builder->icmp(Builder::INT_NE, $raw, $zero); + } +} diff --git a/ext/standard/is_infinite.php b/ext/standard/is_infinite.php new file mode 100644 index 00000000000..a0673253021 --- /dev/null +++ b/ext/standard/is_infinite.php @@ -0,0 +1,70 @@ +calledArgs)) { + throw new \LogicException('is_infinite() requires exactly one argument'); + } + $v = $frame->calledArgs[0]->resolveIndirect(); + if (null === $frame->returnVar) { + return; + } + if (Variable::TYPE_INTEGER === $v->type) { + $frame->returnVar->bool(false); + + return; + } + if (Variable::TYPE_FLOAT === $v->type) { + $frame->returnVar->bool(\is_infinite($v->toFloat())); + + return; + } + throw new \LogicException('is_infinite() only supports integers and floats in this compiler build'); + } + + public Context $context; + + public function call(Context $context, JITVariable ...$args): Value + { + $this->context = $context; + if (1 !== count($args)) { + throw new \LogicException('is_infinite() requires exactly one argument'); + } + if (JITVariable::TYPE_NATIVE_LONG === $args[0]->type) { + return $context->constantFromBool(false); + } + if (JITVariable::TYPE_NATIVE_DOUBLE !== $args[0]->type) { + throw new \LogicException('is_infinite() only supports integers and floats in this compiler build'); + } + $asFloat = $context->helper->loadValue($args[0]); + $fn = $context->lookupFunction('isinf'); + $raw = $context->builder->call($fn, $asFloat); + $zero = $raw->typeOf()->constInt(0, false); + + return $context->builder->icmp(Builder::INT_NE, $raw, $zero); + } +} diff --git a/ext/standard/is_nan.php b/ext/standard/is_nan.php new file mode 100644 index 00000000000..778057ea1b7 --- /dev/null +++ b/ext/standard/is_nan.php @@ -0,0 +1,70 @@ +calledArgs)) { + throw new \LogicException('is_nan() requires exactly one argument'); + } + $v = $frame->calledArgs[0]->resolveIndirect(); + if (null === $frame->returnVar) { + return; + } + if (Variable::TYPE_INTEGER === $v->type) { + $frame->returnVar->bool(false); + + return; + } + if (Variable::TYPE_FLOAT === $v->type) { + $frame->returnVar->bool(\is_nan($v->toFloat())); + + return; + } + throw new \LogicException('is_nan() only supports integers and floats in this compiler build'); + } + + public Context $context; + + public function call(Context $context, JITVariable ...$args): Value + { + $this->context = $context; + if (1 !== count($args)) { + throw new \LogicException('is_nan() requires exactly one argument'); + } + if (JITVariable::TYPE_NATIVE_LONG === $args[0]->type) { + return $context->constantFromBool(false); + } + if (JITVariable::TYPE_NATIVE_DOUBLE !== $args[0]->type) { + throw new \LogicException('is_nan() only supports integers and floats in this compiler build'); + } + $asFloat = $context->helper->loadValue($args[0]); + $fn = $context->lookupFunction('isnan'); + $raw = $context->builder->call($fn, $asFloat); + $zero = $raw->typeOf()->constInt(0, false); + + return $context->builder->icmp(Builder::INT_NE, $raw, $zero); + } +} diff --git a/ext/standard/log.php b/ext/standard/log.php new file mode 100644 index 00000000000..7a0f09e4a10 --- /dev/null +++ b/ext/standard/log.php @@ -0,0 +1,63 @@ +calledArgs)) { + throw new \LogicException('log() requires exactly one argument'); + } + $v = $frame->calledArgs[0]->resolveIndirect(); + if (null === $frame->returnVar) { + return; + } + $frame->returnVar->float(\log(self::toFloat($v))); + } + + public Context $context; + + public function call(Context $context, JITVariable ...$args): Value + { + $this->context = $context; + if (1 !== count($args)) { + throw new \LogicException('log() requires exactly one argument'); + } + $double = $context->getTypeFromString('double'); + $asFloat = pow::toJitDouble($context, $args[0], $double); + $fn = $context->lookupFunction('log'); + + return $context->builder->call($fn, $asFloat); + } + + private static function toFloat(Variable $v): float + { + if (Variable::TYPE_INTEGER === $v->type) { + return (float) $v->toInt(); + } + if (Variable::TYPE_FLOAT === $v->type) { + return $v->toFloat(); + } + throw new \LogicException('log() only supports integers and floats in this compiler build'); + } +} diff --git a/ext/standard/pi.php b/ext/standard/pi.php new file mode 100644 index 00000000000..e85161d15be --- /dev/null +++ b/ext/standard/pi.php @@ -0,0 +1,48 @@ +calledArgs)) { + throw new \LogicException('pi() takes no arguments'); + } + if (null === $frame->returnVar) { + return; + } + $frame->returnVar->float(\M_PI); + } + + public Context $context; + + public function call(Context $context, JITVariable ...$args): Value + { + $this->context = $context; + if (0 !== count($args)) { + throw new \LogicException('pi() takes no arguments'); + } + $double = $context->getTypeFromString('double'); + + return $double->constReal(\M_PI); + } +} diff --git a/ext/standard/rad2deg.php b/ext/standard/rad2deg.php new file mode 100644 index 00000000000..7fc59333160 --- /dev/null +++ b/ext/standard/rad2deg.php @@ -0,0 +1,65 @@ +calledArgs)) { + throw new \LogicException('rad2deg() requires exactly one argument'); + } + $v = $frame->calledArgs[0]->resolveIndirect(); + if (null === $frame->returnVar) { + return; + } + $frame->returnVar->float(self::FACTOR * self::toFloat($v)); + } + + public Context $context; + + public function call(Context $context, JITVariable ...$args): Value + { + $this->context = $context; + if (1 !== count($args)) { + throw new \LogicException('rad2deg() requires exactly one argument'); + } + $double = $context->getTypeFromString('double'); + $asFloat = pow::toJitDouble($context, $args[0], $double); + $factor = $double->constReal(self::FACTOR); + + return $context->builder->fMul($asFloat, $factor); + } + + private static function toFloat(Variable $v): float + { + if (Variable::TYPE_INTEGER === $v->type) { + return (float) $v->toInt(); + } + if (Variable::TYPE_FLOAT === $v->type) { + return $v->toFloat(); + } + throw new \LogicException('rad2deg() only supports integers and floats in this compiler build'); + } +} diff --git a/test/compliance/cases/stdlib/deg2rad.phpt b/test/compliance/cases/stdlib/deg2rad.phpt new file mode 100644 index 00000000000..1530c8a9e18 --- /dev/null +++ b/test/compliance/cases/stdlib/deg2rad.phpt @@ -0,0 +1,11 @@ +--TEST-- +stdlib deg2rad() for integers and floats +--FILE-- +