From 9a646466c0cac8de0f27a4d48ddaf8db221d006f Mon Sep 17 00:00:00 2001
From: Ilija Tovilo <ilija.tovilo@me.com>
Date: Thu, 17 Oct 2024 00:32:21 +0200
Subject: [PATCH 1/2] Implement frameless static calls

---
 Zend/Optimizer/zend_dump.c                    |  5 +++-
 Zend/zend_compile.c                           | 24 +++++++++++++++++++
 build/gen_stub.php                            | 18 ++++++++++----
 ext/zend_test/test.c                          | 24 +++++++++++++++++++
 ext/zend_test/test.stub.php                   |  3 +++
 ext/zend_test/test_arginfo.h                  | 22 ++++++++++++++++-
 .../tests/frameless_static_methods.phpt       | 12 ++++++++++
 7 files changed, 101 insertions(+), 7 deletions(-)
 create mode 100644 ext/zend_test/tests/frameless_static_methods.phpt

diff --git a/Zend/Optimizer/zend_dump.c b/Zend/Optimizer/zend_dump.c
index 4e46b38a8eb5e..b171eaebd1890 100644
--- a/Zend/Optimizer/zend_dump.c
+++ b/Zend/Optimizer/zend_dump.c
@@ -479,7 +479,10 @@ ZEND_API void zend_dump_op(const zend_op_array *op_array, const zend_basic_block
 
 	if (ZEND_OP_IS_FRAMELESS_ICALL(opline->opcode)) {
 		zend_function *func = ZEND_FLF_FUNC(opline);
-		fprintf(stderr, "(%s)", ZSTR_VAL(func->common.function_name));
+		fprintf(stderr, "(%s%s%s)",
+			func->common.scope ? ZSTR_VAL(func->common.scope->name) : "",
+			func->common.scope ? "::" : "",
+			ZSTR_VAL(func->common.function_name));
 	}
 
 	if (ZEND_VM_EXT_NUM == (flags & ZEND_VM_EXT_MASK)) {
diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index e33990c73bb67..1a402fd43c846 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -59,6 +59,13 @@
 
 #define FC(member) (CG(file_context).member)
 
+#define ZEND_OP1_LITERAL(opline)		(op_array)->literals[(opline)->op1.constant]
+#define ZEND_OP2_LITERAL(opline)		(op_array)->literals[(opline)->op2.constant]
+#define literal_dtor(zv) do { \
+		zval_ptr_dtor_nogc(zv); \
+		ZVAL_NULL(zv); \
+	} while (0)
+
 typedef struct _zend_loop_var {
 	uint8_t opcode;
 	uint8_t var_type;
@@ -5336,6 +5343,7 @@ static void zend_compile_static_call(znode *result, zend_ast *ast, uint32_t type
 		}
 	}
 
+	uint32_t init_opnum = get_next_op_number();
 	opline = get_next_op();
 	opline->opcode = ZEND_INIT_STATIC_METHOD_CALL;
 
@@ -5378,6 +5386,22 @@ static void zend_compile_static_call(znode *result, zend_ast *ast, uint32_t type
 		}
 	}
 
+	if (!(CG(compiler_options) & ZEND_COMPILE_NO_BUILTINS)
+	 && fbc
+	 && (fbc->type == ZEND_INTERNAL_FUNCTION)
+	 && zend_ast_is_list(args_ast)
+	 && !zend_args_contain_unpack_or_named(zend_ast_get_list(args_ast))) {
+		if (zend_compile_frameless_icall(result, zend_ast_get_list(args_ast), fbc, type) != (uint32_t)-1) {
+			/* Update opline in case it got invalidated. */
+			zend_op_array *op_array = CG(active_op_array);
+			opline = &op_array->opcodes[init_opnum];
+			literal_dtor(&ZEND_OP1_LITERAL(opline));
+			literal_dtor(&ZEND_OP2_LITERAL(opline));
+			MAKE_NOP(opline);
+			return;
+		}
+	}
+
 	zend_compile_call_common(result, args_ast, fbc, zend_ast_get_lineno(method_ast));
 }
 /* }}} */
