/*
  +----------------------------------------------------------------------+
  | parallel                                                             |
  +----------------------------------------------------------------------+
  | Copyright (c) Joe Watkins 2019-2024                                  |
  +----------------------------------------------------------------------+
  | This source file is subject to version 3.01 of the PHP license,      |
  | that is bundled with this package in the file LICENSE, and is        |
  | available through the world-wide-web at the following url:           |
  | http://www.php.net/license/3_01.txt                                  |
  | If you did not receive a copy of the PHP license and are unable to   |
  | obtain it through the world-wide-web, please send a note to          |
  | license@php.net so we can mail you a copy immediately.               |
  +----------------------------------------------------------------------+
  | Author: krakjoe                                                      |
  +----------------------------------------------------------------------+
 */
#ifndef HAVE_PARALLEL_CHECK
#define HAVE_PARALLEL_CHECK

#include "parallel.h"

TSRM_TLS struct {
	HashTable tasks;
	HashTable functions;
	HashTable types;
	HashTable classes;
} php_parallel_check_globals;

#ifndef ZEND_TYPE_IS_COMPLEX
#define ZEND_TYPE_IS_COMPLEX ZEND_TYPE_HAS_CLASS
#endif

#define PCG(e) php_parallel_check_globals.e

typedef struct _php_parallel_check_task_t {
	zend_function *function;
	bool           returns;
} php_parallel_check_task_t;

typedef struct _php_parallel_check_function_t {
	zend_function *function;
	zend_uchar     instruction;
	bool           valid;
} php_parallel_check_function_t;

typedef struct _php_parallel_check_type_t {
	bool valid;
} php_parallel_check_type_t;

typedef enum {
	PHP_PARALLEL_CHECK_CLASS_UNKNOWN,
	PHP_PARALLEL_CHECK_CLASS_VALID,
	PHP_PARALLEL_CHECK_CLASS_INVALID,
	PHP_PARALLEL_CHECK_CLASS_INVALID_PROPERTY
} php_parallel_check_class_result_t;

typedef struct _php_parallel_check_class_t {
	php_parallel_check_class_result_t result;
} php_parallel_check_class_t;

static zend_always_inline const char *php_parallel_check_opcode_name(zend_uchar opcode)
{ /* {{{ */
	switch (opcode) {
	case ZEND_DECLARE_CLASS:
	case ZEND_DECLARE_CLASS_DELAYED:
		return "class";

	case ZEND_DECLARE_ANON_CLASS:
		return "new class";

	case ZEND_YIELD:
	case ZEND_YIELD_FROM:
		return "yield";

	case ZEND_DECLARE_FUNCTION:
		return "function";

	default:
		return "class";
	}
} /* }}} */

static zend_always_inline bool php_parallel_check_type(zend_type type)
{ /* {{{ */
	zend_string *name;
	zend_type   *single;
	zend_class_entry *class;
	php_parallel_check_type_t check, *checked;

	if (ZEND_TYPE_HAS_LIST(type)) {
		ZEND_TYPE_FOREACH(type, single)
		{
			if (ZEND_TYPE_HAS_NAME(*single)) {
				name = ZEND_TYPE_NAME(*single);

				if ((checked = zend_hash_find_ptr(&PCG(types), name))) {
					if (!checked->valid) {
						return 0;
					}
					continue;
				}

				class = zend_lookup_class(ZEND_TYPE_NAME(*single));

				if (!class) {
					return 0;
				}

				memset(&check, 0, sizeof(php_parallel_check_type_t));

				if (class == zend_ce_closure || class == php_parallel_channel_ce ||
				    instanceof_function(class, php_parallel_sync_ce) || !class->create_object) {

					check.valid = 1;
				}

				zend_hash_add_mem(&PCG(types), name, &check, sizeof(php_parallel_check_type_t));

				if (!check.valid) {
					return 0;
				}
			}
		}
		ZEND_TYPE_FOREACH_END();

		return 1;
	}

	name = ZEND_TYPE_NAME(type);

	if ((checked = zend_hash_find_ptr(&PCG(types), name))) {
		return checked->valid;
	}

	class = zend_lookup_class(name);

	if (!class) {
		return 0;
	}

	memset(&check, 0, sizeof(php_parallel_check_type_t));

	if (class == zend_ce_closure || class == php_parallel_channel_ce ||
	    instanceof_function(class, php_parallel_sync_ce) || !class->create_object) {

		check.valid = 1;
	}

	zend_hash_add_mem(&PCG(types), name, &check, sizeof(php_parallel_check_type_t));

	return check.valid;
} /* }}} */

