/*
   +----------------------------------------------------------------------+
   | Zend Engine                                                          |
   +----------------------------------------------------------------------+
   | Copyright © Zend Technologies Ltd., a subsidiary company of          |
   |     Perforce Software, Inc., and Contributors.                       |
   +----------------------------------------------------------------------+
   | This source file is subject to the Modified BSD License that is      |
   | bundled with this package in the file LICENSE, and is available      |
   | through the World Wide Web at <https://www.php.net/license/>.        |
   |                                                                      |
   | SPDX-License-Identifier: BSD-3-Clause                                |
   +----------------------------------------------------------------------+
   | Authors: Arnaud Le Blanc <arnaud.lb@gmail.com>                       |
   +----------------------------------------------------------------------+
*/

/**
 * Partial Function Application:
 *
 * A partial application is compiled to the usual sequence of function call
 * opcodes (INIT_FCALL, SEND_VAR, etc), but the sequence ends with a
 * CALLABLE_CONVERT_PARTIAL opcode instead of DO_FCALL, similarly to
 * first class callables. Placeholders are compiled to SEND_PLACEHOLDER opcodes:
 *
 * $f = f($a, ?)
 *
 * 0001 INIT_FCALL f
 * 0002 SEND_VAR CV($a)
 * 0003 SEND_PLACEHOLDER
 * 0004 CV($f) = CALLABLE_CONVERT_PARTIAL
 *
 * SEND_PLACEHOLDER sets the argument slot type to _IS_PLACEHOLDER.
 *
 * CALLABLE_CONVERT_PARTIAL uses the information available on the stack to
 * create a Closure and return it, consuming the stack frame in the process
 * like an internal function call.
 *
 * We create the Closure by generating the relevant AST and compiling it to an
 * op_array. The op_array is cached in the Opcache SHM and inline caches.
 *
 * This file implements the Closure generation logic
 * (see zend_partial_create(), zp_compile()).
 */

#include "zend.h"
#include "zend_API.h"
#include "zend_arena.h"
#include "zend_ast.h"
#include "zend_compile.h"
#include "zend_closures.h"
#include "zend_attributes.h"
#include "zend_exceptions.h"
#include "zend_partial.h"
#include "ext/opcache/ZendAccelerator.h"

static zend_always_inline bool Z_IS_PLACEHOLDER_P(const zval *p) {
	return Z_TYPE_P(p) == _IS_PLACEHOLDER;
}

static zend_always_inline bool zp_is_static_closure(const zend_function *function) {
	return ((function->common.fn_flags & (ZEND_ACC_STATIC|ZEND_ACC_CLOSURE)) == (ZEND_ACC_STATIC|ZEND_ACC_CLOSURE));
}

static zend_always_inline bool zp_is_non_static_closure(const zend_function *function) {
	return ((function->common.fn_flags & (ZEND_ACC_STATIC|ZEND_ACC_CLOSURE)) == ZEND_ACC_CLOSURE);
}

/* Whether argument at offset 'offset' is const. Such arguments can be burned into the generated op_array */
static inline bool zp_is_const_arg(uint32_t const_args, uint32_t offset) {
	if (offset < sizeof(const_args) * CHAR_BIT) {
		return const_args & (UINT32_C(1) << offset);
	}
	return false;
}

static zend_never_inline ZEND_COLD void zp_args_underflow(
		const zend_function *function, uint32_t args, uint32_t expected)
{
	zend_string *symbol = get_function_or_method_name(function);
	const char *limit = function->common.num_args <= function->common.required_num_args ?
			"exactly" : "at least";

	zend_argument_count_error(
		"Partial application of %s() expects %s %d arguments, %d given",
		ZSTR_VAL(symbol), limit, expected, args);

	zend_string_release(symbol);
}

static zend_never_inline ZEND_COLD void zp_args_overflow(
		const zend_function *function, uint32_t args, uint32_t expected)
{
	zend_string *symbol = get_function_or_method_name(function);

	zend_argument_count_error(
		"Partial application of %s() expects at most %d arguments, %d given",
		ZSTR_VAL(symbol), expected, args);

	zend_string_release(symbol);
}

static zend_result zp_args_check(const zend_function *function,
		uint32_t argc, const zval *argv,
		const zend_array *extra_named_args,
		bool uses_variadic_placeholder) {

	if (extra_named_args) {
		ZEND_HASH_MAP_FOREACH_STR_KEY_VAL(extra_named_args, zend_string *key, zval *arg) {
			if (UNEXPECTED(Z_IS_PLACEHOLDER_P(arg))) {
				zend_throw_error(NULL,
						"Cannot use named placeholder for unknown or variadic parameter $%s",
						ZSTR_VAL(key));
				return FAILURE;
			}
		} ZEND_HASH_FOREACH_END();
	}

	if (argc < function->common.required_num_args) {
		if (uses_variadic_placeholder) {
			/* Missing args will be turned into placeholders */
			return SUCCESS;
		}

		zp_args_underflow(function, argc, function->common.required_num_args);
		return FAILURE;
	} else if (argc > function->common.num_args
			&& !(function->common.fn_flags & ZEND_ACC_VARIADIC)) {
		zp_args_overflow(function, argc, function->common.num_args);
		return FAILURE;
	}

	return SUCCESS;
}

typedef struct zp_names {
	zend_string *variadic_param;
	zend_string *extra_named_params;
	zend_string *inner_closure;
	zend_string *params[1]; /* argc elements */
} zp_names;

static bool zp_name_exists(const zp_names *names, uint32_t argc, const zend_string *name)
{
	for (uint32_t i = 0; i < argc; i++) {
		if (names->params[i] && zend_string_equals(names->params[i], name)) {
			return true;
		}
	}
	if (names->variadic_param && zend_string_equals(names->variadic_param, name)) {
		return true;
	}
	if (names->extra_named_params && zend_string_equals(names->extra_named_params, name)) {
		return true;
	}
	if (names->inner_closure && zend_string_equals(names->inner_closure, name)) {
		return true;
	}

	return false;
}