diff --git a/build/gen_stub.php b/build/gen_stub.php
index f8e16064ee292..d4eff6a16ae57 100755
--- a/build/gen_stub.php
+++ b/build/gen_stub.php
@@ -1060,6 +1060,10 @@ public function getDeclarationClassName(): string {
         return implode('_', $this->className->getParts());
     }
 
+    public function getDeclarationName(): string {
+        return "{$this->getDeclarationClassName()}_{$this->methodName}";
+    }
+
     public function getDeclaration(): string {
         return "ZEND_METHOD({$this->getDeclarationClassName()}, $this->methodName);\n";
     }
@@ -1068,6 +1072,10 @@ public function getArgInfoName(): string {
         return "arginfo_class_{$this->getDeclarationClassName()}_{$this->methodName}";
     }
 
+    public function getFramelessFunctionInfosName(): string {
+        return "frameless_function_infos_{$this->className}_{$this->methodName}";
+    }
+
     public function getMethodSynopsisFilename(): string
     {
         $parts = [...$this->className->getParts(), ltrim($this->methodName, '_')];
@@ -1331,12 +1339,12 @@ public function getFramelessDeclaration(): ?string {
         }
 
         foreach ($this->framelessFunctionInfos as $framelessFunctionInfo) {
-            $code .= "ZEND_FRAMELESS_FUNCTION({$this->name->getFunctionName()}, {$framelessFunctionInfo->arity});\n";
+            $code .= "ZEND_FRAMELESS_FUNCTION({$this->name->getDeclarationName()}, {$framelessFunctionInfo->arity});\n";
         }
 
         $code .= 'static const zend_frameless_function_info ' . $this->getFramelessFunctionInfosName() . "[] = {\n";
         foreach ($this->framelessFunctionInfos as $framelessFunctionInfo) {
-            $code .= "\t{ ZEND_FRAMELESS_FUNCTION_NAME({$this->name->getFunctionName()}, {$framelessFunctionInfo->arity}), {$framelessFunctionInfo->arity} },\n";
+            $code .= "\t{ ZEND_FRAMELESS_FUNCTION_NAME({$this->name->getDeclarationName()}, {$framelessFunctionInfo->arity}), {$framelessFunctionInfo->arity} },\n";
         }
         $code .= "\t{ 0 },\n";
         $code .= "};\n";
@@ -1362,10 +1370,10 @@ public function getFunctionEntry(): string {
         $functionEntryCode = null;
 
         if (!empty($this->framelessFunctionInfos)) {
-            if ($this->isMethod()) {
-                throw new Exception('Frameless methods are not supported yet');
+            if ($this->isMethod() && !($this->flags & Modifiers::STATIC)) {
+                throw new Exception('Frameless methods must be static');
             }
-            if ($this->name->getNamespace()) {
+            if (!$this->isMethod() && $this->name->getNamespace()) {
                 throw new Exception('Namespaced direct calls to frameless functions are not supported yet');
             }
             if ($this->alias) {
diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c
index e3f87ee1e1636..55d86896468e8 100644
--- a/ext/zend_test/test.c
+++ b/ext/zend_test/test.c
@@ -1169,6 +1169,30 @@ static ZEND_METHOD(_ZendTestClass, takesUnionType)
 	RETURN_NULL();
 }
 
+static ZEND_METHOD(_ZendTestClass, framelessStaticMethod)
+{
+	zend_long lhs, rhs;
+
+	ZEND_PARSE_PARAMETERS_START(2, 2)
+		Z_PARAM_LONG(lhs)
+		Z_PARAM_LONG(rhs)
+	ZEND_PARSE_PARAMETERS_END();
+
+	RETURN_LONG(lhs + rhs);
+}
+
+ZEND_FRAMELESS_FUNCTION(_ZendTestClass_framelessStaticMethod, 2)
+{
+	zend_long lhs, rhs;
+
+	Z_FLF_PARAM_LONG(1, lhs);
+	Z_FLF_PARAM_LONG(2, rhs);
+
+	ZVAL_LONG(return_value, lhs + rhs);
+
+flf_clean:;
+}
+
 // Returns a newly allocated DNF type `Iterator|(Traversable&Countable)`.
 //
 // We need to generate it "manually" because gen_stubs.php does not support codegen for DNF types ATM.
diff --git a/ext/zend_test/test.stub.php b/ext/zend_test/test.stub.php
index 59cb9661e4e43..bddffaa1accc5 100644
--- a/ext/zend_test/test.stub.php
+++ b/ext/zend_test/test.stub.php
@@ -69,6 +69,9 @@ public function returnsThrowable(): Throwable {}
         static public function variadicTest(string|Iterator ...$elements) : static {}
 
         public function takesUnionType(stdclass|Iterator $arg): void {}
+
+        /** @frameless-function {"arity": 2} */
+        public static function framelessStaticMethod(int $lhs, int $rhs): int {}
     }
 
     class _ZendTestMagicCall
diff --git a/ext/zend_test/test_arginfo.h b/ext/zend_test/test_arginfo.h
index c558b58f65169..94e602a155810 100644
--- a/ext/zend_test/test_arginfo.h
+++ b/ext/zend_test/test_arginfo.h
@@ -1,5 +1,5 @@
 /* This is a generated file, edit the .stub.php file instead.
- * Stub hash: 3082e62e96d5f4383c98638513463c676a7c3a69 */
+ * Stub hash: 5ea0e1aae76e9f10b6e5c17d4d55f51e353ebbf2 */
 
 ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_test_array_return, 0, 0, IS_ARRAY, 0)
 ZEND_END_ARG_INFO()
@@ -203,6 +203,11 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class__ZendTestClass_takesUnionT
 	ZEND_ARG_OBJ_TYPE_MASK(0, arg, stdclass|Iterator, 0, NULL)
 ZEND_END_ARG_INFO()
 
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class__ZendTestClass_framelessStaticMethod, 0, 2, IS_LONG, 0)
+	ZEND_ARG_TYPE_INFO(0, lhs, IS_LONG, 0)
+	ZEND_ARG_TYPE_INFO(0, rhs, IS_LONG, 0)
+ZEND_END_ARG_INFO()
+
 ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class__ZendTestMagicCall___call, 0, 2, IS_MIXED, 0)
 	ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0)
 	ZEND_ARG_TYPE_INFO(0, args, IS_ARRAY, 0)