static zend_always_inline bool php_parallel_check_arginfo(const zend_function *function)
{ /* {{{ */
	zend_arg_info *it, *end;
	int            argc = 1;

	if (!function->op_array.arg_info) {
		return 1;
	}

	if (function->common.fn_flags & ZEND_ACC_HAS_RETURN_TYPE) {
		it = function->op_array.arg_info - 1;
		if (ZEND_TYPE_IS_SET(it->type) && ZEND_TYPE_IS_COMPLEX(it->type)) {
			if (!php_parallel_check_type(it->type)) {
				php_parallel_exception_ex(php_parallel_runtime_error_illegal_return_ce, "illegal return (%s) from task",
				                          ZSTR_VAL(ZEND_TYPE_NAME(it->type)));
				return 0;
			}
		}

		if (function->common.fn_flags & ZEND_ACC_RETURN_REFERENCE) {
			php_parallel_exception_ex(php_parallel_runtime_error_illegal_return_ce,
			                          "illegal return (reference) from task");
			return 0;
		}
	}

	it = function->op_array.arg_info;
	end = it + function->op_array.num_args;

	if (function->common.fn_flags & ZEND_ACC_VARIADIC) {
		end++;
	}

	while (it < end) {
		if (ZEND_TYPE_IS_SET(it->type) && ZEND_TYPE_IS_COMPLEX(it->type)) {
			if (!php_parallel_check_type(it->type)) {
				php_parallel_exception_ex(php_parallel_runtime_error_illegal_parameter_ce,
				                          "illegal parameter (%s) accepted by task at argument %d",
				                          ZSTR_VAL(ZEND_TYPE_NAME(it->type)), argc);
				return 0;
			}
		}

		if (ZEND_ARG_SEND_MODE(it)) {
			php_parallel_exception_ex(php_parallel_runtime_error_illegal_parameter_ce,
			                          "illegal parameter (reference) accepted by task at argument %d", argc);
			return 0;
		}
		it++;
		argc++;
	}

	return 1;
} /* }}} */

static zend_always_inline bool php_parallel_check_statics(const zend_function *function, zend_string **errn,
                                                          zval **errz)
{ /* {{{ */
	HashTable   *statics;
	zval        *value, *error;
	zend_string *name;

	if (!function->op_array.static_variables) {
		return 1;
	}

	statics = ZEND_MAP_PTR_GET(function->op_array.static_variables_ptr);
	if (!statics) {
		statics = function->op_array.static_variables;
	}

	ZEND_HASH_FOREACH_STR_KEY_VAL(statics, name, value)
	{
		if (!PARALLEL_ZVAL_CHECK(value, &error)) {
			if (errn) {
				*errn = name;
			}
			if (errz) {
				*errz = error;
			}
			return 0;
		}
	}
	ZEND_HASH_FOREACH_END();

	return 1;
} /* }}} */

static zend_always_inline bool php_parallel_check_argv(zval *args, uint32_t *argc, zval **error)
{ /* {{{ */
	zval *arg;

	if (*argc == 0) {
		*argc = 1;
	}

	ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(args), arg)
	{
		if (!PARALLEL_ZVAL_CHECK(arg, error)) {
			return 0;
		}

		(*argc)++;
	}
	ZEND_HASH_FOREACH_END();

	return 1;
} /* }}} */

static zend_always_inline bool php_parallel_check_use(zend_execute_data *execute_data, const zend_function *function,
                                                      zend_op *bind)
{ /* {{{ */
	zend_op *opline, *end;

	if (EX(func)->type != ZEND_USER_FUNCTION) {
		return 0;
	}

	opline = EX(func)->op_array.opcodes;
	end = opline + EX(func)->op_array.last;

	while (opline < end) {
		if ((opline->opcode == ZEND_BIND_LEXICAL) && (opline->extended_value & ZEND_BIND_REF)) {
			if (zend_string_equals(zend_get_compiled_variable_name((zend_op_array *)function, bind->op1.var),
			                       zend_get_compiled_variable_name((zend_op_array *)EX(func), opline->op2.var))) {
				return 1;
			}
		}
		opline++;
	}

	return 0;
} /* }}} */