static void zp_names_dtor(zp_names *names, uint32_t argc)
{
	for (uint32_t i = 0; i < argc; i++) {
		if (names->params[i]) {
			zend_string_release(names->params[i]);
		}
	}
	if (names->variadic_param) {
		zend_string_release(names->variadic_param);
	}
	if (names->extra_named_params) {
		zend_string_release(names->extra_named_params);
	}
	if (names->inner_closure) {
		zend_string_release(names->inner_closure);
	}
}

static zend_string *zp_get_func_param_name(const zend_function *function, uint32_t arg_offset)
{
	return zend_string_copy(function->common.arg_info[arg_offset].name);
}

/* Assign a name for every variable that will be used in the generated closure,
 * including params and used vars. */
static zp_names *zp_assign_names(uint32_t argc, zval *argv,
		zend_function *function, bool variadic_partial,
		zend_array *extra_named_params, uint32_t const_args)
{
	zp_names *names = zend_arena_calloc(&CG(ast_arena),
			1, zend_safe_address_guarded(argc, sizeof(*names->params), offsetof(zp_names, params)));

	/* Assign names for params. We never rename those. */
	for (uint32_t offset = 0, num_args = MIN(argc, function->common.num_args);
			offset < num_args; offset++) {
		if (Z_IS_PLACEHOLDER_P(&argv[offset])) {
			names->params[offset] = zp_get_func_param_name(function, offset);
		}
	}

	/* Assign name for the variadic param. Never renamed. */
	if (variadic_partial && (function->common.fn_flags & ZEND_ACC_VARIADIC)) {
		names->variadic_param = zp_get_func_param_name(function, function->common.num_args);
	}

	/* Assign names for placeholders that bind to the variadic param:
	 *
	 * function f($a, ...$args) {}
	 * f(?, ?, ...); // The second placeholder binds into the variadic param.
	 *
	 * By default these are named $origNameN with N the offset from the
	 * variadic param. In case of clash we increment N until a free name is
	 * found. */
	for (uint32_t offset = function->common.num_args; offset < argc; offset++) {
		ZEND_ASSERT(function->common.fn_flags & ZEND_ACC_VARIADIC);
		if (!Z_IS_PLACEHOLDER_P(&argv[offset])) {
			continue;
		}
		zend_string *orig_name = zp_get_func_param_name(function, function->common.num_args);
		zend_string *new_name;
		for (uint32_t n = 0;; n++) {
			new_name = zend_strpprintf_unchecked(0, "%S%" PRIu32, orig_name, n);
			if (!zp_name_exists(names, argc, new_name)) {
				break;
			}
			zend_string_release(new_name);
		}
		names->params[offset] = new_name;
		zend_string_release(orig_name);
	}

	/* Assign names for pre-bound params (lexical vars).
	 * There may be clashes, we ensure to generate unique names. */
	for (uint32_t offset = 0; offset < argc; offset++) {
		if (Z_IS_PLACEHOLDER_P(&argv[offset]) || Z_ISUNDEF(argv[offset]) || zp_is_const_arg(const_args, offset)) {
			continue;
		}
		uint32_t n = 2;
		zend_string *orig_name = zp_get_func_param_name(function, MIN(offset, function->common.num_args));
		zend_string *new_name = zend_string_copy(orig_name);
		while (zp_name_exists(names, argc, new_name)) {
			zend_string_release(new_name);
			new_name = zend_strpprintf_unchecked(0, "%S%" PRIu32, orig_name, n);
			n++;
		}
		names->params[offset] = new_name;
		zend_string_release(orig_name);
	}

	/* Assign name for $extra_named_params */
	if (extra_named_params) {
		uint32_t n = 2;
		zend_string *new_name = ZSTR_INIT_LITERAL("extra_named_params", 0);
		while (zp_name_exists(names, argc, new_name)) {
			zend_string_release(new_name);
			new_name = zend_strpprintf(0, "%s%" PRIu32, "extra_named_params", n);
			n++;
		}
		names->extra_named_params = new_name;
	}

	/* Assign name for $fn */
	if (function->common.fn_flags & ZEND_ACC_CLOSURE) {
		uint32_t n = 2;
		zend_string *new_name = ZSTR_INIT_LITERAL("fn", 0);
		while (zp_name_exists(names, argc, new_name)) {
			zend_string_release(new_name);
			new_name = zend_strpprintf(0, "%s%" PRIu32, "fn", n);
			n++;
		}
		names->inner_closure = new_name;
	}

	return names;
}

ZEND_ATTRIBUTE_CONST static inline bool zp_is_power_of_two(uint32_t x)
{
	return (x > 0) && !(x & (x - 1));
}

ZEND_ATTRIBUTE_CONST static bool zp_is_simple_type(uint32_t type_mask)
{
	ZEND_ASSERT(!(type_mask & ~_ZEND_TYPE_MAY_BE_MASK));

	return zp_is_power_of_two(type_mask)
		|| type_mask == MAY_BE_BOOL
		|| type_mask == MAY_BE_ANY;
}

