Commit 06ea0e5df1d for php.net

commit 06ea0e5df1d63f1dbd7bf93295f2ff64d1855075
Author: Kyle <kylekatarnls@users.noreply.github.com>
Date:   Thu Dec 18 20:27:30 2025 +0100

    [RFC] Add clamp function (#19434)

    * Implement clamp function

    Co-authored-by: thinkverse <hallberg.kim@gmail.com>

    * - Use a common function for normal and frameless implementations
    - Add tests for null and not-comparable cases
    - Fix object support for frameless clamp function
    - Improve NAN handling

    * Create tests triggering both frameless and dynamic variants

    * Add changelog

    * [Review] rephrase error messages to use "must not"

    * Enable assert()

    ---------

    Co-authored-by: thinkverse <hallberg.kim@gmail.com>

diff --git a/NEWS b/NEWS
index 9fa63137d88..cea609f1d72 100644
--- a/NEWS
+++ b/NEWS
@@ -7,6 +7,7 @@ PHP                                                                        NEWS
     request. (ilutov)
   . It is now possible to use reference assign on WeakMap without the key
     needing to be present beforehand. (ndossche)
+  . Added `clamp()`. (kylekatarnls, thinkverse)

 - Hash:
   . Upgrade xxHash to 0.8.2. (timwolla)
diff --git a/UPGRADING b/UPGRADING
index 4e511d09479..9a2159a33e7 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -71,6 +71,10 @@ PHP 8.6 UPGRADE NOTES
 6. New Functions
 ========================================

+- Standard:
+  . `clamp()` returns the given value if in range, else return the nearest bound.
+    RFC: https://wiki.php.net/rfc/clamp_v2
+
 ========================================
 7. New Classes and Interfaces
 ========================================
diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php
index 7913ca0e001..e27dca069c5 100644
--- a/ext/standard/basic_functions.stub.php
+++ b/ext/standard/basic_functions.stub.php
@@ -1606,6 +1606,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 0a21d7d7642..6f202c01463 100644
Binary files a/ext/standard/basic_functions_arginfo.h and b/ext/standard/basic_functions_arginfo.h differ
diff --git a/ext/standard/math.c b/ext/standard/math.c
index 142d473864f..95384c06588 100644
--- a/ext/standard/math.c
+++ b/ext/standard/math.c
@@ -389,6 +389,62 @@ PHP_FUNCTION(round)
 }
 /* }}} */