bool php_parallel_check_task(php_parallel_runtime_t *runtime, zend_execute_data *execute_data,
                             const zend_function *function, zval *argv, bool *returns)
{ /* {{{ */
	php_parallel_check_task_t check, *checked;
	zend_op                  *it, *end;

	if (function->type != ZEND_USER_FUNCTION) {
		php_parallel_exception_ex(php_parallel_runtime_error_illegal_function_ce, "illegal function type (internal)");
		return 0;
	}

	if (argv && Z_TYPE_P(argv) == IS_ARRAY) {
		uint32_t errat = 0;
		zval    *errarg;

		if (!php_parallel_check_argv(argv, &errat, &errarg)) {
			php_parallel_exception_ex(php_parallel_runtime_error_illegal_parameter_ce,
			                          "illegal parameter (%s) passed to task at argument %d",
			                          Z_TYPE_P(errarg) == IS_OBJECT ? ZSTR_VAL(Z_OBJCE_P(errarg)->name)
			                                                        : zend_get_type_by_const(Z_TYPE_P(errarg)),
			                          errat);
			return 0;
		}
	}

	if ((checked = zend_hash_index_find_ptr(&PCG(tasks), (zend_ulong)function->op_array.opcodes))) {
		*returns = checked->returns;

		if (!*returns) {
			if (EX(opline)->result_type != IS_UNUSED) {
				*returns = 1;
			}
		}

		return 1;
	}

	if (!php_parallel_check_arginfo(function)) {
		return 0;
	}

	{
		zend_string *errn;
		zval        *errv;
		if (!php_parallel_check_statics(function, &errn, &errv)) {
			php_parallel_exception_ex(php_parallel_runtime_error_illegal_variable_ce,
			                          "illegal variable (%s) named %s in static scope of function",
			                          Z_TYPE_P(errv) == IS_OBJECT ? ZSTR_VAL(Z_OBJCE_P(errv)->name)
			                                                      : zend_get_type_by_const(Z_TYPE_P(errv)),
			                          ZSTR_VAL(errn));
			return 0;
		}
	}

	memset(&check, 0, sizeof(php_parallel_check_task_t));

	it = function->op_array.opcodes;
	end = it + function->op_array.last;

	while (it < end) {
		switch (it->opcode) {
		case ZEND_DECLARE_LAMBDA_FUNCTION: {
			zend_string   *key;
			zend_function *dependency, *errf;
			zend_uchar     erro;

			PARALLEL_COPY_OPLINE_TO_FUNCTION(function, it, &key, &dependency);

			if (!php_parallel_check_function(dependency, &errf, &erro)) {
				php_parallel_exception_ex(php_parallel_runtime_error_illegal_instruction_ce,
				                          "illegal instruction (%s) in closure on line %d of task",
				                          php_parallel_check_opcode_name(erro),
				                          errf->op_array.line_start - function->op_array.line_start);
				return 0;
			}

			{
				zend_string *errn;
				zval        *errv;
				if (!php_parallel_check_statics(dependency, &errn, &errv)) {
					php_parallel_exception_ex(php_parallel_runtime_error_illegal_variable_ce,
					                          "illegal variable (%s) named %s in static scope of dependency",
					                          Z_TYPE_P(errv) == IS_OBJECT ? ZSTR_VAL(Z_OBJCE_P(errv)->name)
					                                                      : zend_get_type_by_const(Z_TYPE_P(errv)),
					                          ZSTR_VAL(errn));
					return 0;
				}
			}
		} break;

		case ZEND_DECLARE_FUNCTION:
		case ZEND_DECLARE_CLASS:
		case ZEND_DECLARE_ANON_CLASS:
		case ZEND_DECLARE_CLASS_DELAYED:
		case ZEND_YIELD:
		case ZEND_YIELD_FROM:
			php_parallel_exception_ex(
			    php_parallel_runtime_error_illegal_instruction_ce, "illegal instruction (%s) on line %d of task",
			    php_parallel_check_opcode_name(it->opcode), it->lineno - function->op_array.line_start);
			return 0;

		case ZEND_BIND_STATIC:
			if (php_parallel_check_use(execute_data, function, it)) {
				php_parallel_exception_ex(php_parallel_runtime_error_illegal_instruction_ce,
				                          "illegal instruction (lexical reference) in task");
				return 0;
			}
			break;

		case ZEND_THROW:
		case ZEND_RETURN:
			if (!*returns && it->extended_value != -1) {
				if (EX(opline)->result_type == IS_UNUSED) {
					php_parallel_exception_ex(php_parallel_runtime_error_illegal_return_ce,
					                          "return on line %d of task ignored by caller, "
					                          "caller must retain reference to Future",
					                          it->lineno - function->op_array.line_start);
					return 0;
				}
				*returns = 1;
			}
			break;
		}
		it++;
	}

	check.returns = *returns;

	if (!*returns) {
		if (EX(opline)->result_type != IS_UNUSED) {
			*returns = 1;
		}
	}

	zend_hash_index_add_mem(&PCG(tasks), (zend_ulong)function->op_array.opcodes, &check,
	                        sizeof(php_parallel_check_task_t));

	return 1;
} /* }}} */