static zend_ast *zp_simple_type_to_ast(uint32_t type)
{
	zend_string *name;

	switch (type) {
		case MAY_BE_NULL:
			name = ZSTR_KNOWN(ZEND_STR_NULL_LOWERCASE);
			break;
		case MAY_BE_TRUE:
			name = ZSTR_KNOWN(ZEND_STR_TRUE);
			break;
		case MAY_BE_FALSE:
			name = ZSTR_KNOWN(ZEND_STR_FALSE);
			break;
		case MAY_BE_LONG:
			name = ZSTR_KNOWN(ZEND_STR_INT);
			break;
		case MAY_BE_DOUBLE:
			name = ZSTR_KNOWN(ZEND_STR_FLOAT);
			break;
		case MAY_BE_STRING:
			name = ZSTR_KNOWN(ZEND_STR_STRING);
			break;
		case MAY_BE_BOOL:
			name = ZSTR_KNOWN(ZEND_STR_BOOL);
			break;
		case MAY_BE_VOID:
			name = ZSTR_KNOWN(ZEND_STR_VOID);
			break;
		case MAY_BE_NEVER:
			name = ZSTR_KNOWN(ZEND_STR_NEVER);
			break;
		case MAY_BE_OBJECT:
			name = ZSTR_KNOWN(ZEND_STR_OBJECT);
			break;
		case MAY_BE_ANY:
			name = ZSTR_KNOWN(ZEND_STR_MIXED);
			break;
		case MAY_BE_CALLABLE:
			return zend_ast_create_ex(ZEND_AST_TYPE, IS_CALLABLE);
		case MAY_BE_ARRAY:
			return zend_ast_create_ex(ZEND_AST_TYPE, IS_ARRAY);
		case MAY_BE_STATIC:
			return zend_ast_create_ex(ZEND_AST_TYPE, IS_STATIC);
		default:
			ZEND_UNREACHABLE();
	}

	zend_ast *ast = zend_ast_create_zval_from_str(name);
	ast->attr = ZEND_NAME_NOT_FQ;

	return ast;
}

static zend_ast *zp_type_name_to_ast(zend_string *name)
{
	zend_ast *ast = zend_ast_create_zval_from_str(name);

	if (zend_get_class_fetch_type(name) != ZEND_FETCH_CLASS_DEFAULT) {
		ast->attr = ZEND_NAME_NOT_FQ;
	} else {
		ast->attr = ZEND_NAME_FQ;
	}

	return ast;
}

static zend_ast *zp_type_to_ast(const zend_type type)
{
	if (!ZEND_TYPE_IS_SET(type)) {
		return NULL;
	}

	if (ZEND_TYPE_IS_UNION(type)
			|| (ZEND_TYPE_IS_COMPLEX(type) && ZEND_TYPE_PURE_MASK(type))
			|| (ZEND_TYPE_PURE_MASK(type) && !zp_is_simple_type(ZEND_TYPE_PURE_MASK(type)))) {
		/* This is a union type */

		zend_ast *type_ast = zend_ast_create_list(0, ZEND_AST_TYPE_UNION);

		/* Add complex types if any */
		if (ZEND_TYPE_HAS_LIST(type)) {
			ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(type), const zend_type *type_ptr) {
				type_ast = zend_ast_list_add(type_ast, zp_type_to_ast(*type_ptr));
			} ZEND_TYPE_LIST_FOREACH_END();
		} else if (ZEND_TYPE_HAS_NAME(type)) {
			zend_ast *name_ast = zp_type_name_to_ast(
					zend_string_copy(ZEND_TYPE_NAME(type)));
			type_ast = zend_ast_list_add(type_ast, name_ast);
		} else {
			ZEND_ASSERT(!ZEND_TYPE_IS_COMPLEX(type));
		}

		/* Add simple types if any */
		uint32_t type_mask = ZEND_TYPE_PURE_MASK(type);

		/* IS_TRUE|IS_FALSE is represented as a single bool node */
		if ((type_mask & MAY_BE_BOOL) == MAY_BE_BOOL) {
			type_ast = zend_ast_list_add(type_ast, zp_simple_type_to_ast(MAY_BE_BOOL));
			type_mask &= ~MAY_BE_BOOL;
		}
		for (uint32_t may_be_type = 1; may_be_type < _ZEND_TYPE_MAY_BE_MASK; may_be_type <<= 1) {
			if (type_mask & may_be_type) {
				type_ast = zend_ast_list_add(type_ast, zp_simple_type_to_ast(may_be_type));
			}
		}

		return type_ast;
	}

	if (ZEND_TYPE_IS_INTERSECTION(type)) {
		ZEND_ASSERT(!ZEND_TYPE_PURE_MASK(type));
		zend_ast *type_ast = zend_ast_create_list(0, ZEND_AST_TYPE_INTERSECTION);
		ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(type), const zend_type *type_ptr) {
			type_ast = zend_ast_list_add(type_ast, zp_type_to_ast(*type_ptr));
		} ZEND_TYPE_LIST_FOREACH_END();
		return type_ast;
	}

	if (ZEND_TYPE_HAS_NAME(type)) {
		ZEND_ASSERT(!ZEND_TYPE_PURE_MASK(type));
		zend_ast *type_ast = zp_type_name_to_ast(
				zend_string_copy(ZEND_TYPE_NAME(type)));
		return type_ast;
	}

	ZEND_ASSERT(!ZEND_TYPE_IS_COMPLEX(type));

	uint32_t type_mask = ZEND_TYPE_PURE_MASK(type);
	ZEND_ASSERT(zp_is_simple_type(type_mask));

	return zp_simple_type_to_ast(type_mask);
}

static zend_result zp_get_param_default_value(zval *result, zend_function *function, uint32_t arg_offset)
{
	ZEND_ASSERT(arg_offset < function->common.num_args);

	if (function->type == ZEND_USER_FUNCTION) {
		zend_op *opline = &function->op_array.opcodes[arg_offset];
		if (EXPECTED(opline->opcode == ZEND_RECV_INIT)) {
			ZVAL_COPY(result, RT_CONSTANT(opline, opline->op2));
			return SUCCESS;
		}
		ZEND_ASSERT(opline->opcode == ZEND_RECV);
	} else {
		if (function->common.fn_flags & ZEND_ACC_USER_ARG_INFO) {
			goto error;
		}

		const zend_arg_info *arg_info = &function->internal_function.arg_info[arg_offset];

		if (zend_get_default_from_internal_arg_info(result, arg_info) == SUCCESS) {
			return SUCCESS;
		}
	}

error:
	zend_argument_error_ex(zend_ce_argument_count_error, function, arg_offset + 1,
			"must be passed explicitly, because the default value is not known");

	return FAILURE;
}