@@ -254,6 +259,15 @@ ZEND_END_ARG_INFO()
 
 #define arginfo_class_ZendTestNS2_ZendSubNS_Foo_method arginfo_zend_test_void_return
 
+
+#if (PHP_VERSION_ID >= 80400)
+ZEND_FRAMELESS_FUNCTION(_ZendTestClass_framelessStaticMethod, 2);
+static const zend_frameless_function_info frameless_function_infos__ZendTestClass_framelessStaticMethod[] = {
+	{ ZEND_FRAMELESS_FUNCTION_NAME(_ZendTestClass_framelessStaticMethod, 2), 2 },
+	{ 0 },
+};
+#endif
+
 static ZEND_FUNCTION(zend_test_array_return);
 static ZEND_FUNCTION(zend_test_nullable_array_return);
 static ZEND_FUNCTION(zend_test_void_return);
@@ -313,6 +327,7 @@ static ZEND_METHOD(_ZendTestClass, returnsStatic);
 static ZEND_METHOD(_ZendTestClass, returnsThrowable);
 static ZEND_METHOD(_ZendTestClass, variadicTest);
 static ZEND_METHOD(_ZendTestClass, takesUnionType);
+static ZEND_METHOD(_ZendTestClass, framelessStaticMethod);
 static ZEND_METHOD(_ZendTestMagicCall, __call);
 static ZEND_METHOD(_ZendTestMagicCallForward, __call);
 static ZEND_METHOD(_ZendTestChildClass, returnsThrowable);