bool php_parallel_check_function(const zend_function *function, zend_function **errf, zend_uchar *erro)
{ /* {{{ */
	php_parallel_check_function_t check, *checked;
	zend_op                      *it, *end;

	if ((checked = zend_hash_index_find_ptr(&PCG(functions), (zend_ulong)function->op_array.opcodes))) {
		check = *checked;

		goto _php_parallel_checked_function_return;
	}

	memset(&check, 0, sizeof(php_parallel_check_function_t));

	it = function->op_array.opcodes;
	end = it + function->op_array.last;

	while (it < end) {
		switch (it->opcode) {
		case ZEND_DECLARE_FUNCTION:
		case ZEND_DECLARE_CLASS:
		case ZEND_DECLARE_CLASS_DELAYED:
		case ZEND_DECLARE_ANON_CLASS:
			check.function = (zend_function *)function;
			check.instruction = it->opcode;

			goto _php_parallel_checked_function_add;

		case ZEND_DECLARE_LAMBDA_FUNCTION: {
			zend_string   *key;
			zend_function *dependency;

			PARALLEL_COPY_OPLINE_TO_FUNCTION(function, it, &key, &dependency);

			if (!php_parallel_check_function(dependency, &check.function, &check.instruction)) {
				goto _php_parallel_checked_function_add;
			}
		} break;
		}
		it++;
	}

	check.valid = 1;

_php_parallel_checked_function_add:
	zend_hash_index_add_mem(&PCG(functions), (zend_ulong)function->op_array.opcodes, &check,
	                        sizeof(php_parallel_check_function_t));

_php_parallel_checked_function_return:
	if (errf) {
		*errf = check.function;
	}

	if (erro) {
		*erro = check.instruction;
	}

	return check.valid;
} /* }}} */

static zend_always_inline bool php_parallel_check_closure(zend_closure_t *closure)
{ /* {{{ */
	return php_parallel_check_statics(&closure->func, NULL, NULL) &&
	       php_parallel_check_function(&closure->func, NULL, NULL);
} /* }}} */

static php_parallel_check_class_result_t                    php_parallel_check_class(zend_class_entry *ce);