static bool zp_arg_must_be_sent_by_ref(const zend_function *function, uint32_t arg_num)
{
	if (EXPECTED(arg_num <= MAX_ARG_FLAG_NUM)) {
		if (QUICK_ARG_MUST_BE_SENT_BY_REF(function, arg_num)) {
			return true;
		}
	} else if (ARG_MUST_BE_SENT_BY_REF(function, arg_num)) {
		return true;
	}
	return false;
}

static zend_ast *zp_attribute_to_ast(zend_attribute *attribute)
{
	zend_ast *args_ast;
	if (attribute->argc) {
		args_ast = zend_ast_create_arg_list(0, ZEND_AST_ARG_LIST);
		for (uint32_t i = 0; i < attribute->argc; i++) {
			zend_ast *arg_ast = zend_ast_create_zval(&attribute->args[i].value);
			if (attribute->args[i].name) {
				arg_ast = zend_ast_create(ZEND_AST_NAMED_ARG,
						zend_ast_create_zval_from_str(
							zend_string_copy(attribute->args[i].name)),
						arg_ast);
			}
			args_ast = zend_ast_list_add(args_ast, arg_ast);
		}
	} else {
		args_ast = NULL;
	}
	return zend_ast_create(ZEND_AST_ATTRIBUTE,
			zend_ast_create_zval_from_str(zend_string_copy(attribute->name)),
			args_ast);
}

static zend_ast *zp_param_attributes_to_ast(zend_function *function,
		uint32_t offset)
{
	zend_ast *attributes_ast = NULL;
	if (!function->common.attributes) {
		return NULL;
	}

	/* Inherit the SensitiveParameter attribute */
	zend_attribute *attr = zend_get_parameter_attribute_str(
			function->common.attributes,
			"sensitiveparameter", strlen("sensitiveparameter"), offset);
	if (attr) {
		attributes_ast = zend_ast_create_list(1, ZEND_AST_ATTRIBUTE_GROUP,
				zp_attribute_to_ast(attr));
		attributes_ast = zend_ast_create_list(1, ZEND_AST_ATTRIBUTE_LIST,
				attributes_ast);
	}

	return attributes_ast;
}

/* Generate the AST for calling the actual function */
static zend_ast *zp_compile_forwarding_call(
	zval *this_ptr, zend_function *function,
	uint32_t argc, zval *argv, zend_array *extra_named_params,
	zp_names *var_names, bool uses_variadic_placeholder, uint32_t num_args,
	zend_class_entry *called_scope, zend_type return_type,
	bool forward_superfluous_args,
	zend_ast *stmts_ast, uint32_t const_args)
{
	bool is_assert = zend_string_equals(function->common.function_name,
			ZSTR_KNOWN(ZEND_STR_ASSERT));

	zend_ast *args_ast = zend_ast_create_list(0, ZEND_AST_ARG_LIST);
	zend_ast *call_ast = NULL;

	if (is_assert) {
		/* We are going to call assert() dynamically (via call_user_func),
		 * otherwise assert() would print the generated AST on failure, which is
		 * irrelevant. */
		args_ast = zend_ast_list_add(args_ast,
				zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_ASSERT)));
	}

	/* Generate positional arguments */
	for (uint32_t offset = 0; offset < argc; offset++) {
		if (Z_ISUNDEF(argv[offset])) {
			/* Argument was not passed. Pass its default value. */
			if (offset < function->common.required_num_args) {
				/* Required param was not passed. This can happen due to named
				 * args. Using the same exception CE and message as
				 * zend_handle_undef_args(). */
				zend_argument_error_ex(zend_ce_argument_count_error, function, offset + 1, "not passed");
				goto error;
			}
			zval default_value;
			if (zp_get_param_default_value(&default_value, function, offset) == FAILURE) {
				ZEND_ASSERT(EG(exception));
				goto error;
			}
			zend_ast *default_value_ast;
			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);
			}
			args_ast = zend_ast_list_add(args_ast, default_value_ast);
		} else if (zp_is_const_arg(const_args, offset)) {
			ZEND_ASSERT(Z_TYPE(argv[offset]) < IS_OBJECT);
			args_ast = zend_ast_list_add(args_ast, zend_ast_create_zval(&argv[offset]));
		} else {
			args_ast = zend_ast_list_add(args_ast, zend_ast_create(ZEND_AST_VAR,
						zend_ast_create_zval_from_str(zend_string_copy(var_names->params[offset]))));
		}
	}
	/* Use unpacking to pass extra named params */
	if (extra_named_params) {
		args_ast = zend_ast_list_add(args_ast, zend_ast_create(ZEND_AST_UNPACK,
					zend_ast_create(ZEND_AST_VAR,
						zend_ast_create_zval_from_str(zend_string_copy(var_names->extra_named_params)))));
	}
	if (uses_variadic_placeholder) {
		if (function->common.fn_flags & ZEND_ACC_VARIADIC) {
			/* Pass variadic params */
			args_ast = zend_ast_list_add(args_ast, zend_ast_create(ZEND_AST_UNPACK,
						zend_ast_create(ZEND_AST_VAR,
							zend_ast_create_zval_from_str(zend_string_copy(var_names->variadic_param)))));
		} else if (forward_superfluous_args) {
			/* When a '...' placeholder is used, and the underlying function is
			 * not variadic, superfluous arguments are forwarded.
			 * Add a ...array_slice(func_get_args(), n) argument, which should
			 * be compiled as ZEND_AST_UNPACK + ZEND_FUNC_GET_ARGS. */

			zend_ast *func_get_args_name_ast = zend_ast_create_zval_from_str(
					zend_string_copy(ZSTR_KNOWN(ZEND_STR_FUNC_GET_ARGS)));
			func_get_args_name_ast->attr = ZEND_NAME_FQ;

			zend_ast *array_slice_name_ast = zend_ast_create_zval_from_str(
					zend_string_copy(ZSTR_KNOWN(ZEND_STR_ARRAY_SLICE)));
			array_slice_name_ast->attr = ZEND_NAME_FQ;

			args_ast = zend_ast_list_add(args_ast,
				zend_ast_create(ZEND_AST_UNPACK,
					zend_ast_create(ZEND_AST_CALL,
							array_slice_name_ast,
							zend_ast_create_list(2, ZEND_AST_ARG_LIST,
								zend_ast_create(ZEND_AST_CALL,
									func_get_args_name_ast,
									zend_ast_create_list(0, ZEND_AST_ARG_LIST)),
								zend_ast_create_zval_from_long(num_args)))));
		}
	}

	if (is_assert) {
		zend_ast *func_name_ast = zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_CALL_USER_FUNC));
		func_name_ast->attr = ZEND_NAME_FQ;
		call_ast = zend_ast_create(ZEND_AST_CALL, func_name_ast, args_ast);
	} else if (function->common.fn_flags & ZEND_ACC_CLOSURE) {
		zend_ast *fn_ast = zend_ast_create(ZEND_AST_VAR,
					zend_ast_create_zval_from_str(zend_string_copy(var_names->inner_closure)));
		call_ast = zend_ast_create(ZEND_AST_CALL, fn_ast, args_ast);
	} else if (Z_TYPE_P(this_ptr) == IS_OBJECT) {
		zend_ast *this_ast = zend_ast_create(ZEND_AST_VAR,
					zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_THIS)));
		zend_ast *method_name_ast = zend_ast_create_zval_from_str(
				zend_string_copy(function->common.function_name));
		call_ast = zend_ast_create(ZEND_AST_METHOD_CALL, this_ast,
				method_name_ast, args_ast);
	} else if (called_scope) {
		zend_ast *class_name_ast = zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_STATIC));
		class_name_ast->attr = ZEND_NAME_NOT_FQ;
		zend_ast *method_name_ast = zend_ast_create_zval_from_str(
				zend_string_copy(function->common.function_name));
		call_ast = zend_ast_create(ZEND_AST_STATIC_CALL, class_name_ast,
				method_name_ast, args_ast);
	} else {
		zend_ast *func_name_ast = zend_ast_create_zval_from_str(zend_string_copy(function->common.function_name));
		func_name_ast->attr = ZEND_NAME_FQ;
		call_ast = zend_ast_create(ZEND_AST_CALL, func_name_ast, args_ast);
	}

	/* Void functions can not 'return $expr' */
	if (ZEND_TYPE_FULL_MASK(return_type) & MAY_BE_VOID) {
		stmts_ast = zend_ast_list_add(stmts_ast, call_ast);
	} else {
		zend_ast *return_ast = zend_ast_create(ZEND_AST_RETURN, call_ast);
		stmts_ast = zend_ast_list_add(stmts_ast, return_ast);
	}

	return stmts_ast;

