From 3168db8e71a3c61f5951499406c85ddaa8e06edc Mon Sep 17 00:00:00 2001 From: kylekatarnls Date: Sat, 9 Aug 2025 15:24:13 +0200 Subject: [PATCH 1/4] Implement clamp function Co-authored-by: thinkverse --- ext/standard/basic_functions.stub.php | 6 +++ ext/standard/basic_functions_arginfo.h | 14 +++++++ ext/standard/math.c | 53 ++++++++++++++++++++++++++ ext/standard/tests/math/clamp.phpt | 49 ++++++++++++++++++++++++ 4 files changed, 122 insertions(+) create mode 100644 ext/standard/tests/math/clamp.phpt diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index 541415a5ab918..6f6dc736a5510 100644 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -1629,6 +1629,12 @@ function min(mixed $value, mixed ...$values): mixed {} */ function max(mixed $value, mixed ...$values): mixed {} +/** + * @compile-time-eval + * @frameless-function {"arity": 3} + */ +function clamp(mixed $value, mixed $min, mixed $max): mixed {} + function array_walk(array|object &$array, callable $callback, mixed $arg = UNKNOWN): true {} function array_walk_recursive(array|object &$array, callable $callback, mixed $arg = UNKNOWN): true {} diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index ba9d1710137cc..8b27f4b65c7ba 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -138,6 +138,12 @@ ZEND_END_ARG_INFO() #define arginfo_max arginfo_min +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_clamp, 0, 3, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, value, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, min, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, max, IS_MIXED, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_array_walk, 0, 2, IS_TRUE, 0) ZEND_ARG_TYPE_MASK(1, array, MAY_BE_ARRAY|MAY_BE_OBJECT, NULL) ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0) @@ -2197,6 +2203,12 @@ static const zend_frameless_function_info frameless_function_infos_max[] = { { 0 }, }; +ZEND_FRAMELESS_FUNCTION(clamp, 3); +static const zend_frameless_function_info frameless_function_infos_clamp[] = { + { ZEND_FRAMELESS_FUNCTION_NAME(clamp, 3), 3 }, + { 0 }, +}; + ZEND_FRAMELESS_FUNCTION(in_array, 2); ZEND_FRAMELESS_FUNCTION(in_array, 3); static const zend_frameless_function_info frameless_function_infos_in_array[] = { @@ -2332,6 +2344,7 @@ ZEND_FUNCTION(current); ZEND_FUNCTION(key); ZEND_FUNCTION(min); ZEND_FUNCTION(max); +ZEND_FUNCTION(clamp); ZEND_FUNCTION(array_walk); ZEND_FUNCTION(array_walk_recursive); ZEND_FUNCTION(in_array); @@ -2925,6 +2938,7 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(key, arginfo_key) ZEND_RAW_FENTRY("min", zif_min, arginfo_min, ZEND_ACC_COMPILE_TIME_EVAL, frameless_function_infos_min, NULL) ZEND_RAW_FENTRY("max", zif_max, arginfo_max, ZEND_ACC_COMPILE_TIME_EVAL, frameless_function_infos_max, NULL) + ZEND_RAW_FENTRY("clamp", zif_clamp, arginfo_clamp, ZEND_ACC_COMPILE_TIME_EVAL, frameless_function_infos_clamp, NULL) ZEND_FE(array_walk, arginfo_array_walk) ZEND_FE(array_walk_recursive, arginfo_array_walk_recursive) ZEND_RAW_FENTRY("in_array", zif_in_array, arginfo_in_array, ZEND_ACC_COMPILE_TIME_EVAL, frameless_function_infos_in_array, NULL) diff --git a/ext/standard/math.c b/ext/standard/math.c index 142d473864f75..90a62e36cd908 100644 --- a/ext/standard/math.c +++ b/ext/standard/math.c @@ -389,6 +389,59 @@ PHP_FUNCTION(round) } /* }}} */ +/* {{{ Return the given value if in range of min and max */ +PHP_FUNCTION(clamp) +{ + zval *zvalue, *zmin, *zmax; + + ZEND_PARSE_PARAMETERS_START(3, 3) + Z_PARAM_ZVAL(zvalue) + Z_PARAM_ZVAL(zmin) + Z_PARAM_ZVAL(zmax) + ZEND_PARSE_PARAMETERS_END(); + + if (zend_compare(zmin, zmax) > 0) { + zend_argument_value_error(2, "must be smaller than or equal to argument #3 ($max)"); + RETURN_THROWS(); + } + + if (zend_compare(zmax, zvalue) == -1) { + RETURN_COPY(zmax); + } + + if (zend_compare(zvalue, zmin) == -1) { + RETURN_COPY(zmin); + } + + RETURN_COPY(zvalue); +} +/* }}} */ + +/* {{{ Return the given value if in range of min and max */ +ZEND_FRAMELESS_FUNCTION(clamp, 3) +{ + zval *zvalue, *zmin, *zmax; + Z_FLF_PARAM_ZVAL(1, zvalue); + Z_FLF_PARAM_ZVAL(2, zmin); + Z_FLF_PARAM_ZVAL(3, zmax); + + if (zend_compare(zmin, zmax) > 0) { + zend_argument_value_error(2, "must be smaller than or equal to argument #3 ($max)"); + RETURN_THROWS(); + } + + if (zend_compare(zmax, zvalue) == -1) { + RETURN_COPY_VALUE(zmax); + } + + if (zend_compare(zvalue, zmin) == -1) { + RETURN_COPY_VALUE(zmin); + } + + RETURN_COPY_VALUE(zvalue); +} +/* }}} */ + /* {{{ Returns the sine of the number in radians */ PHP_FUNCTION(sin) { diff --git a/ext/standard/tests/math/clamp.phpt b/ext/standard/tests/math/clamp.phpt new file mode 100644 index 0000000000000..cec0da9ea47b2 --- /dev/null +++ b/ext/standard/tests/math/clamp.phpt @@ -0,0 +1,49 @@ +--TEST-- +clamp() tests +--INI-- +precision=14 +date.timezone = UTC +--FILE-- +format('Y-m-d'), "\n"; +echo clamp(new \DateTimeImmutable('2025-08-20'), new \DateTimeImmutable('2025-08-15'), new \DateTimeImmutable('2025-09-15'))->format('Y-m-d'), "\n"; + +?> +--EXPECT-- +int(2) +int(1) +int(3) +int(2) +float(2.5) +float(2.5) +float(1.3) +float(3.141592653589793) +double(NAN) +int(4) +int(6) +int(7) +int(6) +string(1) "c" +string(1) "d" +2025-08-15 +2025-08-20 +2025-08-15 +2025-08-20 From 90ab306a9b1b834a2c776260ef1ea0577d952e3b Mon Sep 17 00:00:00 2001 From: kylekatarnls Date: Sun, 10 Aug 2025 10:22:18 +0200 Subject: [PATCH 2/4] Improve NAN handling --- ext/standard/math.c | 20 ++++++++++++++++++++ ext/standard/tests/math/clamp.phpt | 28 ++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/ext/standard/math.c b/ext/standard/math.c index 90a62e36cd908..910b68368cdd8 100644 --- a/ext/standard/math.c +++ b/ext/standard/math.c @@ -400,6 +400,16 @@ PHP_FUNCTION(clamp) Z_PARAM_ZVAL(zmax) ZEND_PARSE_PARAMETERS_END(); + if (EXPECTED(Z_TYPE_P(zmin) == IS_DOUBLE) && UNEXPECTED(zend_isnan(Z_DVAL_P(zmin)))) { + zend_argument_value_error(2, "cannot be NAN"); + RETURN_THROWS(); + } + + if (EXPECTED(Z_TYPE_P(zmax) == IS_DOUBLE) && UNEXPECTED(zend_isnan(Z_DVAL_P(zmax)))) { + zend_argument_value_error(3, "cannot be NAN"); + RETURN_THROWS(); + } + if (zend_compare(zmin, zmax) > 0) { zend_argument_value_error(2, "must be smaller than or equal to argument #3 ($max)"); RETURN_THROWS(); @@ -425,6 +435,16 @@ ZEND_FRAMELESS_FUNCTION(clamp, 3) Z_FLF_PARAM_ZVAL(2, zmin); Z_FLF_PARAM_ZVAL(3, zmax); + if (EXPECTED(Z_TYPE_P(zmin) == IS_DOUBLE) && UNEXPECTED(zend_isnan(Z_DVAL_P(zmin)))) { + zend_argument_value_error(2, "cannot be NAN"); + RETURN_THROWS(); + } + + if (EXPECTED(Z_TYPE_P(zmax) == IS_DOUBLE) && UNEXPECTED(zend_isnan(Z_DVAL_P(zmax)))) { + zend_argument_value_error(3, "cannot be NAN"); + RETURN_THROWS(); + } + if (zend_compare(zmin, zmax) > 0) { zend_argument_value_error(2, "must be smaller than or equal to argument #3 ($max)"); RETURN_THROWS(); diff --git a/ext/standard/tests/math/clamp.phpt b/ext/standard/tests/math/clamp.phpt index cec0da9ea47b2..2699cbd9188d2 100644 --- a/ext/standard/tests/math/clamp.phpt +++ b/ext/standard/tests/math/clamp.phpt @@ -15,10 +15,6 @@ var_dump(clamp(2.5, 1.3, 3.4)); var_dump(clamp(0, 1.3, 3.4)); var_dump(clamp(M_PI, -INF, INF)); var_dump(clamp(NAN, 4, 6)); -var_dump(clamp(4, NAN, 6)); -var_dump(clamp(8, NAN, 6)); -var_dump(clamp(7, 6, NAN)); -var_dump(clamp(4, 6, NAN)); var_dump(clamp("a", "c", "g")); var_dump(clamp("d", "c", "g")); echo clamp('2025-08-01', '2025-08-15', '2025-09-15'), "\n"; @@ -26,6 +22,23 @@ echo clamp('2025-08-20', '2025-08-15', '2025-09-15'), "\n"; echo clamp(new \DateTimeImmutable('2025-08-01'), new \DateTimeImmutable('2025-08-15'), new \DateTimeImmutable('2025-09-15'))->format('Y-m-d'), "\n"; echo clamp(new \DateTimeImmutable('2025-08-20'), new \DateTimeImmutable('2025-08-15'), new \DateTimeImmutable('2025-09-15'))->format('Y-m-d'), "\n"; +try { + var_dump(clamp(4, NAN, 6)); +} catch (ValueError $error) { + echo $error->getMessage(), "\n"; +} + +try { + var_dump(clamp(7, 6, NAN)); +} catch (ValueError $error) { + echo $error->getMessage(), "\n"; +} + +try { + var_dump(clamp(1, 3, 2)); +} catch (ValueError $error) { + echo $error->getMessage(), "\n"; +} ?> --EXPECT-- int(2) @@ -37,13 +50,12 @@ float(2.5) float(1.3) float(3.141592653589793) double(NAN) -int(4) -int(6) -int(7) -int(6) string(1) "c" string(1) "d" 2025-08-15 2025-08-20 2025-08-15 2025-08-20 +Argument #2 ($min) cannot be NAN +Argument #3 ($max) cannot be NAN +Argument #2 ($min) must be smaller than or equal to argument #3 ($max) From a189b2cb5b71a45a3da6ba949a176d071fe29dd3 Mon Sep 17 00:00:00 2001 From: kylekatarnls Date: Sun, 10 Aug 2025 10:35:20 +0200 Subject: [PATCH 3/4] Fix object support for frameless clamp function --- ext/standard/math.c | 6 +++--- ext/standard/tests/math/clamp.phpt | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ext/standard/math.c b/ext/standard/math.c index 910b68368cdd8..f2ba5083a5fcb 100644 --- a/ext/standard/math.c +++ b/ext/standard/math.c @@ -451,14 +451,14 @@ ZEND_FRAMELESS_FUNCTION(clamp, 3) } if (zend_compare(zmax, zvalue) == -1) { - RETURN_COPY_VALUE(zmax); + RETURN_COPY(zmax); } if (zend_compare(zvalue, zmin) == -1) { - RETURN_COPY_VALUE(zmin); + RETURN_COPY(zmin); } - RETURN_COPY_VALUE(zvalue); + RETURN_COPY(zvalue); } /* }}} */ diff --git a/ext/standard/tests/math/clamp.phpt b/ext/standard/tests/math/clamp.phpt index 2699cbd9188d2..bd8fdf9e09ad2 100644 --- a/ext/standard/tests/math/clamp.phpt +++ b/ext/standard/tests/math/clamp.phpt @@ -49,13 +49,13 @@ float(2.5) float(2.5) float(1.3) float(3.141592653589793) -double(NAN) +float(NAN) string(1) "c" string(1) "d" 2025-08-15 2025-08-20 2025-08-15 2025-08-20 -Argument #2 ($min) cannot be NAN -Argument #3 ($max) cannot be NAN -Argument #2 ($min) must be smaller than or equal to argument #3 ($max) +clamp(): Argument #2 ($min) cannot be NAN +clamp(): Argument #3 ($max) cannot be NAN +clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max) From 37cbb4a6f42d62f8c2765694b3223239281327f9 Mon Sep 17 00:00:00 2001 From: kylekatarnls Date: Sun, 10 Aug 2025 11:21:54 +0200 Subject: [PATCH 4/4] Update stub hash --- ext/standard/basic_functions_arginfo.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index 8b27f4b65c7ba..d568b6341a0ca 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: deb4ea96dd130d8a0174678095c30a61e118bd60 */ + * Stub hash: a5eef8f94b2c661c0fd50202e3bf1e01a282c24f */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, seconds, IS_LONG, 0)