static zend_always_inline php_parallel_check_class_result_t php_parallel_check_class_inline(zend_class_entry *ce)
{ /* {{{ */
	zend_property_info        *info;
	php_parallel_check_class_t check, *checked = zend_hash_index_find_ptr(&PCG(classes), (zend_ulong)ce);

	if (checked) {
		return checked->result;
	}

	if (!ce) {
		return PHP_PARALLEL_CHECK_CLASS_INVALID;
	}

	memset(&check, 0, sizeof(php_parallel_check_class_t));

	if (ce == php_parallel_channel_ce || ce == zend_ce_closure) {
		check.result = PHP_PARALLEL_CHECK_CLASS_VALID;

		goto _php_parallel_checked_class;
	}

	if (ce->create_object) {
		check.result = PHP_PARALLEL_CHECK_CLASS_INVALID;

		goto _php_parallel_checked_class;
	}

	if (!ce->default_properties_count) {
		check.result = PHP_PARALLEL_CHECK_CLASS_VALID;

		goto _php_parallel_checked_class;
	}

	ZEND_HASH_FOREACH_PTR(&ce->properties_info, info)
	{
		zend_class_entry *next;

		if (!ZEND_TYPE_IS_SET(info->type)) {
			check.result = PHP_PARALLEL_CHECK_CLASS_UNKNOWN;

			goto _php_parallel_checked_class;
		}

		if (!ZEND_TYPE_IS_SET(info->type) || !ZEND_TYPE_IS_COMPLEX(info->type)) {
			continue;
		}

		if (ZEND_TYPE_HAS_LIST(info->type)) {
			zend_type *single;

			ZEND_TYPE_FOREACH(info->type, single)
			{
#ifdef ZEND_TYPE_HAS_CE
				if (ZEND_TYPE_HAS_CE(*single)) {
					next = ZEND_TYPE_CE(*single);
				} else
#endif
				    if (ZEND_TYPE_HAS_NAME(*single)) {
					next = zend_lookup_class(ZEND_TYPE_NAME(*single));
				} else {
					continue;
				}

				if (next == ce) {
					continue;
				}

				memset(&check, 0, sizeof(php_parallel_check_class_t));

				switch (php_parallel_check_class(next)) {
				case PHP_PARALLEL_CHECK_CLASS_INVALID:
				case PHP_PARALLEL_CHECK_CLASS_INVALID_PROPERTY:
					check.result = PHP_PARALLEL_CHECK_CLASS_INVALID_PROPERTY;
					break;

				case PHP_PARALLEL_CHECK_CLASS_UNKNOWN:
					check.result = PHP_PARALLEL_CHECK_CLASS_UNKNOWN;
					break;

				case PHP_PARALLEL_CHECK_CLASS_VALID:
					check.result = PHP_PARALLEL_CHECK_CLASS_VALID;
					break;
				}

				if (check.result != PHP_PARALLEL_CHECK_CLASS_VALID) {
					zend_hash_index_add_mem(&PCG(classes), (zend_ulong)next, &check,
					                        sizeof(php_parallel_check_class_t));

					check.result = PHP_PARALLEL_CHECK_CLASS_INVALID_PROPERTY;

					zend_hash_index_add_mem(&PCG(classes), (zend_ulong)ce, &check, sizeof(php_parallel_check_class_t));

					return check.result;
				}
			}
			ZEND_TYPE_FOREACH_END();

		} else {
#ifdef ZEND_TYPE_HAS_CE
			if (ZEND_TYPE_HAS_CE(info->type)) {
				next = ZEND_TYPE_CE(info->type);
			} else
#endif
			{
				next = zend_lookup_class(ZEND_TYPE_NAME(info->type));
			}

			if (next == ce) {
				continue;
			}

			switch (php_parallel_check_class(next)) {
			case PHP_PARALLEL_CHECK_CLASS_INVALID:
			case PHP_PARALLEL_CHECK_CLASS_INVALID_PROPERTY:
				check.result = PHP_PARALLEL_CHECK_CLASS_INVALID_PROPERTY;
				goto _php_parallel_checked_class;

			case PHP_PARALLEL_CHECK_CLASS_UNKNOWN:
				check.result = PHP_PARALLEL_CHECK_CLASS_UNKNOWN;
				goto _php_parallel_checked_class;

			case PHP_PARALLEL_CHECK_CLASS_VALID:
				/* do nothing */
				break;
			}
		}
	}
	ZEND_HASH_FOREACH_END();

	check.result = PHP_PARALLEL_CHECK_CLASS_VALID;

_php_parallel_checked_class:
	checked = zend_hash_index_add_mem(&PCG(classes), (zend_ulong)ce, &check, sizeof(php_parallel_check_class_t));

	return checked->result;
} /* }}} */

static php_parallel_check_class_result_t php_parallel_check_class(zend_class_entry *ce)
{
	return php_parallel_check_class_inline(ce);
}