error:
	zend_ast_destroy(args_ast);
	zend_ast_destroy(call_ast);
	return NULL;
}

static uint32_t zp_compute_num_required(const zend_function *function,
		uint32_t orig_offset, uint32_t new_offset, uint32_t num_required) {
	if (orig_offset < function->common.num_args) {
		if (orig_offset < function->common.required_num_args) {
			num_required = MAX(num_required, new_offset + 1);
		}
	} else {
		ZEND_ASSERT(function->common.fn_flags & ZEND_ACC_VARIADIC);
		/* Placeholders that run into the variadic portion become
		 * required and make all params before them required */
		ZEND_ASSERT(orig_offset >= num_required);
		num_required = new_offset + 1;
	}

	return num_required;
}

/* Compile PFA to an op_array */
static zend_op_array *zp_compile(zval *this_ptr, zend_function *function,
		uint32_t argc, zval *argv, zend_array *extra_named_params,
		const zend_array *named_positions,
		zend_string *declaring_filename,
		const uint32_t *declaring_lineno_ptr, void **cache_slot,
		zend_string *pfa_name, uint32_t flags, uint32_t const_args) {

	zend_op_array *op_array = NULL;

	if (UNEXPECTED(function->common.fn_flags2 & ZEND_ACC2_FORBID_DYN_CALLS)) {
		const char *format = "Cannot call %S() dynamically";
		zend_throw_error(NULL, format, function->common.function_name);
		return NULL;
	}

	bool uses_variadic_placeholder = flags & ZEND_PARTIAL_USES_VARIADIC_PLACEHOLDER;

	if (UNEXPECTED(zp_args_check(function, argc, argv, extra_named_params, uses_variadic_placeholder) != SUCCESS)) {
		ZEND_ASSERT(EG(exception));
		return NULL;
	}

	zend_class_entry *called_scope;
	if (Z_TYPE_P(this_ptr) == IS_OBJECT) {
		called_scope = Z_OBJCE_P(this_ptr);
	} else {
		called_scope = Z_CE_P(this_ptr);
	}

	/* CG(ast_arena) is usually NULL, so we can't just make a snapshot */
	zend_arena *orig_ast_arena = CG(ast_arena);
	CG(ast_arena) = zend_arena_create(1024 * 4);

	uint32_t orig_lineno = CG(zend_lineno);
	CG(zend_lineno) = zend_get_executed_lineno();

	uint32_t new_argc = argc;

	if (uses_variadic_placeholder) {
		/* A variadic placeholder generates an implicit positional placeholder
		 * for any unspecified arg, so make room for those. */
		new_argc = MAX(new_argc, function->common.num_args);
	}

	zval *tmp = zend_arena_alloc(&CG(ast_arena), zend_safe_address_guarded(new_argc, sizeof(*tmp), 0));
	memcpy(tmp, argv, zend_safe_address_guarded(argc, sizeof(*tmp), 0));
	argv = tmp;

	/* Compute param positions and number of required args, add implicit
	 * placeholders.
	 *
	 * Parameters are placed in the following order:
	 * - Positional placeholders
	 * - Then named placeholders in their syntax order
	 * - Then implicit placeholders added by '...'
	 */
	uint32_t num_params = 0;
	uint32_t num_required = 0;
	uint32_t *arg_to_param_offset_map = zend_arena_alloc(&CG(ast_arena),
			zend_safe_address_guarded(new_argc, sizeof(*arg_to_param_offset_map), 0));
	{
		uint32_t num_positional = 0;

		/* First, we handle explicit placeholders */
		for (uint32_t arg_offset = 0; arg_offset < argc; arg_offset++) {
			if (!Z_IS_PLACEHOLDER_P(&argv[arg_offset])) {
				continue;
			}

			num_params++;

			zend_arg_info *arg_info = &function->common.arg_info[MIN(arg_offset, function->common.num_args)];
			zval *named_pos = named_positions ? zend_hash_find(named_positions, arg_info->name) : NULL;
			uint32_t param_offset;
			if (named_pos) {
				/* Placeholder is sent as named arg. 'num_positional' is fixed
				 * at this point. */
				param_offset = num_positional + Z_LVAL_P(named_pos);
			} else {
				/* Placeholder is sent as positional */
				param_offset = num_positional++;
			}

			arg_to_param_offset_map[arg_offset] = param_offset;
		}

		num_required = num_params;

		/* Handle implicit placeholders added by '...' */
		if (uses_variadic_placeholder) {
			for (uint32_t arg_offset = 0; arg_offset < new_argc; arg_offset++) {
				if (arg_offset < argc && !Z_ISUNDEF(argv[arg_offset])) {
					continue;
				}

				/* Unspecified parameters become placeholders */
				Z_TYPE_INFO(argv[arg_offset]) = _IS_PLACEHOLDER;

				num_params++;

				uint32_t param_offset = num_params - 1;

				arg_to_param_offset_map[arg_offset] = param_offset;

				num_required = zp_compute_num_required(function,
						arg_offset, param_offset, num_required);
			}
		}
	}

	argc = new_argc;

	/* Assign variable names */

	zp_names *var_names = zp_assign_names(argc, argv, function,
			uses_variadic_placeholder, extra_named_params, const_args);

	/* Generate AST */

	zend_ast *lexical_vars_ast = zend_ast_create_list(0, ZEND_AST_CLOSURE_USES);
	zend_ast *params_ast = zend_ast_create_list(0, ZEND_AST_ARG_LIST);
	zend_ast *return_type_ast = NULL;
	zend_ast *stmts_ast = zend_ast_create_list(0, ZEND_AST_STMT_LIST);
	zend_ast *attributes_ast = NULL;

	/* Generate AST for params and lexical vars */
	{
		/* The inner Closure, if any, is assumed to be the first lexical var by
		 * do_closure_bind(). */
		if (function->common.fn_flags & ZEND_ACC_CLOSURE) {
			zend_ast *lexical_var_ast = zend_ast_create_zval_from_str(
					zend_string_copy(var_names->inner_closure));
			lexical_vars_ast = zend_ast_list_add(lexical_vars_ast, lexical_var_ast);
		}

		zend_ast **params = zend_arena_calloc(&CG(ast_arena), num_params, sizeof(*params));
		for (uint32_t offset = 0; offset < argc; offset++) {
			if (Z_IS_PLACEHOLDER_P(&argv[offset])) {
				zend_arg_info *arg_info = &function->common.arg_info[MIN(offset, function->common.num_args)];

				int param_flags = 0;
				if (zp_arg_must_be_sent_by_ref(function, offset+1)) {
					param_flags |= ZEND_PARAM_REF;
				}

				uint32_t param_offset = arg_to_param_offset_map[offset];
				zend_ast *param_type_ast = zp_type_to_ast(arg_info->type);
				zend_ast *default_value_ast = NULL;
				if (param_offset >= num_required) {
					zval default_value;
					if (zp_get_param_default_value(&default_value, function, offset) == FAILURE) {
						for (uint32_t i = 0; i < num_params; i++) {
							zend_ast_destroy(params[i]);
						}
						goto error;
					}
					default_value_ast = zend_ast_create_zval(&default_value);
				}

				ZEND_ASSERT(offset < function->common.num_args || (function->common.fn_flags & ZEND_ACC_VARIADIC));

				zend_ast *attributes_ast = zp_param_attributes_to_ast(function, MIN(offset, function->common.num_args));
				params[param_offset] = zend_ast_create_ex(ZEND_AST_PARAM,
						param_flags, param_type_ast,
						zend_ast_create_zval_from_str(
							zend_string_copy(var_names->params[offset])),
						default_value_ast, attributes_ast, NULL, NULL);

			} else if (!Z_ISUNDEF(argv[offset])) {
				if (zp_is_const_arg(const_args, offset)) {
					/* Will be burned into the op_array */
				} else {
					zend_ast *lexical_var_ast = zend_ast_create_zval_from_str(
							zend_string_copy(var_names->params[offset]));
					if (zp_arg_must_be_sent_by_ref(function, offset+1)) {
						lexical_var_ast->attr = ZEND_BIND_REF;
					}
					lexical_vars_ast = zend_ast_list_add(
							lexical_vars_ast, lexical_var_ast);
				}
			}
		}

		for (uint32_t i = 0; i < num_params; i++) {
			params_ast = zend_ast_list_add(params_ast, params[i]);
		}
	}

	if (extra_named_params) {
		zend_ast *lexical_var_ast = zend_ast_create_zval_from_str(
				zend_string_copy(var_names->extra_named_params));
		lexical_vars_ast = zend_ast_list_add(lexical_vars_ast, lexical_var_ast);
	}

	/* If we have a variadic placeholder and the underlying function is
	 * variadic, add a variadic param. */
	if (uses_variadic_placeholder
			&& (function->common.fn_flags & ZEND_ACC_VARIADIC)) {
		zend_arg_info *arg_info = &function->common.arg_info[function->common.num_args];
		int param_flags = ZEND_PARAM_VARIADIC;
		if (zp_arg_must_be_sent_by_ref(function, function->common.num_args+1)) {
			param_flags |= ZEND_PARAM_REF;
		}
		zend_ast *param_type_ast = zp_type_to_ast(arg_info->type);
		zend_ast *attributes_ast = zp_param_attributes_to_ast(function, function->common.num_args);
		params_ast = zend_ast_list_add(params_ast, zend_ast_create_ex(ZEND_AST_PARAM,
				param_flags, param_type_ast,
				zend_ast_create_zval_from_str(
					zend_string_copy(var_names->variadic_param)),
				NULL, attributes_ast, NULL, NULL));
	}

	zend_type return_type = {0};
	if (function->common.fn_flags & ZEND_ACC_HAS_RETURN_TYPE) {
		return_type = (function->common.arg_info-1)->type;
		return_type_ast = zp_type_to_ast(return_type);
	}

	/**
	 * Generate function body.
	 *
	 * If we may need to forward superflous arguments, do that conditionally, as
	 * it's faster:
	 *
	 * if (func_num_args() <= n) {
	 *    // normal call
	 * } else {
	 *    // call with superflous arg forwarding
	 * }
	 *
	 * The func_num_args() call should be compiled to a single FUNC_NUM_ARGS op.
	 */

	if (uses_variadic_placeholder && !(function->common.fn_flags & ZEND_ACC_VARIADIC)) {
		zend_ast *no_forwarding_ast = zend_ast_create_list(0, ZEND_AST_STMT_LIST);
		zend_ast *forwarding_ast = zend_ast_create_list(0, ZEND_AST_STMT_LIST);

		no_forwarding_ast = zp_compile_forwarding_call(this_ptr, function,
				argc, argv, extra_named_params,
				var_names, uses_variadic_placeholder, num_params,
				called_scope, return_type, false, no_forwarding_ast, const_args);

		if (!no_forwarding_ast) {
			ZEND_ASSERT(EG(exception));
			goto error;
		}

		forwarding_ast = zp_compile_forwarding_call(this_ptr, function,
				argc, argv, extra_named_params,
				var_names, uses_variadic_placeholder, num_params,
				called_scope, return_type, true, forwarding_ast, const_args);

		if (!forwarding_ast) {
			ZEND_ASSERT(EG(exception));
			zend_ast_destroy(no_forwarding_ast);
			goto error;
		}

		zend_ast *func_num_args_name_ast = zend_ast_create_zval_from_str(
				zend_string_copy(ZSTR_KNOWN(ZEND_STR_FUNC_NUM_ARGS)));
		func_num_args_name_ast->attr = ZEND_NAME_FQ;

		stmts_ast = zend_ast_list_add(stmts_ast,
			zend_ast_create_list(2, ZEND_AST_IF,
				zend_ast_create(ZEND_AST_IF_ELEM,
					zend_ast_create_binary_op(ZEND_IS_SMALLER_OR_EQUAL,
						zend_ast_create(ZEND_AST_CALL, func_num_args_name_ast,
							zend_ast_create_list(0, ZEND_AST_ARG_LIST)),
						zend_ast_create_zval_from_long(num_params)),
					no_forwarding_ast),
				zend_ast_create(ZEND_AST_IF_ELEM,
					NULL,
					forwarding_ast)));
	} else {
		stmts_ast = zp_compile_forwarding_call(this_ptr, function,
				argc, argv, extra_named_params,
				var_names, uses_variadic_placeholder, num_params,
				called_scope, return_type, false, stmts_ast, const_args);

		if (!stmts_ast) {
			ZEND_ASSERT(EG(exception));
			goto error;
		}
	}

	/* Inherit the NoDiscard attribute */
	if (function->common.attributes) {
		zend_attribute *attr = zend_get_attribute_str(
				function->common.attributes, "nodiscard", strlen("nodiscard"));
		if (attr) {
			attributes_ast = zend_ast_create_list(1, ZEND_AST_ATTRIBUTE_GROUP,
					zp_attribute_to_ast(attr));
			attributes_ast = zend_ast_create_list(1, ZEND_AST_ATTRIBUTE_LIST,
					attributes_ast);
		}
	}

	int closure_flags = function->common.fn_flags & ZEND_ACC_RETURN_REFERENCE;
	zend_ast *closure_ast = zend_ast_create_decl(ZEND_AST_CLOSURE,
			closure_flags, CG(zend_lineno), NULL,
			NULL, params_ast, lexical_vars_ast, stmts_ast,
			return_type_ast, attributes_ast);

	if (Z_TYPE_P(this_ptr) != IS_OBJECT && !zp_is_non_static_closure(function)) {
		((zend_ast_decl*)closure_ast)->flags |= ZEND_ACC_STATIC;
	}

#if ZEND_DEBUG
	{
		const char *tmp = getenv("DUMP_PFA_AST");
		if (tmp && ZEND_ATOL(tmp)) {
			zend_string *str = zend_ast_export("", closure_ast, "");
			fprintf(stderr, "PFA AST: %s\n", ZSTR_VAL(str));
			zend_string_release(str);
		}
	}
#endif

	op_array = zend_accel_compile_pfa(closure_ast, declaring_filename,
			declaring_lineno_ptr, function, pfa_name, flags & ZEND_PARTIAL_CACHEABLE_IN_SHM);

	zend_ast_destroy(closure_ast);

clean:
	zp_names_dtor(var_names, argc);
	zend_arena_destroy(CG(ast_arena));
	CG(ast_arena) = orig_ast_arena;
	CG(zend_lineno) = orig_lineno;

	return op_array;

error:
	zend_ast_destroy(lexical_vars_ast);
	zend_ast_destroy(params_ast);
	zend_ast_destroy(return_type_ast);
	zend_ast_destroy(stmts_ast);
	zend_ast_destroy(attributes_ast);
	goto clean;
}