@@ -458,6 +473,11 @@ static const zend_function_entry class__ZendTestClass_methods[] = {
 	ZEND_ME(_ZendTestClass, returnsThrowable, arginfo_class__ZendTestClass_returnsThrowable, ZEND_ACC_PUBLIC)
 	ZEND_ME(_ZendTestClass, variadicTest, arginfo_class__ZendTestClass_variadicTest, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
 	ZEND_ME(_ZendTestClass, takesUnionType, arginfo_class__ZendTestClass_takesUnionType, ZEND_ACC_PUBLIC)
+#if (PHP_VERSION_ID >= 80400)
+	ZEND_RAW_FENTRY("framelessStaticMethod", zim__ZendTestClass_framelessStaticMethod, arginfo_class__ZendTestClass_framelessStaticMethod, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC, frameless_function_infos__ZendTestClass_framelessStaticMethod, NULL)
+#else
+	ZEND_RAW_FENTRY("framelessStaticMethod", zim__ZendTestClass_framelessStaticMethod, arginfo_class__ZendTestClass_framelessStaticMethod, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
+#endif
 	ZEND_FE_END
 };
 
diff --git a/ext/zend_test/tests/frameless_static_methods.phpt b/ext/zend_test/tests/frameless_static_methods.phpt
new file mode 100644
index 0000000000000..39a1bbbf68fd5
--- /dev/null
+++ b/ext/zend_test/tests/frameless_static_methods.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Frameless static methods
+--EXTENSIONS--
+zend_test
+--FILE--
+<?php
+
+var_dump(_ZendTestClass::framelessStaticMethod(42, 69));
+
+?>
+--EXPECT--
+int(111)

From ba9092a84a5858c3895fc445adee5c222aa45816 Mon Sep 17 00:00:00 2001
From: Ilija Tovilo <ilija.tovilo@me.com>
Date: Wed, 27 Nov 2024 16:00:56 +0100
Subject: [PATCH 2/2] Compile static frameless calls before emitting opcodes

Co-authored-by: Dmitry Stogov <dmitry@zend.com>
---
 Zend/zend_compile.c | 80 ++++++++++++++++++++-------------------------
 1 file changed, 36 insertions(+), 44 deletions(-)

diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index 1a402fd43c846..7eadef4644c41 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -59,13 +59,6 @@
 
 #define FC(member) (CG(file_context).member)
 
-#define ZEND_OP1_LITERAL(opline)		(op_array)->literals[(opline)->op1.constant]
-#define ZEND_OP2_LITERAL(opline)		(op_array)->literals[(opline)->op2.constant]
-#define literal_dtor(zv) do { \
-		zval_ptr_dtor_nogc(zv); \
-		ZVAL_NULL(zv); \
-	} while (0)
-
 typedef struct _zend_loop_var {
 	uint8_t opcode;
 	uint8_t var_type;
@@ -5343,29 +5336,11 @@ static void zend_compile_static_call(znode *result, zend_ast *ast, uint32_t type
 		}
 	}
 
-	uint32_t init_opnum = get_next_op_number();
-	opline = get_next_op();
-	opline->opcode = ZEND_INIT_STATIC_METHOD_CALL;
-
-	zend_set_class_name_op1(opline, &class_node);
-
-	if (method_node.op_type == IS_CONST) {
-		opline->op2_type = IS_CONST;
-		opline->op2.constant = zend_add_func_name_literal(
-			Z_STR(method_node.u.constant));
-		opline->result.num = zend_alloc_cache_slots(2);
-	} else {
-		if (opline->op1_type == IS_CONST) {
-			opline->result.num = zend_alloc_cache_slot();
-		}
-		SET_NODE(opline->op2, &method_node);
-	}
-
 	/* Check if we already know which method we're calling */
-	if (opline->op2_type == IS_CONST) {
+	if (method_node.op_type == IS_CONST) {
 		zend_class_entry *ce = NULL;
-		if (opline->op1_type == IS_CONST) {
-			zend_string *lcname = Z_STR_P(CT_CONSTANT(opline->op1) + 1);
+		if (class_node.op_type == IS_CONST) {
+			zend_string *lcname = zend_string_tolower(Z_STR(class_node.u.constant));
 			ce = zend_hash_find_ptr(CG(class_table), lcname);
 			if (ce) {
 				if (zend_compile_ignore_class(ce, CG(active_op_array)->filename)) {
@@ -5375,31 +5350,48 @@ static void zend_compile_static_call(znode *result, zend_ast *ast, uint32_t type
 					&& zend_string_equals_ci(CG(active_class_entry)->name, lcname)) {
 				ce = CG(active_class_entry);
 			}
-		} else if (opline->op1_type == IS_UNUSED
-				&& (opline->op1.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF
+			zend_string_release(lcname);
+		} else if (class_node.op_type == IS_UNUSED
+				&& (class_node.u.op.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF
 				&& zend_is_scope_known()) {
 			ce = CG(active_class_entry);
 		}
 		if (ce) {
-			zend_string *lcname = Z_STR_P(CT_CONSTANT(opline->op2) + 1);
+			zend_string *lcname = zend_string_tolower(Z_STR(method_node.u.constant));
 			fbc = zend_get_compatible_func_or_null(ce, lcname);
+			zend_string_release(lcname);
+
+			if (fbc
+			 && !(CG(compiler_options) & ZEND_COMPILE_NO_BUILTINS)
+			 && (fbc->type == ZEND_INTERNAL_FUNCTION)
+			 && zend_ast_is_list(args_ast)
+			 && !zend_args_contain_unpack_or_named(zend_ast_get_list(args_ast))) {
+				if (zend_compile_frameless_icall(result, zend_ast_get_list(args_ast), fbc, type) != (uint32_t)-1) {
+					zval_ptr_dtor(&method_node.u.constant);
+					if (class_node.op_type == IS_CONST) {
+						zval_ptr_dtor(&class_node.u.constant);
+					}
+					return;
+				}
+			}
 		}
 	}
 
-	if (!(CG(compiler_options) & ZEND_COMPILE_NO_BUILTINS)
-	 && fbc
-	 && (fbc->type == ZEND_INTERNAL_FUNCTION)
-	 && zend_ast_is_list(args_ast)
-	 && !zend_args_contain_unpack_or_named(zend_ast_get_list(args_ast))) {
-		if (zend_compile_frameless_icall(result, zend_ast_get_list(args_ast), fbc, type) != (uint32_t)-1) {
-			/* Update opline in case it got invalidated. */
-			zend_op_array *op_array = CG(active_op_array);
-			opline = &op_array->opcodes[init_opnum];
-			literal_dtor(&ZEND_OP1_LITERAL(opline));
-			literal_dtor(&ZEND_OP2_LITERAL(opline));
-			MAKE_NOP(opline);
-			return;
+	opline = get_next_op();
+	opline->opcode = ZEND_INIT_STATIC_METHOD_CALL;
+
+	zend_set_class_name_op1(opline, &class_node);
+
+	if (method_node.op_type == IS_CONST) {
+		opline->op2_type = IS_CONST;
+		opline->op2.constant = zend_add_func_name_literal(
+			Z_STR(method_node.u.constant));
+		opline->result.num = zend_alloc_cache_slots(2);
+	} else {
+		if (opline->op1_type == IS_CONST) {
+			opline->result.num = zend_alloc_cache_slot();
 		}
+		SET_NODE(opline->op2, &method_node);
 	}
 
 	zend_compile_call_common(result, args_ast, fbc, zend_ast_get_lineno(method_ast));