static zend_always_inline bool php_parallel_check_object(zend_object *object, zval **error)
{ /* {{{ */
	if (instanceof_function(object->ce, php_parallel_channel_ce) ||
	    instanceof_function(object->ce, php_parallel_sync_ce)) {
		return 1;
	}

	if (object->ce->create_object) {
		return 0;
	}

	if ((object->properties == NULL) || (object->properties->nNumUsed == 0)) {
		switch (php_parallel_check_class_inline(object->ce)) {
		case PHP_PARALLEL_CHECK_CLASS_VALID:
			/* whole graph is typed and allowed */
			return 1;

		case PHP_PARALLEL_CHECK_CLASS_INVALID:
			return 0;

		case PHP_PARALLEL_CHECK_CLASS_INVALID_PROPERTY:
			/* type info for a property in graph is invalid, but may be undef/null */
		case PHP_PARALLEL_CHECK_CLASS_UNKNOWN:
			/* not enough type info */
			break;
		}
	}

	if (object->ce->default_properties_count) {
		zval *property = object->properties_table, *end = property + object->ce->default_properties_count;

		while (property < end) {
			if ((Z_TYPE_P(property) == IS_OBJECT) && (Z_OBJ_P(property) == object)) {
				property++;
				continue;
			}

			if (!php_parallel_check_zval(property, error)) {
				return 0;
			}

			property++;
		}
	}

	if (object->properties) {
		zval *property;

		ZEND_HASH_FOREACH_VAL(object->properties, property)
		{
			if ((Z_TYPE_P(property) == IS_OBJECT) && (Z_OBJ_P(property) == object)) {
				continue;
			}

			if (!php_parallel_check_zval(property, error)) {
				return 0;
			}
		}
		ZEND_HASH_FOREACH_END();
	}

	return 1;
} /* }}} */

static zend_always_inline bool php_parallel_check_resource(zval *zv)
{ /* {{{ */
	zend_resource *resource = Z_RES_P(zv);

	if (resource->type == php_file_le_stream() || resource->type == php_file_le_pstream()) {
		return 1;
	}

	return 0;
} /* }}} */

bool php_parallel_check_zval(zval *zv, zval **error)
{ /* {{{ */
	switch (Z_TYPE_P(zv)) {
	case IS_OBJECT:
		if (PARALLEL_ZVAL_CHECK_CLOSURE(zv)) {
			if (!php_parallel_check_closure((zend_closure_t *)Z_OBJ_P(zv))) {
				if (error) {
					*error = zv;
				}
				return 0;
			}
			return 1;
		} else if (php_parallel_check_object(Z_OBJ_P(zv), error)) {
			return 1;
		}

		if (error) {
			*error = zv;
		}
		return 0;

	case IS_ARRAY: {
		zval *el;

		ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(zv), el)
		{
			if ((Z_TYPE_P(el) == IS_ARRAY) && (Z_ARRVAL_P(el) == Z_ARRVAL_P(zv))) {
				continue;
			}

			if (!php_parallel_check_zval(el, error)) {
				if (error) {
					*error = el;
				}
				return 0;
			}
		}
		ZEND_HASH_FOREACH_END();
	} break;

	case IS_RESOURCE:
		if (php_parallel_check_resource(zv)) {
			return 1;
		}

		if (error) {
			*error = zv;
		}
		return 0;
	}
	return 1;
} /* }}} */

/* {{{ */
static void php_parallel_checked_dtor(zval *zv) { efree(Z_PTR_P(zv)); }

PHP_RINIT_FUNCTION(PARALLEL_CHECK)
{
	zend_hash_init(&PCG(tasks), 32, NULL, php_parallel_checked_dtor, 0);
	zend_hash_init(&PCG(functions), 32, NULL, php_parallel_checked_dtor, 0);
	zend_hash_init(&PCG(types), 32, NULL, php_parallel_checked_dtor, 0);
	zend_hash_init(&PCG(classes), 32, NULL, php_parallel_checked_dtor, 0);

	return SUCCESS;
}

PHP_RSHUTDOWN_FUNCTION(PARALLEL_CHECK)
{
	zend_hash_destroy(&PCG(classes));
	zend_hash_destroy(&PCG(types));
	zend_hash_destroy(&PCG(functions));
	zend_hash_destroy(&PCG(tasks));

	return SUCCESS;
}
/* }}} */
#endif