/* Get the op_array of a PFA from caches or compile it */
static const zend_op_array *zp_get_op_array(zval *this_ptr, zend_function *function,
		uint32_t argc, zval *argv, zend_array *extra_named_params,
		const zend_array *named_positions,
		zend_string *declaring_filename,
		const uint32_t *declaring_lineno_ptr, void **cache_slot,
		zend_string *pfa_name, uint32_t flags, uint32_t const_args) {

	if (EXPECTED(function->type == ZEND_INTERNAL_FUNCTION
					? cache_slot[0] == function
					: cache_slot[0] == function->op_array.opcodes)) {
		ZEND_ASSERT(!(function->common.fn_flags & ZEND_ACC_NEVER_CACHE));
		return cache_slot[1];
	}

	const zend_op_array *op_array = zend_accel_pfa_cache_get(declaring_lineno_ptr, function,
			flags & ZEND_PARTIAL_CACHEABLE_IN_SHM);

	if (UNEXPECTED(!op_array)) {
		op_array = zp_compile(this_ptr, function, argc, argv,
			extra_named_params, named_positions, declaring_filename, declaring_lineno_ptr,
			cache_slot, pfa_name, flags, const_args);
	}

	if (EXPECTED(op_array) && !(function->common.fn_flags & ZEND_ACC_NEVER_CACHE)) {
		cache_slot[0] = function->type == ZEND_INTERNAL_FUNCTION
			? (void*)function
			: (void*)function->op_array.opcodes;
		cache_slot[1] = (zend_op_array*)op_array;
	}

	return op_array;
}