+/* Return the given value if in range of min and max */
+static void php_math_clamp(zval *return_value, zval *value, zval *min, zval *max)
+{
+	if (Z_TYPE_P(min) == IS_DOUBLE && UNEXPECTED(zend_isnan(Z_DVAL_P(min)))) {
+		zend_argument_value_error(2, "must not be NAN");
+		RETURN_THROWS();
+	}
+
+	if (Z_TYPE_P(max) == IS_DOUBLE && UNEXPECTED(zend_isnan(Z_DVAL_P(max)))) {
+		zend_argument_value_error(3, "must not be NAN");
+		RETURN_THROWS();
+	}
+
+	if (zend_compare(max, min) == -1) {
+		zend_argument_value_error(2, "must be smaller than or equal to argument #3 ($max)");
+		RETURN_THROWS();
+	}
+
+	if (zend_compare(max, value) == -1) {
+		RETURN_COPY(max);
+	}
+
+	if (zend_compare(value, min) == -1) {
+		RETURN_COPY(min);
+	}
+
+	RETURN_COPY(value);
+}
+
+/* {{{ 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();
+
+	php_math_clamp(return_value, zvalue, zmin, zmax);
+}
+/* }}} */
+
+/* {{{ 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);
+
+	php_math_clamp(return_value, zvalue, zmin, zmax);
+}
+/* }}} */
+
 /* {{{ 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 00000000000..beb4c8d5314
--- /dev/null
+++ b/ext/standard/tests/math/clamp.phpt
@@ -0,0 +1,104 @@
+--TEST--
+clamp() tests
+--INI--
+precision=14
+date.timezone=UTC
+zend.assertions=1
+--FILE--
+<?php
+
+function make_clamp_fcc() {
+    return clamp(...);
+}
+
+function check_clamp_result($value, $min, $max) {
+    $flf = clamp($value, $min, $max);
+    $dyn = make_clamp_fcc()($value, $min, $max);
+    assert($flf === $dyn || (is_nan($flf) && is_nan($dyn)));
+
+    return $flf;
+}
+
+function check_clamp_exception($value, $min, $max) {
+    try {
+        var_dump(clamp($value, $min, $max));
+    } catch (ValueError $error) {
+        echo $error->getMessage(), "\n";
+    }
+
+    try {
+        var_dump(make_clamp_fcc()($value, $min, $max));
+    } catch (ValueError $error) {
+        echo $error->getMessage(), "\n";
+    }
+}
+
+var_dump(check_clamp_result(2, 1, 3));
+var_dump(check_clamp_result(0, 1, 3));
+var_dump(check_clamp_result(6, 1, 3));
+var_dump(check_clamp_result(2, 1.3, 3.4));
+var_dump(check_clamp_result(2.5, 1, 3));
+var_dump(check_clamp_result(2.5, 1.3, 3.4));
+var_dump(check_clamp_result(0, 1.3, 3.4));
+var_dump(check_clamp_result(M_PI, -INF, INF));
+var_dump(check_clamp_result(NAN, 4, 6));
+var_dump(check_clamp_result("a", "c", "g"));
+var_dump(check_clamp_result("d", "c", "g"));
+echo check_clamp_result('2025-08-01', '2025-08-15', '2025-09-15'), "\n";
+echo check_clamp_result('2025-08-20', '2025-08-15', '2025-09-15'), "\n";
+echo check_clamp_result(new \DateTimeImmutable('2025-08-01'), new \DateTimeImmutable('2025-08-15'), new \DateTimeImmutable('2025-09-15'))->format('Y-m-d'), "\n";
+echo check_clamp_result(new \DateTimeImmutable('2025-08-20'), new \DateTimeImmutable('2025-08-15'), new \DateTimeImmutable('2025-09-15'))->format('Y-m-d'), "\n";
+var_dump(check_clamp_result(null, -1, 1));
+var_dump(check_clamp_result(null, 1, 3));
+var_dump(check_clamp_result(null, -3, -1));
+var_dump(check_clamp_result(-9999, null, 10));
+var_dump(check_clamp_result(12, null, 10));
+
+$a = new \InvalidArgumentException('a');
+$b = new \RuntimeException('b');
+$c = new \LogicException('c');
+echo check_clamp_result($a, $b, $c)::class, "\n";
+echo check_clamp_result($b, $a, $c)::class, "\n";
+echo check_clamp_result($c, $a, $b)::class, "\n";
+
+check_clamp_exception(4, NAN, 6);
+check_clamp_exception(7, 6, NAN);
+check_clamp_exception(1, 3, 2);
+check_clamp_exception(-9999, 5, null);
+check_clamp_exception(12, -5, null);
+
+?>
+--EXPECT--
+int(2)
+int(1)
+int(3)
+int(2)
+float(2.5)
+float(2.5)
+float(1.3)
+float(3.141592653589793)
+float(NAN)
+string(1) "c"
+string(1) "d"
+2025-08-15
+2025-08-20
+2025-08-15
+2025-08-20
+int(-1)
+int(1)
+int(-3)
+int(-9999)
+int(10)
+InvalidArgumentException
+RuntimeException
+LogicException
+clamp(): Argument #2 ($min) must not be NAN
+clamp(): Argument #2 ($min) must not be NAN
+clamp(): Argument #3 ($max) must not be NAN
+clamp(): Argument #3 ($max) must not be NAN
+clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)
+clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)
+clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)
+clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)
+clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)
+clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)