Commit 61a987b5a19 for php.net

commit 61a987b5a1927dd3fe39b4744bba20de7ca73d33
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Mon Jul 20 10:58:59 2026 -0400

    Fix crash when a partial skips a param with a constant default (#22804)

    Const exprs represent a constant fetch that could not be evaluated at
    compile time as ZEND_AST_CONSTANT, which zend_compile_expr_inner() had
    no case for, so compiling such a default into a partial's forwarding
    call hit ZEND_ASSERT(0). Compile it to ZEND_FETCH_CONSTANT using the
    name already resolved by zend_compile_const_expr_const().

    zp_compile_forwarding_call() also dropped the reference it took on a
    constant-expression default value.

    Closes GH-22804

diff --git a/Zend/tests/partial_application/default_const_expr.phpt b/Zend/tests/partial_application/default_const_expr.phpt
new file mode 100644
index 00000000000..12d25adedef
--- /dev/null
+++ b/Zend/tests/partial_application/default_const_expr.phpt
@@ -0,0 +1,116 @@
+--TEST--
+PFA: constant-expression default for a skipped optional parameter
+--FILE--
+<?php
+
+namespace App;
+
+const K = 7;
+
+function f($a, $b = K, $c = 0) {
+    return [$a, $b, $c];
+}
+
+function variadic($a, $b = K, $c = 0, ...$args) {
+    return [$a, $b, $c, $args];
+}
+
+function late($a, $b = LATE, $c = 0) {
+    return [$a, $b, $c];
+}
+
+function fallback($a, $b = GLOBAL_ONLY, $c = 0) {
+    return [$a, $b, $c];
+}
+
+class C {
+    const X = 42;
+    static function m($a, $b = self::X, $c = 0) {
+        return [$a, $b, $c];
+    }
+}
+
+enum E {
+    case A;
+    case B;
+}
+
+function g($a, $b = E::A, $c = 0) {
+    return [$a, $b->name, $c];
+}
+
+$p = f(a: 10, c: ?);
+var_dump($p(99));
+
+$v = variadic(a: 10, c: ?, foo: 1);
+var_dump($v(99));
+
+$q = C::m(a: 1, c: ?);
+var_dump($q(9));
+
+$r = g(a: 1, c: ?);
+var_dump($r(9));
+
+$l = late(a: 1, c: ?);
+define('App\LATE', 'defined after the partial was created');
+var_dump($l(9));
+
+define('GLOBAL_ONLY', 'global fallback');
+$s = fallback(a: 1, c: ?);
+var_dump($s(9));
+
+?>
+--EXPECT--
+array(3) {
+  [0]=>
+  int(10)
+  [1]=>
+  int(7)
+  [2]=>
+  int(99)
+}
+array(4) {
+  [0]=>
+  int(10)
+  [1]=>
+  int(7)
+  [2]=>
+  int(99)
+  [3]=>
+  array(1) {
+    ["foo"]=>
+    int(1)
+  }
+}
+array(3) {
+  [0]=>
+  int(1)
+  [1]=>
+  int(42)
+  [2]=>
+  int(9)
+}
+array(3) {
+  [0]=>
+  int(1)
+  [1]=>
+  string(1) "A"
+  [2]=>
+  int(9)
+}
+array(3) {
+  [0]=>
+  int(1)
+  [1]=>
+  string(37) "defined after the partial was created"
+  [2]=>
+  int(9)
+}
+array(3) {
+  [0]=>
+  int(1)
+  [1]=>
+  string(15) "global fallback"
+  [2]=>
+  int(9)
+}
diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index f4478423953..bb24d73fb24 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -11519,12 +11519,25 @@ static void zend_compile_array(znode *result, zend_ast *ast) /* {{{ */
 }
 /* }}} */

+static void zend_emit_fetch_constant(znode *result, zend_string *resolved_name, bool unqualified_in_namespace)
+{
+	zend_op *opline = zend_emit_op_tmp(result, ZEND_FETCH_CONSTANT, NULL, NULL);
+	opline->op2_type = IS_CONST;
+
+	if (unqualified_in_namespace) {
+		opline->op1.num = IS_CONSTANT_UNQUALIFIED_IN_NAMESPACE;
+		opline->op2.constant = zend_add_const_name_literal(resolved_name, true);
+	} else {
+		opline->op1.num = 0;
+		opline->op2.constant = zend_add_const_name_literal(resolved_name, false);
+	}
+	opline->extended_value = zend_alloc_cache_slot();
+}
+
 static void zend_compile_const(znode *result, const zend_ast *ast) /* {{{ */
 {
 	zend_ast *name_ast = ast->child[0];

-	zend_op *opline;
-
 	bool is_fully_qualified;
 	zend_string *orig_name = zend_ast_get_str(name_ast);
 	zend_string *resolved_name = zend_resolve_const_name(orig_name, name_ast->attr, &is_fully_qualified);
@@ -11553,22 +11566,19 @@ static void zend_compile_const(znode *result, const zend_ast *ast) /* {{{ */
 		return;
 	}

-	opline = zend_emit_op_tmp(result, ZEND_FETCH_CONSTANT, NULL, NULL);
-	opline->op2_type = IS_CONST;
-
-	if (is_fully_qualified || !FC(current_namespace)) {
-		opline->op1.num = 0;
-		opline->op2.constant = zend_add_const_name_literal(
-			resolved_name, false);
-	} else {
-		opline->op1.num = IS_CONSTANT_UNQUALIFIED_IN_NAMESPACE;
-		opline->op2.constant = zend_add_const_name_literal(
-			resolved_name, true);
-	}
-	opline->extended_value = zend_alloc_cache_slot();
+	zend_emit_fetch_constant(result, resolved_name,
+		!is_fully_qualified && FC(current_namespace));
 }
 /* }}} */

+static void zend_compile_constant(znode *result, zend_ast *ast)
+{
+	zend_string *name = zend_ast_get_constant_name(ast);
+
+	zend_emit_fetch_constant(result, zend_string_copy(name),
+		(ast->attr & IS_CONSTANT_UNQUALIFIED_IN_NAMESPACE) != 0);
+}
+
 static void zend_compile_class_const(znode *result, zend_ast *ast) /* {{{ */
 {
 	zend_ast *class_ast;
@@ -12436,6 +12446,9 @@ static void zend_compile_expr_inner(znode *result, zend_ast *ast) /* {{{ */
 		case ZEND_AST_CONST:
 			zend_compile_const(result, ast);
 			return;
+		case ZEND_AST_CONSTANT:
+			zend_compile_constant(result, ast);
+			return;
 		case ZEND_AST_CLASS_CONST:
 			zend_compile_class_const(result, ast);
 			return;
diff --git a/Zend/zend_partial.c b/Zend/zend_partial.c
index ef335cd805c..e7a3e1c92b5 100644
--- a/Zend/zend_partial.c
+++ b/Zend/zend_partial.c
@@ -588,6 +588,7 @@ static zend_ast *zp_compile_forwarding_call(
 			if (Z_TYPE(default_value) == IS_CONSTANT_AST) {
 				/* Must dup AST because we are going to destroy it */
 				default_value_ast = zend_ast_dup(Z_ASTVAL(default_value));
+				zval_ptr_dtor_nogc(&default_value);
 			} else {
 				default_value_ast = zend_ast_create_zval(&default_value);
 			}