static void zp_free_unbound_args(uint32_t start, uint32_t argc, zval *argv)
{
	for (uint32_t offset = start; offset < argc; offset++) {
		zval_ptr_dtor_nogc(&argv[offset]);
	}
}

/* Bind pre-bound arguments as lexical vars */
static void zp_bind(zval *result, zend_function *function, uint32_t argc, zval *argv,
		zend_array *extra_named_params, uint32_t const_args) {

	zend_arg_info *arg_infos = function->common.arg_info;
	uint32_t bind_offset = 0;

	if (function->common.fn_flags & ZEND_ACC_CLOSURE) {
		zval var;
		ZVAL_OBJ(&var, ZEND_CLOSURE_OBJECT(function));
		Z_ADDREF(var);
		zend_closure_bind_var_ex(result, bind_offset, &var);
		bind_offset += sizeof(Bucket);
	}

	for (uint32_t offset = 0; offset < argc; offset++) {
		zval *var = &argv[offset];
		if (Z_IS_PLACEHOLDER_P(var) || Z_ISUNDEF_P(var) || zp_is_const_arg(const_args, offset)) {
			continue;
		}
		zend_arg_info *arg_info;
		if (offset < function->common.num_args) {
			arg_info = &arg_infos[offset];
		} else if (function->common.fn_flags & ZEND_ACC_VARIADIC) {
			arg_info = &arg_infos[function->common.num_args];
		} else {
			arg_info = NULL;
		}
		if (arg_info && ZEND_TYPE_IS_SET(arg_info->type)
				&& UNEXPECTED(!zend_check_type_ex(&arg_info->type, var,
					/* current_frame */ true, /* is_internal */ false))) {
			zend_string *need_msg = zend_type_to_string_resolved(arg_info->type,
					function->common.scope);
			zend_argument_type_error_ex(function, offset + 1,
					"must be of type %s, %s given",
					ZSTR_VAL(need_msg), zend_zval_value_name(var));
			zend_string_release(need_msg);
			zval_ptr_dtor(result);
			ZVAL_NULL(result);
			zp_free_unbound_args(offset, argc, argv);
			return;
		}
		ZEND_ASSERT(zp_arg_must_be_sent_by_ref(function, offset+1) ? Z_ISREF_P(var) : !Z_ISREF_P(var));
		zend_closure_bind_var_ex(result, bind_offset, var);
		bind_offset += sizeof(Bucket);
	}

	if (extra_named_params) {
		zval var;
		ZVAL_ARR(&var, extra_named_params);
		Z_ADDREF(var);
		zend_closure_bind_var_ex(result, bind_offset, &var);
	}
}

void zend_partial_create(zval *result, zval *this_ptr, zend_function *function,
		uint32_t argc, zval *argv, zend_array *extra_named_params,
		const zend_array *named_positions,
		zend_string *declaring_filename,
		const uint32_t *declaring_lineno_ptr, void **cache_slot,
		zend_string *pfa_name, uint32_t flags, uint32_t const_args) {

	ZEND_ASSERT(pfa_name);

	const zend_op_array *op_array = zp_get_op_array(this_ptr, function, argc, argv,
			extra_named_params, named_positions,
			declaring_filename, declaring_lineno_ptr,
			cache_slot, pfa_name, flags, const_args);

	if (UNEXPECTED(!op_array)) {
		ZEND_ASSERT(EG(exception));
		zp_free_unbound_args(0, argc, argv);
		ZVAL_NULL(result);
		return;
	}

	zend_class_entry *called_scope;
	zval object;

	if (Z_TYPE_P(this_ptr) == IS_OBJECT) {
		called_scope = Z_OBJCE_P(this_ptr);
	} else {
		called_scope = Z_CE_P(this_ptr);
	}

	if (Z_TYPE_P(this_ptr) == IS_OBJECT && !zp_is_static_closure(function)) {
		ZVAL_COPY_VALUE(&object, this_ptr);
	} else {
		ZVAL_UNDEF(&object);
	}

	zend_create_partial_closure(result, (zend_function*)op_array,
			function->common.scope, called_scope, &object,
			(function->common.fn_flags & ZEND_ACC_CLOSURE) != 0);

	zp_bind(result, function, argc, argv, extra_named_params, const_args);
}

void zend_partial_op_array_dtor(zval *pDest)
{
	destroy_op_array(Z_PTR_P(pDest));
}
