diff --git a/Zend/Optimizer/zend_inference.c b/Zend/Optimizer/zend_inference.c index 05d33d3d75fb..37f2592624e4 100644 --- a/Zend/Optimizer/zend_inference.c +++ b/Zend/Optimizer/zend_inference.c @@ -2382,15 +2382,29 @@ static uint32_t zend_convert_type(const zend_script *script, zend_type type, zen uint32_t tmp = zend_convert_type_declaration_mask(ZEND_TYPE_PURE_MASK(type)); if (ZEND_TYPE_IS_COMPLEX(type)) { - tmp |= MAY_BE_OBJECT; - if (pce) { - /* As we only have space to store one CE, - * we use a plain object type for class unions. */ - if (ZEND_TYPE_HAS_NAME(type)) { - zend_string *lcname = zend_string_tolower(ZEND_TYPE_NAME(type)); - // TODO: Pass through op_array. - *pce = zend_optimizer_get_class_entry(script, NULL, lcname); - zend_string_release_ex(lcname, 0); + /* A complex type is a class/object type, unless it is made up solely of + * literal types, which contribute their base scalar type instead. */ + bool has_class = false; + const zend_type *single_type; + ZEND_TYPE_FOREACH(type, single_type) { + if (ZEND_TYPE_HAS_LITERAL(*single_type)) { + /* int/float/string literal -> MAY_BE_LONG/DOUBLE/STRING */ + tmp |= 1u << Z_TYPE_P(ZEND_TYPE_LITERAL_VALUE(*single_type)); + } else { + has_class = true; + } + } ZEND_TYPE_FOREACH_END(); + if (has_class) { + tmp |= MAY_BE_OBJECT; + if (pce) { + /* As we only have space to store one CE, + * we use a plain object type for class unions. */ + if (ZEND_TYPE_HAS_NAME(type)) { + zend_string *lcname = zend_string_tolower(ZEND_TYPE_NAME(type)); + // TODO: Pass through op_array. + *pce = zend_optimizer_get_class_entry(script, NULL, lcname); + zend_string_release_ex(lcname, 0); + } } } } diff --git a/Zend/tests/type_declarations/literal_types/basic.phpt b/Zend/tests/type_declarations/literal_types/basic.phpt new file mode 100644 index 000000000000..b8cc73aa5af5 --- /dev/null +++ b/Zend/tests/type_declarations/literal_types/basic.phpt @@ -0,0 +1,39 @@ +--TEST-- +Literal types: basic accept and reject (int, float, string) +--FILE-- +getMessage(), "\n"; +} +try { + f(3.5); +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} +try { + s('c'); +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +int(1) +int(2) +int(3) +float(1.5) +float(2.5) +string(1) "a" +string(1) "b" +i(): Argument #1 ($x) must be of type 1|2|3, int given, called in %s on line %d +f(): Argument #1 ($x) must be of type 1.5|2.5, float given, called in %s on line %d +s(): Argument #1 ($x) must be of type 'a'|'b', string given, called in %s on line %d diff --git a/Zend/tests/type_declarations/literal_types/default_value_invalid.phpt b/Zend/tests/type_declarations/literal_types/default_value_invalid.phpt new file mode 100644 index 000000000000..ba9facb956fd --- /dev/null +++ b/Zend/tests/type_declarations/literal_types/default_value_invalid.phpt @@ -0,0 +1,8 @@ +--TEST-- +Literal types: a default value not equal to any literal is rejected +--FILE-- + +--EXPECTF-- +Fatal error: Cannot use int as default value for parameter $x of type 1|2 in %s on line %d diff --git a/Zend/tests/type_declarations/literal_types/interpolation_error.phpt b/Zend/tests/type_declarations/literal_types/interpolation_error.phpt new file mode 100644 index 000000000000..7369e6d43fd5 --- /dev/null +++ b/Zend/tests/type_declarations/literal_types/interpolation_error.phpt @@ -0,0 +1,8 @@ +--TEST-- +Literal types: interpolated double-quoted string is not a valid literal type +--FILE-- + +--EXPECTF-- +Parse error: syntax error, %s in %s on line %d diff --git a/Zend/tests/type_declarations/literal_types/intersection_error.phpt b/Zend/tests/type_declarations/literal_types/intersection_error.phpt new file mode 100644 index 000000000000..971aa90103c3 --- /dev/null +++ b/Zend/tests/type_declarations/literal_types/intersection_error.phpt @@ -0,0 +1,9 @@ +--TEST-- +Literal types: literals may not appear in intersection types +--FILE-- + +--EXPECTF-- +Parse error: syntax error, %s in %s on line %d diff --git a/Zend/tests/type_declarations/literal_types/negative.phpt b/Zend/tests/type_declarations/literal_types/negative.phpt new file mode 100644 index 000000000000..aae05da03458 --- /dev/null +++ b/Zend/tests/type_declarations/literal_types/negative.phpt @@ -0,0 +1,22 @@ +--TEST-- +Literal types: negative int and float literals +--FILE-- +getMessage(), "\n"; +} +?> +--EXPECTF-- +int(-1) +int(-2) +float(-1.5) +float(2.5) +i(): Argument #1 ($x) must be of type -1|-2, int given, called in %s on line %d diff --git a/Zend/tests/type_declarations/literal_types/property.phpt b/Zend/tests/type_declarations/literal_types/property.phpt new file mode 100644 index 000000000000..f3c2b886742f --- /dev/null +++ b/Zend/tests/type_declarations/literal_types/property.phpt @@ -0,0 +1,38 @@ +--TEST-- +Literal types: typed properties with literal types +--FILE-- +p, $c->state); + +$c->p = 3; +$c->state = 'on'; +var_dump($c->p, $c->state); + +$c->p = "2"; +var_dump($c->p); + +try { + $c->p = 9; +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} +try { + $c->state = 'maybe'; +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +int(1) +string(3) "off" +int(3) +string(2) "on" +int(2) +Cannot assign int to property C::$p of type 1|2|3 +Cannot assign string to property C::$state of type 'on'|'off' diff --git a/Zend/tests/type_declarations/literal_types/redundant_duplicate.phpt b/Zend/tests/type_declarations/literal_types/redundant_duplicate.phpt new file mode 100644 index 000000000000..6cac706e1316 --- /dev/null +++ b/Zend/tests/type_declarations/literal_types/redundant_duplicate.phpt @@ -0,0 +1,8 @@ +--TEST-- +Literal types: duplicate literal value is redundant +--FILE-- + +--EXPECTF-- +Fatal error: Literal type 1 is redundant as it is already present in the union in %s on line %d diff --git a/Zend/tests/type_declarations/literal_types/redundant_wider_after.phpt b/Zend/tests/type_declarations/literal_types/redundant_wider_after.phpt new file mode 100644 index 000000000000..84d5cd0756d1 --- /dev/null +++ b/Zend/tests/type_declarations/literal_types/redundant_wider_after.phpt @@ -0,0 +1,8 @@ +--TEST-- +Literal types: literal is redundant when the base scalar type is also present (literal first) +--FILE-- + +--EXPECTF-- +Fatal error: Literal type 1 is redundant as the union already allows its base type in %s on line %d diff --git a/Zend/tests/type_declarations/literal_types/redundant_wider_before.phpt b/Zend/tests/type_declarations/literal_types/redundant_wider_before.phpt new file mode 100644 index 000000000000..ce5cef6e0d0f --- /dev/null +++ b/Zend/tests/type_declarations/literal_types/redundant_wider_before.phpt @@ -0,0 +1,8 @@ +--TEST-- +Literal types: literal is redundant when the base scalar type is also present (scalar first) +--FILE-- + +--EXPECTF-- +Fatal error: Literal type 'foo' is redundant as the union already allows its base type in %s on line %d diff --git a/Zend/tests/type_declarations/literal_types/reflection.phpt b/Zend/tests/type_declarations/literal_types/reflection.phpt new file mode 100644 index 000000000000..4e64bdba1f9b --- /dev/null +++ b/Zend/tests/type_declarations/literal_types/reflection.phpt @@ -0,0 +1,31 @@ +--TEST-- +Literal types: reflection (union members, single literal, nullable) +--FILE-- +getParameters(); + +$union = $params[0]->getType(); +printf("a: %s | %s\n", get_class($union), (string) $union); +foreach ($union->getTypes() as $m) { + printf(" %s value=%s\n", get_class($m), var_export($m->getValue(), true)); +} + +$single = $params[1]->getType(); +printf("b: %s | %s | value=%s | nullable=%s\n", + get_class($single), (string) $single, var_export($single->getValue(), true), + var_export($single->allowsNull(), true)); + +$nullable = $params[2]->getType(); +printf("c: %s | %s | value=%s | nullable=%s\n", + get_class($nullable), (string) $nullable, var_export($nullable->getValue(), true), + var_export($nullable->allowsNull(), true)); +?> +--EXPECT-- +a: ReflectionUnionType | 1|2|'foo' + ReflectionLiteralScalarType value=1 + ReflectionLiteralScalarType value=2 + ReflectionLiteralScalarType value='foo' +b: ReflectionLiteralScalarType | 42 | value=42 | nullable=false +c: ReflectionLiteralScalarType | ?1 | value=1 | nullable=true diff --git a/Zend/tests/type_declarations/literal_types/reflection_literal_value.phpt b/Zend/tests/type_declarations/literal_types/reflection_literal_value.phpt new file mode 100644 index 000000000000..34b6a2e4205e --- /dev/null +++ b/Zend/tests/type_declarations/literal_types/reflection_literal_value.phpt @@ -0,0 +1,41 @@ +--TEST-- +Literal types: ReflectionLiteralScalarType::getValue() +--FILE-- +getParameters(); + +function dump(ReflectionType $t): void { + if ($t instanceof ReflectionLiteralScalarType) { + $v = $t->getValue(); + printf("%-7s %s value=%s (%s)\n", (string) $t, get_class($t), var_export($v, true), gettype($v)); + } else { + printf("%-7s %s\n", (string) $t, get_class($t)); + } +} + +foreach ($params[0]->getType()->getTypes() as $m) { + dump($m); +} +dump($params[1]->getType()); +dump($params[2]->getType()); +dump($params[3]->getType()); +dump($params[4]->getType()); +dump($params[5]->getType()); +dump($params[6]->getType()); +?> +--EXPECT-- +1 ReflectionLiteralScalarType value=1 (integer) +2 ReflectionLiteralScalarType value=2 (integer) +'foo' ReflectionLiteralScalarType value='foo' (string) +3.14 ReflectionLiteralScalarType value=3.14 (double) +42 ReflectionLiteralScalarType value=42 (integer) +-1 ReflectionLiteralScalarType value=-1 (integer) +'x' ReflectionLiteralScalarType value='x' (string) +?1 ReflectionLiteralScalarType value=1 (integer) +int ReflectionNamedType +Foo ReflectionNamedType diff --git a/Zend/tests/type_declarations/literal_types/return_and_default.phpt b/Zend/tests/type_declarations/literal_types/return_and_default.phpt new file mode 100644 index 000000000000..2a96f450f477 --- /dev/null +++ b/Zend/tests/type_declarations/literal_types/return_and_default.phpt @@ -0,0 +1,36 @@ +--TEST-- +Literal types: return types, default values, single and nullable literal types +--FILE-- +getMessage(), "\n"; +} + +function d(1|2 $x = 2): int { return $x; } +function df(1.0|2.5 $x = 1): float { return $x; } +var_dump(d(), df()); + +function one(42 $x): 42 { return $x; } +var_dump(one(42)); +try { + one(43); +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} + +function n(1|2|null $x): null|int { return $x; } +var_dump(n(1), n(null)); +?> +--EXPECTF-- +int(2) +r(): Return value must be of type 1|2|3, int returned +int(2) +float(1) +int(42) +one(): Argument #1 ($x) must be of type 42, int given, called in %s on line %d +int(1) +NULL diff --git a/Zend/tests/type_declarations/literal_types/strict_types.phpt b/Zend/tests/type_declarations/literal_types/strict_types.phpt new file mode 100644 index 000000000000..7bb6202e23a2 --- /dev/null +++ b/Zend/tests/type_declarations/literal_types/strict_types.phpt @@ -0,0 +1,32 @@ +--TEST-- +Literal types: strict_types disallows coercion (except int -> float literal) +--FILE-- +getMessage(), "\n"; + } +} +?> +--EXPECTF-- +int(2) +float(1.5) +float(2) +string(1) "a" +i(): Argument #1 ($x) must be of type 1|2|3, string given, called in %s on line %d +i(): Argument #1 ($x) must be of type 1|2|3, float given, called in %s on line %d +f(): Argument #1 ($x) must be of type 1.5|2.0, int given, called in %s on line %d +s(): Argument #1 ($x) must be of type 'a'|'b', int given, called in %s on line %d diff --git a/Zend/tests/type_declarations/literal_types/string_quotes.phpt b/Zend/tests/type_declarations/literal_types/string_quotes.phpt new file mode 100644 index 000000000000..4f0583ea4c0c --- /dev/null +++ b/Zend/tests/type_declarations/literal_types/string_quotes.phpt @@ -0,0 +1,26 @@ +--TEST-- +Literal types: single- and double-quoted string literals are equivalent +--FILE-- +getParameters()[0]->getType(), "\n"; +echo (new ReflectionFunction('b'))->getParameters()[0]->getType(), "\n"; + +function c('a\'b' $x): string { return $x; } +var_dump(c("a'b")); +echo (new ReflectionFunction('c'))->getParameters()[0]->getType(), "\n"; +?> +--EXPECT-- +string(3) "foo" +string(3) "foo" +string(3) "foo" +string(3) "foo" +'foo' +'foo' +string(3) "a'b" +'a\'b' diff --git a/Zend/tests/type_declarations/literal_types/union_with_wide_type.phpt b/Zend/tests/type_declarations/literal_types/union_with_wide_type.phpt new file mode 100644 index 000000000000..29c203632118 --- /dev/null +++ b/Zend/tests/type_declarations/literal_types/union_with_wide_type.phpt @@ -0,0 +1,34 @@ +--TEST-- +Literal types: a literal combined with a wide scalar type (1|string) +--FILE-- + %s(%s)\n", var_export($v, true), gettype($r), var_export($r, true)); +} + +foreach ([null, [1]] as $v) { + try { + foo($v); + } catch (TypeError $e) { + echo $e->getMessage(), "\n"; + } +} +?> +--EXPECTF-- +1 => integer(1) +2 => string('2') +0 => string('0') +1.0 => integer(1) +2.0 => string('2') +2.5 => string('2.5') +'1' => string('1') +'2' => string('2') +'foo' => string('foo') +true => integer(1) +false => string('') +foo(): Argument #1 ($x) must be of type 1|string, null given, called in %s on line %d +foo(): Argument #1 ($x) must be of type 1|string, array given, called in %s on line %d diff --git a/Zend/tests/type_declarations/literal_types/variance.phpt b/Zend/tests/type_declarations/literal_types/variance.phpt new file mode 100644 index 000000000000..c9e0db756b07 --- /dev/null +++ b/Zend/tests/type_declarations/literal_types/variance.phpt @@ -0,0 +1,33 @@ +--TEST-- +Literal types: valid variance (covariant returns, contravariant params) +--FILE-- +r()); +$b->m(3); + +$d = new D(); +var_dump($d->r()); +?> +--EXPECT-- +ok +int(1) +int(1) diff --git a/Zend/tests/type_declarations/literal_types/variance_invalid_param.phpt b/Zend/tests/type_declarations/literal_types/variance_invalid_param.phpt new file mode 100644 index 000000000000..96a3c0553ba6 --- /dev/null +++ b/Zend/tests/type_declarations/literal_types/variance_invalid_param.phpt @@ -0,0 +1,13 @@ +--TEST-- +Literal types: narrowing a contravariant parameter type is invalid +--FILE-- + +--EXPECTF-- +Fatal error: Declaration of B::m(1|2 $x): void must be compatible with A::m(int $x): void in %s on line %d diff --git a/Zend/tests/type_declarations/literal_types/variance_invalid_return.phpt b/Zend/tests/type_declarations/literal_types/variance_invalid_return.phpt new file mode 100644 index 000000000000..d8d73cb79bd7 --- /dev/null +++ b/Zend/tests/type_declarations/literal_types/variance_invalid_return.phpt @@ -0,0 +1,13 @@ +--TEST-- +Literal types: widening a covariant return type is invalid +--FILE-- + +--EXPECTF-- +Fatal error: Declaration of B::r(): 1|2|3 must be compatible with A::r(): 1|2 in %s on line %d diff --git a/Zend/tests/type_declarations/literal_types/weak_coercion.phpt b/Zend/tests/type_declarations/literal_types/weak_coercion.phpt new file mode 100644 index 000000000000..702aac73a573 --- /dev/null +++ b/Zend/tests/type_declarations/literal_types/weak_coercion.phpt @@ -0,0 +1,32 @@ +--TEST-- +Literal types: weak-mode coercion (coerce to base, then membership) +--FILE-- +getMessage(), "\n"; +} + +function mix(1|2|string $x): string { return $x === 1 ? "int-one" : (string) $x; } +var_dump(mix(1)); +var_dump(mix("hello")); +var_dump(mix(5)); +?> +--EXPECTF-- +int(2) +int(2) +int(1) +float(2.5) +i(): Argument #1 ($x) must be of type 1|2|3, string given, called in %s on line %d +string(7) "int-one" +string(5) "hello" +string(1) "5" diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index 983299c0a9d8..b408de695fa5 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -2061,6 +2061,10 @@ static ZEND_COLD void zend_ast_export_type(smart_str *str, zend_ast *ast, int in } return; } + if (ast->kind == ZEND_AST_TYPE_LITERAL) { + zend_ast_export_ex(str, ast->child[0], 0, indent); + return; + } if (ast->attr & ZEND_TYPE_NULLABLE) { smart_str_appendc(str, '?'); } diff --git a/Zend/zend_ast.h b/Zend/zend_ast.h index 24b77d7d3493..da7379917878 100644 --- a/Zend/zend_ast.h +++ b/Zend/zend_ast.h @@ -111,6 +111,7 @@ enum _zend_ast_kind { ZEND_AST_BREAK, ZEND_AST_CONTINUE, ZEND_AST_PROPERTY_HOOK_SHORT_BODY, + ZEND_AST_TYPE_LITERAL, /* 2 child nodes */ ZEND_AST_DIM = 2 << ZEND_AST_NUM_CHILDREN_SHIFT, diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 7e9f7ceac8db..a0d4b7b23f74 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -1441,6 +1441,59 @@ static zend_string *add_intersection_type(zend_string *str, return str; } +/* Render a string literal type as a single-quoted, escaped literal, e.g. 'fo\'o'. */ +static zend_string *zend_string_literal_type_to_string(const zend_string *s) +{ + size_t extra = 0; + for (size_t i = 0; i < ZSTR_LEN(s); i++) { + char c = ZSTR_VAL(s)[i]; + if (c == '\'' || c == '\\') { + extra++; + } + } + zend_string *out = zend_string_alloc(ZSTR_LEN(s) + extra + 2, 0); + char *p = ZSTR_VAL(out); + *p++ = '\''; + for (size_t i = 0; i < ZSTR_LEN(s); i++) { + char c = ZSTR_VAL(s)[i]; + if (c == '\'' || c == '\\') { + *p++ = '\\'; + } + + *p++ = c; + } + *p++ = '\''; + *p = '\0'; + return out; +} + +/* Render a literal type value to its source-level form. Floats use the + * serialize_precision=-1 (shortest round-trip) form so the rendering is + * deterministic and independent of ini settings. */ +static zend_string *zend_literal_type_to_string(const zval *zv) +{ + switch (Z_TYPE_P(zv)) { + case IS_LONG: + return zend_strpprintf(0, ZEND_LONG_FMT, Z_LVAL_P(zv)); + case IS_DOUBLE: { + zend_string *s = zend_strpprintf_unchecked(0, "%.*H", -1, Z_DVAL_P(zv)); + if (zend_finite(Z_DVAL_P(zv)) + && !memchr(ZSTR_VAL(s), '.', ZSTR_LEN(s)) + && !memchr(ZSTR_VAL(s), 'E', ZSTR_LEN(s))) { + zend_string *with_fraction = zend_strpprintf(0, "%s.0", ZSTR_VAL(s)); + zend_string_release(s); + return with_fraction; + } + return s; + } + case IS_STRING: + return zend_string_literal_type_to_string(Z_STR_P(zv)); + default: + ZEND_UNREACHABLE(); + } + return NULL; +} + zend_string *zend_type_to_string_resolved(const zend_type type, const zend_class_entry *scope) { zend_string *str = NULL; @@ -1456,6 +1509,12 @@ zend_string *zend_type_to_string_resolved(const zend_type type, const zend_class str = add_intersection_type(str, ZEND_TYPE_LIST(*list_type), /* is_bracketed */ true); continue; } + if (ZEND_TYPE_HAS_LITERAL(*list_type)) { + zend_string *literal_str = zend_literal_type_to_string(ZEND_TYPE_LITERAL_VALUE(*list_type)); + str = add_type_string(str, literal_str, /* is_intersection */ false); + zend_string_release(literal_str); + continue; + } ZEND_ASSERT(!ZEND_TYPE_HAS_LIST(*list_type)); ZEND_ASSERT(ZEND_TYPE_HAS_NAME(*list_type)); @@ -1466,6 +1525,8 @@ zend_string *zend_type_to_string_resolved(const zend_type type, const zend_class } ZEND_TYPE_LIST_FOREACH_END(); } else if (ZEND_TYPE_HAS_NAME(type)) { str = resolve_class_name(ZEND_TYPE_NAME(type), scope); + } else if (ZEND_TYPE_HAS_LITERAL(type)) { + str = zend_literal_type_to_string(ZEND_TYPE_LITERAL_VALUE(type)); } uint32_t type_mask = ZEND_TYPE_PURE_MASK(type); @@ -5141,7 +5202,7 @@ static zend_result zend_compile_func_array_map(znode *result, zend_ast_list *arg * breaking for the generated call. */ if (callback->kind == ZEND_AST_CALL - && callback->child[0]->kind == ZEND_AST_ZVAL + && callback->child[0]->kind == ZEND_AST_ZVAL && Z_TYPE_P(zend_ast_get_zval(callback->child[0])) == IS_STRING && zend_string_equals_literal_ci(zend_ast_get_str(callback->child[0]), "assert")) { return FAILURE; @@ -7361,9 +7422,79 @@ ZEND_API void zend_set_function_arg_flags(zend_function *func) /* {{{ */ } /* }}} */ +/* Whether two literal type values denote the same type. Distinct base types + * (e.g. int 1 vs float 1.0) are never equal. */ +static bool zend_literal_types_equal(const zval *a, const zval *b) +{ + if (Z_TYPE_P(a) != Z_TYPE_P(b)) { + return false; + } + + switch (Z_TYPE_P(a)) { + case IS_LONG: + return Z_LVAL_P(a) == Z_LVAL_P(b); + case IS_DOUBLE: + return Z_DVAL_P(a) == Z_DVAL_P(b); + case IS_STRING: + return zend_string_equals(Z_STR_P(a), Z_STR_P(b)); + default: + ZEND_UNREACHABLE(); + } + + return false; +} + +/* The MAY_BE_* bit for the base scalar kind a literal narrows. */ +static zend_always_inline uint32_t zend_literal_type_base_mask(const zval *zv) +{ + ZEND_ASSERT(Z_TYPE_P(zv) == IS_LONG || Z_TYPE_P(zv) == IS_DOUBLE || Z_TYPE_P(zv) == IS_STRING); + return 1u << Z_TYPE_P(zv); +} + +/* Compile an int/float/string literal (optionally negated) into a literal type. + * The value zval is arena-allocated alongside the type; string literals are + * interned so they persist and compare cheaply. */ +static zend_type zend_compile_literal_typename(zend_ast *ast) +{ + bool negate = false; + zend_ast *zv_ast = ast; + + if (ast->kind == ZEND_AST_UNARY_MINUS) { + negate = true; + zv_ast = ast->child[0]; + } + + ZEND_ASSERT(zv_ast->kind == ZEND_AST_ZVAL); + + zval value; + ZVAL_COPY(&value, zend_ast_get_zval(zv_ast)); + + if (negate) { + if (Z_TYPE(value) == IS_LONG) { + ZVAL_LONG(&value, -Z_LVAL(value)); + } else { + ZEND_ASSERT(Z_TYPE(value) == IS_DOUBLE); + ZVAL_DOUBLE(&value, -Z_DVAL(value)); + } + } + + if (Z_TYPE(value) == IS_STRING) { + ZVAL_STR(&value, zend_new_interned_string(Z_STR(value))); + } + + zval *zv = zend_arena_alloc(&CG(arena), sizeof(zval)); + ZVAL_COPY_VALUE(zv, &value); + + return (zend_type) ZEND_TYPE_INIT_LITERAL(zv, 0); +} + static zend_type zend_compile_single_typename(zend_ast *ast) { ZEND_ASSERT(!(ast->attr & ZEND_TYPE_NULLABLE)); + if (ast->kind == ZEND_AST_TYPE_LITERAL) { + return zend_compile_literal_typename(ast->child[0]); + } + if (ast->kind == ZEND_AST_TYPE) { if (ast->attr == IS_STATIC && !CG(active_class_entry) && zend_is_scope_known()) { zend_error_noreturn(E_COMPILE_ERROR, @@ -7521,6 +7652,10 @@ static void zend_is_type_list_redundant_by_single_type(const zend_type_list *typ { ZEND_ASSERT(!ZEND_TYPE_IS_INTERSECTION(type)); for (size_t i = 0; i < type_list->num_types - 1; i++) { + if (ZEND_TYPE_HAS_LITERAL(type_list->types[i])) { + continue; + } + if (ZEND_TYPE_IS_INTERSECTION(type_list->types[i])) { zend_is_intersection_type_redundant_by_single_type(type_list->types[i], type); continue; @@ -7618,6 +7753,51 @@ static zend_type zend_compile_typename_ex( /* Clear MAY_BE_* type flags */ ZEND_TYPE_FULL_MASK(single_type) &= ~_ZEND_TYPE_MAY_BE_MASK; + if (ZEND_TYPE_HAS_LITERAL(single_type)) { + zval *literal = ZEND_TYPE_LITERAL_VALUE(single_type); + uint32_t base_mask = zend_literal_type_base_mask(literal); + + if (ZEND_TYPE_PURE_MASK(type) & base_mask) { + zend_string *literal_str = zend_type_to_string(single_type); + zend_error_noreturn(E_COMPILE_ERROR, + "Literal type %s is redundant as the union already allows its base type", ZSTR_VAL(literal_str)); + } + + if (!ZEND_TYPE_HAS_LIST(type)) { + if (ZEND_TYPE_IS_COMPLEX(type)) { + type_list->num_types = 1; + type_list->types[0] = type; + ZEND_TYPE_FULL_MASK(type_list->types[0]) &= ~_ZEND_TYPE_MAY_BE_MASK; + } + + ZEND_TYPE_SET_LIST(type, type_list); + } + + for (uint32_t j = 0; j < type_list->num_types; j++) { + if (ZEND_TYPE_HAS_LITERAL(type_list->types[j]) + && zend_literal_types_equal(ZEND_TYPE_LITERAL_VALUE(type_list->types[j]), literal)) { + zend_string *literal_str = zend_type_to_string(single_type); + zend_error_noreturn(E_COMPILE_ERROR, + "Literal type %s is redundant as it is already present in the union", ZSTR_VAL(literal_str)); + } + } + + type_list->types[type_list->num_types++] = single_type; + continue; + } + + if ((single_type_mask & (MAY_BE_LONG|MAY_BE_DOUBLE|MAY_BE_STRING)) && ZEND_TYPE_HAS_LIST(type)) { + const zend_type_list *cur_list = ZEND_TYPE_LIST(type); + for (uint32_t j = 0; j < cur_list->num_types; j++) { + if (ZEND_TYPE_HAS_LITERAL(cur_list->types[j]) + && (zend_literal_type_base_mask(ZEND_TYPE_LITERAL_VALUE(cur_list->types[j])) & single_type_mask)) { + zend_string *literal_str = zend_type_to_string(cur_list->types[j]); + zend_error_noreturn(E_COMPILE_ERROR, + "Literal type %s is redundant as the union already allows its base type", ZSTR_VAL(literal_str)); + } + } + } + if (ZEND_TYPE_IS_COMPLEX(single_type)) { if (!ZEND_TYPE_IS_COMPLEX(type) && !is_composite) { /* The first class type can be stored directly as the type ptr payload. */ @@ -7676,6 +7856,13 @@ static zend_type zend_compile_typename_ex( zend_ast *type_ast = list->child[i]; zend_type single_type = zend_compile_single_typename(type_ast); + /* The grammar already excludes lit & named; this guards the name-based checks below should that ever change. */ + if (ZEND_TYPE_HAS_LITERAL(single_type)) { + zend_string *literal_str = zend_type_to_string(single_type); + zend_error_noreturn(E_COMPILE_ERROR, + "Type %s cannot be part of an intersection type", ZSTR_VAL(literal_str)); + } + /* An intersection of union types cannot exist so invalidate it * Currently only can happen with iterable getting canonicalized to Traversable|array */ if (ZEND_TYPE_IS_ITERABLE_FALLBACK(single_type)) { @@ -7729,6 +7916,17 @@ static zend_type zend_compile_typename_ex( } } else { type = zend_compile_single_typename(ast); + if (ZEND_TYPE_HAS_LITERAL(type)) { + /* A standalone literal type is wrapped in a single-element union list so + * the container carries _ZEND_TYPE_LIST_BIT (keeping ZEND_TYPE_IS_SET valid) + * while its pure mask stays empty (so it never accepts the base scalar). */ + zend_type_list *literal_list = zend_arena_alloc(&CG(arena), ZEND_TYPE_LIST_SIZE(1)); + literal_list->num_types = 1; + literal_list->types[0] = type; + type = (zend_type) ZEND_TYPE_INIT_NONE(0); + ZEND_TYPE_SET_LIST(type, literal_list); + ZEND_TYPE_FULL_MASK(type) |= _ZEND_TYPE_UNION_BIT | _ZEND_TYPE_ARENA_BIT; + } } uint32_t type_mask = ZEND_TYPE_PURE_MASK(type); @@ -7781,6 +7979,33 @@ static bool zend_is_valid_default_value(zend_type type, zval *value) convert_to_double(value); return true; } + + /* For literal types the default value must equal one of the literals (an int + * is also accepted for a matching float literal). */ + if (ZEND_TYPE_HAS_LIST(type)) { + const zend_type *list_type; + bool int_matches_double_literal = false; + ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(type), list_type) { + if (!ZEND_TYPE_HAS_LITERAL(*list_type)) { + continue; + } + + const zval *literal = ZEND_TYPE_LITERAL_VALUE(*list_type); + if (zend_literal_types_equal(literal, value)) { + return true; + } + + if (Z_TYPE_P(literal) == IS_DOUBLE && Z_TYPE_P(value) == IS_LONG + && Z_DVAL_P(literal) == (double) Z_LVAL_P(value)) { + int_matches_double_literal = true; + } + } ZEND_TYPE_LIST_FOREACH_END(); + + if (int_matches_double_literal) { + convert_to_double(value); + return true; + } + } return false; } diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 1b28ce25fe37..4357f95053d5 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -1009,6 +1009,10 @@ static bool zend_check_and_resolve_property_or_class_constant_class_type( } else { const zend_type *list_type; ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(member_type), list_type) { + if (ZEND_TYPE_HAS_LITERAL(*list_type)) { + continue; + } + if (ZEND_TYPE_IS_INTERSECTION(*list_type)) { if (zend_check_intersection_for_property_or_class_constant_class_type( scope, ZEND_TYPE_LIST(*list_type), value_ce)) { @@ -1039,6 +1043,8 @@ static bool zend_check_and_resolve_property_or_class_constant_class_type( return false; } +static bool zend_check_literal_type(const zend_type *type, zval *arg, bool strict, bool allow_coercion); + static zend_always_inline bool i_zend_check_property_type(const zend_property_info *info, zval *property, bool strict) { ZEND_ASSERT(!Z_ISREF_P(property)); @@ -1051,6 +1057,10 @@ static zend_always_inline bool i_zend_check_property_type(const zend_property_in return 1; } + if (ZEND_TYPE_HAS_LIST(info->type) && zend_check_literal_type(&info->type, property, strict, /* allow_coercion */ true)) { + return 1; + } + uint32_t type_mask = ZEND_TYPE_FULL_MASK(info->type); ZEND_ASSERT(!(type_mask & (MAY_BE_CALLABLE|MAY_BE_STATIC|MAY_BE_NEVER|MAY_BE_VOID))); return zend_verify_scalar_type_hint(type_mask, property, strict, false); @@ -1151,6 +1161,148 @@ static bool zend_check_intersection_type_from_list( return true; } +/* Whether a runtime value exactly equals a literal type value (same base type + * and value; int 1 and float 1.0 are distinct). */ +static zend_always_inline bool zend_literal_type_value_matches(const zval *literal, const zval *arg) +{ + if (Z_TYPE_P(literal) != Z_TYPE_P(arg)) { + return false; + } + + switch (Z_TYPE_P(literal)) { + case IS_LONG: + return Z_LVAL_P(literal) == Z_LVAL_P(arg); + case IS_DOUBLE: + return Z_DVAL_P(literal) == Z_DVAL_P(arg); + case IS_STRING: + return zend_string_equals(Z_STR_P(literal), Z_STR_P(arg)); + default: + return false; + } +} + +/* Whether a value equals any literal entry of the type (used after coercion). */ +static zend_always_inline bool zend_value_matches_a_literal(const zend_type *type, const zval *value) +{ + const zend_type *list_type; + ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(*type), list_type) { + if (ZEND_TYPE_HAS_LITERAL(*list_type) && zend_literal_type_value_matches(ZEND_TYPE_LITERAL_VALUE(*list_type), value)) { + return true; + } + } ZEND_TYPE_LIST_FOREACH_END(); + + return false; +} + +/* Check whether arg satisfies any literal entry of a (union) type. In weak mode + * (allow_coercion && !strict) the value is coerced to a literal's base scalar + * type and then tested for membership; on success arg is updated in place. */ +static bool zend_check_literal_type(const zend_type *type, zval *arg, bool strict, bool allow_coercion) +{ + const zend_type *list_type; + uint32_t literal_base_mask = 0; + + /* Exact value match first; this never mutates arg. */ + ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(*type), list_type) { + if (!ZEND_TYPE_HAS_LITERAL(*list_type)) { + continue; + } + + const zval *literal = ZEND_TYPE_LITERAL_VALUE(*list_type); + literal_base_mask |= 1u << Z_TYPE_P(literal); + if (zend_literal_type_value_matches(literal, arg)) { + return true; + } + } ZEND_TYPE_LIST_FOREACH_END(); + + if (literal_base_mask == 0 || !allow_coercion) { + return false; + } + + if (strict) { + /* Under strict_types only the int -> float widening is allowed, mirroring + * how the float type accepts an int argument. */ + if ((literal_base_mask & MAY_BE_DOUBLE) && Z_TYPE_P(arg) == IS_LONG) { + double dval = (double) Z_LVAL_P(arg); + ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(*type), list_type) { + if (ZEND_TYPE_HAS_LITERAL(*list_type)) { + const zval *literal = ZEND_TYPE_LITERAL_VALUE(*list_type); + if (Z_TYPE_P(literal) == IS_DOUBLE && Z_DVAL_P(literal) == dval) { + ZVAL_DOUBLE(arg, dval); + return true; + } + } + } ZEND_TYPE_LIST_FOREACH_END(); + } + + return false; + } + + if (Z_TYPE_P(arg) == IS_NULL) { + return false; + } + + zend_long lval; + double dval; + zval coerced; + + if (literal_base_mask & MAY_BE_LONG) { + if ((literal_base_mask & MAY_BE_DOUBLE) && Z_TYPE_P(arg) == IS_STRING) { + uint8_t kind = is_numeric_str_function(Z_STR_P(arg), &lval, &dval); + if (kind == IS_LONG) { + ZVAL_LONG(&coerced, lval); + if (zend_value_matches_a_literal(type, &coerced)) { + zval_ptr_dtor(arg); + ZVAL_LONG(arg, lval); + return true; + } + } else if (kind == IS_DOUBLE) { + ZVAL_DOUBLE(&coerced, dval); + if (zend_value_matches_a_literal(type, &coerced)) { + zval_ptr_dtor(arg); + ZVAL_DOUBLE(arg, dval); + return true; + } + } + } else if (zend_parse_arg_long_weak(arg, &lval, (uint32_t)-1)) { + ZVAL_LONG(&coerced, lval); + if (zend_value_matches_a_literal(type, &coerced)) { + zend_parse_arg_long_weak(arg, &lval, 0); + zval_ptr_dtor(arg); + ZVAL_LONG(arg, lval); + return true; + } + } + } + + if (literal_base_mask & MAY_BE_DOUBLE) { + dval = zend_parse_arg_double_weak(arg, (uint32_t)-1); + if (!zend_isnan(dval)) { + ZVAL_DOUBLE(&coerced, dval); + if (zend_value_matches_a_literal(type, &coerced)) { + zval_ptr_dtor(arg); + ZVAL_DOUBLE(arg, dval); + return true; + } + } + } + + if (literal_base_mask & MAY_BE_STRING) { + zval tmp; + ZVAL_COPY(&tmp, arg); + if (zend_parse_arg_str_weak(&tmp, (uint32_t)-1)) { + if (zend_value_matches_a_literal(type, &tmp)) { + zval_ptr_dtor(arg); + ZVAL_COPY_VALUE(arg, &tmp); + return true; + } + } + zval_ptr_dtor(&tmp); + } + + return false; +} + static zend_always_inline bool zend_check_type_slow( const zend_type *type, zval *arg, const zend_reference *ref, bool is_return_type, bool is_internal) @@ -1167,6 +1319,9 @@ static zend_always_inline bool zend_check_type_slow( if (zend_check_intersection_type_from_list(ZEND_TYPE_LIST(*list_type), Z_OBJCE_P(arg))) { return true; } + } else if (ZEND_TYPE_HAS_LITERAL(*list_type)) { + /* A literal entry can never match an object */ + continue; } else { ZEND_ASSERT(!ZEND_TYPE_HAS_LIST(*list_type)); ce = zend_fetch_ce_from_type(list_type); @@ -1187,6 +1342,15 @@ static zend_always_inline bool zend_check_type_slow( } } + /* Literal types (int/float/string literal values), stored as list entries. */ + if (ZEND_TYPE_HAS_LIST(*type)) { + bool strict = is_return_type ? ZEND_RET_USES_STRICT_TYPES() : ZEND_ARG_USES_STRICT_TYPES(); + bool allow_coercion = !(ref && ZEND_REF_HAS_TYPE_SOURCES(ref)) && !(is_internal && is_return_type); + if (zend_check_literal_type(type, arg, strict, allow_coercion)) { + return 1; + } + } + const uint32_t type_mask = ZEND_TYPE_FULL_MASK(*type); if ((type_mask & MAY_BE_CALLABLE) && zend_is_callable(arg, is_internal ? IS_CALLABLE_SUPPRESS_DEPRECATIONS : 0, NULL)) { diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index e85b4ea42250..9bd7210f40b4 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -92,6 +92,17 @@ static void zend_type_copy_ctor(zend_type *const type, bool use_arena, bool pers zend_type_list_copy_ctor(type, use_arena, persistent); } else if (ZEND_TYPE_HAS_NAME(*type)) { zend_string_addref(ZEND_TYPE_NAME(*type)); + } else if (ZEND_TYPE_HAS_LITERAL(*type)) { + /* Deep-copy the literal value into a fresh holder in the target domain. */ + const zval *old_zv = ZEND_TYPE_LITERAL_VALUE(*type); + zval *new_zv = use_arena ? zend_arena_alloc(&CG(arena), sizeof(zval)) : pemalloc(sizeof(zval), persistent); + if (Z_TYPE_P(old_zv) == IS_STRING && persistent) { + ZVAL_STR(new_zv, zend_string_dup(Z_STR_P(old_zv), 1)); + } else { + ZVAL_COPY(new_zv, old_zv); + } + + ZEND_TYPE_SET_PTR(*type, new_zv); } } @@ -670,6 +681,39 @@ static inheritance_status zend_is_intersection_subtype_of_type( return early_exit_status == INHERITANCE_ERROR ? INHERITANCE_SUCCESS : INHERITANCE_ERROR; } +static bool zend_literal_type_values_identical(const zval *a, const zval *b) +{ + if (Z_TYPE_P(a) != Z_TYPE_P(b)) { + return false; + } + + switch (Z_TYPE_P(a)) { + case IS_LONG: return Z_LVAL_P(a) == Z_LVAL_P(b); + case IS_DOUBLE: return Z_DVAL_P(a) == Z_DVAL_P(b); + case IS_STRING: return zend_string_equals(Z_STR_P(a), Z_STR_P(b)); + default: return false; + } +} + +/* A literal fe member is a subtype of proto if proto allows its base scalar type + * (e.g. 1 ∈ int) or contains an identical literal (e.g. 1 ∈ 1|2). */ +static inheritance_status zend_is_literal_subtype_of_type( + const zval *literal, const zend_type proto_type) +{ + if (ZEND_TYPE_PURE_MASK(proto_type) & (1u << Z_TYPE_P(literal))) { + return INHERITANCE_SUCCESS; + } + + const zend_type *single_type; + ZEND_TYPE_FOREACH(proto_type, single_type) { + if (ZEND_TYPE_HAS_LITERAL(*single_type) && zend_literal_type_values_identical(literal, ZEND_TYPE_LITERAL_VALUE(*single_type))) { + return INHERITANCE_SUCCESS; + } + } ZEND_TYPE_FOREACH_END(); + + return INHERITANCE_ERROR; +} + static inheritance_status zend_perform_covariant_type_check( zend_class_entry *fe_scope, const zend_type fe_type, zend_class_entry *proto_scope, const zend_type proto_type) @@ -733,6 +777,8 @@ static inheritance_status zend_perform_covariant_type_check( if (ZEND_TYPE_IS_INTERSECTION(*single_type)) { status = zend_is_intersection_subtype_of_type( fe_scope, *single_type, proto_scope, proto_type); + } else if (ZEND_TYPE_HAS_LITERAL(*single_type)) { + status = zend_is_literal_subtype_of_type(ZEND_TYPE_LITERAL_VALUE(*single_type), proto_type); } else { zend_string *fe_class_name = get_class_from_type(fe_scope, *single_type); if (!fe_class_name) { diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index b4dda00404ea..495755b082bc 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -279,7 +279,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %type array_pair non_empty_array_pair_list array_pair_list possible_array_pair %type isset_variable type return_type type_expr type_without_static %type identifier type_expr_without_static union_type_without_static_element union_type_without_static intersection_type_without_static -%type inline_function union_type_element union_type intersection_type +%type inline_function union_type_element union_type intersection_type literal_type %type attributed_statement attributed_top_statement attributed_class_statement attributed_parameter %type attribute_decl attribute attributes attribute_group namespace_declaration_name %type match match_arm_list non_empty_match_arm_list match_arm match_arm_cond_list @@ -833,6 +833,7 @@ optional_type_without_static: type_expr: type { $$ = $1; } | '?' type { $$ = $2; $$->attr |= ZEND_TYPE_NULLABLE; } + | literal_type { $$ = $1; } | union_type { $$ = $1; } | intersection_type { $$ = $1; } ; @@ -842,8 +843,17 @@ type: | T_STATIC { $$ = zend_ast_create_ex(ZEND_AST_TYPE, IS_STATIC); } ; +literal_type: + T_LNUMBER { $$ = zend_ast_create(ZEND_AST_TYPE_LITERAL, $1); } + | T_DNUMBER { $$ = zend_ast_create(ZEND_AST_TYPE_LITERAL, $1); } + | '-' T_LNUMBER { $$ = zend_ast_create(ZEND_AST_TYPE_LITERAL, zend_ast_create(ZEND_AST_UNARY_MINUS, $2)); } + | '-' T_DNUMBER { $$ = zend_ast_create(ZEND_AST_TYPE_LITERAL, zend_ast_create(ZEND_AST_UNARY_MINUS, $2)); } + | T_CONSTANT_ENCAPSED_STRING { $$ = zend_ast_create(ZEND_AST_TYPE_LITERAL, $1); } +; + union_type_element: type { $$ = $1; } + | literal_type { $$ = $1; } | '(' intersection_type ')' { $$ = $2; } ; @@ -865,6 +875,7 @@ intersection_type: type_expr_without_static: type_without_static { $$ = $1; } | '?' type_without_static { $$ = $2; $$->attr |= ZEND_TYPE_NULLABLE; } + | literal_type { $$ = $1; } | union_type_without_static { $$ = $1; } | intersection_type_without_static { $$ = $1; } ; @@ -877,6 +888,7 @@ type_without_static: union_type_without_static_element: type_without_static { $$ = $1; } + | literal_type { $$ = $1; } | '(' intersection_type_without_static ')' { $$ = $2; } ; diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index 538eff3ea34d..802e1e82494b 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -111,11 +111,24 @@ ZEND_API void destroy_zend_function(zend_function *function) ZEND_API void zend_type_release(zend_type type, bool persistent) { if (ZEND_TYPE_HAS_LIST(type)) { + bool uses_arena = ZEND_TYPE_USES_ARENA(type); zend_type *list_type; ZEND_TYPE_LIST_FOREACH_MUTABLE(ZEND_TYPE_LIST(type), list_type) { + /* Literal entries own a zval holder (and, for strings, the string). + * The holder lives in the arena unless the list is non-arena. */ + if (ZEND_TYPE_HAS_LITERAL(*list_type)) { + zval *zv = ZEND_TYPE_LITERAL_VALUE(*list_type); + if (Z_TYPE_P(zv) == IS_STRING) { + zend_string_release(Z_STR_P(zv)); + } + if (!uses_arena) { + pefree(zv, persistent); + } + continue; + } zend_type_release(*list_type, persistent); } ZEND_TYPE_LIST_FOREACH_END(); - if (!ZEND_TYPE_USES_ARENA(type)) { + if (!uses_arena) { pefree(ZEND_TYPE_LIST(type), persistent); } } else if (ZEND_TYPE_HAS_NAME(type)) { diff --git a/Zend/zend_types.h b/Zend/zend_types.h index 0edc4df37484..6c33248ea3fc 100644 --- a/Zend/zend_types.h +++ b/Zend/zend_types.h @@ -129,12 +129,14 @@ typedef struct { #define _ZEND_TYPE_EXTRA_FLAGS_SHIFT 25 #define _ZEND_TYPE_MASK ((1u << 25) - 1) +/* Used to signify that type.ptr is a `zval*` holding a literal type value (int/float/string literal type). */ +#define _ZEND_TYPE_LITERAL_BIT (1u << 30) /* Only one of these bits may be set. */ #define _ZEND_TYPE_NAME_BIT (1u << 24) // Used to signify that type.ptr is not a `zend_string*` but a `const char*`, #define _ZEND_TYPE_LITERAL_NAME_BIT (1u << 23) #define _ZEND_TYPE_LIST_BIT (1u << 22) -#define _ZEND_TYPE_KIND_MASK (_ZEND_TYPE_LIST_BIT|_ZEND_TYPE_NAME_BIT|_ZEND_TYPE_LITERAL_NAME_BIT) +#define _ZEND_TYPE_KIND_MASK (_ZEND_TYPE_LITERAL_BIT|_ZEND_TYPE_LIST_BIT|_ZEND_TYPE_NAME_BIT|_ZEND_TYPE_LITERAL_NAME_BIT) /* For BC behaviour with iterable type */ #define _ZEND_TYPE_ITERABLE_BIT (1u << 21) /* Whether the type list is arena allocated */ @@ -165,6 +167,10 @@ typedef struct { #define ZEND_TYPE_HAS_LIST(t) \ ((((t).type_mask) & _ZEND_TYPE_LIST_BIT) != 0) +/* Whether type.ptr is a zval* holding a literal type value. */ +#define ZEND_TYPE_HAS_LITERAL(t) \ + ((((t).type_mask) & _ZEND_TYPE_LITERAL_BIT) != 0) + #define ZEND_TYPE_IS_ITERABLE_FALLBACK(t) \ ((((t).type_mask) & _ZEND_TYPE_ITERABLE_BIT) != 0) @@ -186,6 +192,9 @@ typedef struct { #define ZEND_TYPE_LITERAL_NAME(t) \ ((const char *) (t).ptr) +#define ZEND_TYPE_LITERAL_VALUE(t) \ + ((zval *) (t).ptr) + #define ZEND_TYPE_LIST(t) \ ((zend_type_list *) (t).ptr) @@ -318,6 +327,11 @@ typedef struct { #define ZEND_TYPE_INIT_CLASS_CONST_MASK(class_name, type_mask) \ ZEND_TYPE_INIT_PTR_MASK(class_name, (_ZEND_TYPE_LITERAL_NAME_BIT | (type_mask))) +/* A literal type entry: ptr is a zval* whose Z_TYPE (IS_LONG/IS_DOUBLE/IS_STRING) + * is both the value and the literal kind discriminator. */ +#define ZEND_TYPE_INIT_LITERAL(zv, extra_flags) \ + ZEND_TYPE_INIT_PTR_MASK(zv, (_ZEND_TYPE_LITERAL_BIT | (extra_flags))) + typedef union _zend_value { zend_long lval; /* long value */ double dval; /* double value */ diff --git a/ext/opcache/zend_file_cache.c b/ext/opcache/zend_file_cache.c index af59b9b2c34a..0dde40036048 100644 --- a/ext/opcache/zend_file_cache.c +++ b/ext/opcache/zend_file_cache.c @@ -487,6 +487,16 @@ static void zend_file_cache_serialize_type( zend_string *type_name = ZEND_TYPE_NAME(*type); SERIALIZE_STR(type_name); ZEND_TYPE_SET_PTR(*type, type_name); + } else if (ZEND_TYPE_HAS_LITERAL(*type)) { + zval *zv = ZEND_TYPE_LITERAL_VALUE(*type); + SERIALIZE_PTR(zv); + ZEND_TYPE_SET_PTR(*type, zv); + UNSERIALIZE_PTR(zv); + if (Z_TYPE_P(zv) == IS_STRING) { + zend_string *str = Z_STR_P(zv); + SERIALIZE_STR(str); + ZVAL_STR(zv, str); + } } } @@ -1407,6 +1417,15 @@ static void zend_file_cache_unserialize_type( } else { zend_alloc_ce_cache(type_name); } + } else if (ZEND_TYPE_HAS_LITERAL(*type)) { + zval *zv = ZEND_TYPE_LITERAL_VALUE(*type); + UNSERIALIZE_PTR(zv); + ZEND_TYPE_SET_PTR(*type, zv); + if (Z_TYPE_P(zv) == IS_STRING) { + zend_string *str = Z_STR_P(zv); + UNSERIALIZE_STR(str); + ZVAL_STR(zv, str); + } } } diff --git a/ext/opcache/zend_persist.c b/ext/opcache/zend_persist.c index c06452e6acf2..c4dd0e4ba1bc 100644 --- a/ext/opcache/zend_persist.c +++ b/ext/opcache/zend_persist.c @@ -384,6 +384,16 @@ static void zend_persist_type(zend_type *type) { zend_accel_get_class_name_map_ptr(type_name); } } + if (ZEND_TYPE_HAS_LITERAL(*single_type)) { + zval *new_zv = zend_shared_memdup(ZEND_TYPE_LITERAL_VALUE(*single_type), sizeof(zval)); + if (Z_TYPE_P(new_zv) == IS_STRING) { + zend_string *str = Z_STR_P(new_zv); + zend_accel_store_interned_string(str); + ZVAL_STR(new_zv, str); + } + + ZEND_TYPE_SET_PTR(*single_type, new_zv); + } } ZEND_TYPE_FOREACH_END(); } diff --git a/ext/opcache/zend_persist_calc.c b/ext/opcache/zend_persist_calc.c index 9ff37079193b..9884be0dc665 100644 --- a/ext/opcache/zend_persist_calc.c +++ b/ext/opcache/zend_persist_calc.c @@ -212,6 +212,16 @@ static void zend_persist_type_calc(zend_type *type) ADD_INTERNED_STRING(type_name); ZEND_TYPE_SET_PTR(*single_type, type_name); } + + if (ZEND_TYPE_HAS_LITERAL(*single_type)) { + ADD_SIZE(sizeof(zval)); + zval *zv = ZEND_TYPE_LITERAL_VALUE(*single_type); + if (Z_TYPE_P(zv) == IS_STRING) { + zend_string *str = Z_STR_P(zv); + ADD_INTERNED_STRING(str); + ZVAL_STR(zv, str); + } + } } ZEND_TYPE_FOREACH_END(); } diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index eff61659d078..a460ff599cd7 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -86,6 +86,7 @@ PHPAPI zend_class_entry *reflection_generator_ptr; PHPAPI zend_class_entry *reflection_parameter_ptr; PHPAPI zend_class_entry *reflection_type_ptr; PHPAPI zend_class_entry *reflection_named_type_ptr; +PHPAPI zend_class_entry *reflection_literal_scalar_type_ptr; PHPAPI zend_class_entry *reflection_intersection_type_ptr; PHPAPI zend_class_entry *reflection_union_type_ptr; PHPAPI zend_class_entry *reflection_class_ptr; @@ -1455,9 +1456,12 @@ static void reflection_parameter_factory(zend_function *fptr, zval *closure_obje typedef enum { NAMED_TYPE = 0, UNION_TYPE = 1, - INTERSECTION_TYPE = 2 + INTERSECTION_TYPE = 2, + LITERAL_SCALAR_TYPE = 3 } reflection_type_kind; +static const zval *reflection_type_get_literal(zend_type type); + /* For backwards compatibility reasons, we need to return T|null style unions * and transformation from iterable to Traversable|array * as a ReflectionNamedType. Here we determine what counts as a union type and @@ -1465,6 +1469,12 @@ typedef enum { static reflection_type_kind get_type_kind(zend_type type) { uint32_t type_mask_without_null = ZEND_TYPE_PURE_MASK_WITHOUT_NULL(type); + /* An int/float/string literal value type, whether bare or wrapped in a + * single-element (possibly nullable) union, is its own reflection type. */ + if (reflection_type_get_literal(type) != NULL) { + return LITERAL_SCALAR_TYPE; + } + if (ZEND_TYPE_HAS_LIST(type)) { if (ZEND_TYPE_IS_INTERSECTION(type)) { return INTERSECTION_TYPE; @@ -1512,6 +1522,9 @@ static void reflection_type_factory(zend_type type, zval *object, bool legacy_be case NAMED_TYPE: object_init_ex(object, reflection_named_type_ptr); break; + case LITERAL_SCALAR_TYPE: + object_init_ex(object, reflection_literal_scalar_type_ptr); + break; default: ZEND_UNREACHABLE(); } @@ -3128,6 +3141,40 @@ ZEND_METHOD(ReflectionNamedType, isBuiltin) } /* }}} */ +/* If the reflected type is an int/float/string literal value type, return its + * value; otherwise NULL. Handles both a bare literal entry (a union member) and + * a standalone literal stored as a single-element union list. */ +static const zval *reflection_type_get_literal(zend_type type) +{ + if (ZEND_TYPE_HAS_LITERAL(type)) { + return ZEND_TYPE_LITERAL_VALUE(type); + } + + if (ZEND_TYPE_HAS_LIST(type) && ZEND_TYPE_IS_UNION(type)) { + const zend_type_list *list = ZEND_TYPE_LIST(type); + if (list->num_types == 1 && ZEND_TYPE_HAS_LITERAL(list->types[0])) { + return ZEND_TYPE_LITERAL_VALUE(list->types[0]); + } + } + + return NULL; +} + +/* {{{ Returns the value of an int/float/string literal value type */ +ZEND_METHOD(ReflectionLiteralScalarType, getValue) +{ + reflection_object *intern; + type_reference *param; + + ZEND_PARSE_PARAMETERS_NONE(); + GET_REFLECTION_OBJECT_PTR(param); + + const zval *literal = reflection_type_get_literal(param->type); + ZEND_ASSERT(literal != NULL); + RETURN_COPY((zval *) literal); +} +/* }}} */ + static void append_type(zval *return_value, zend_type type) { zval reflection_type; /* Drop iterable BC bit for type list */ @@ -8204,6 +8251,10 @@ PHP_MINIT_FUNCTION(reflection) /* {{{ */ reflection_intersection_type_ptr->create_object = reflection_objects_new; reflection_intersection_type_ptr->default_object_handlers = &reflection_object_handlers; + reflection_literal_scalar_type_ptr = register_class_ReflectionLiteralScalarType(reflection_type_ptr); + reflection_literal_scalar_type_ptr->create_object = reflection_objects_new; + reflection_literal_scalar_type_ptr->default_object_handlers = &reflection_object_handlers; + reflection_method_ptr = register_class_ReflectionMethod(reflection_function_abstract_ptr); reflection_method_ptr->create_object = reflection_objects_new; reflection_method_ptr->default_object_handlers = &reflection_object_handlers; diff --git a/ext/reflection/php_reflection.stub.php b/ext/reflection/php_reflection.stub.php index dd605100f8ba..e2e2ef97be38 100644 --- a/ext/reflection/php_reflection.stub.php +++ b/ext/reflection/php_reflection.stub.php @@ -743,6 +743,11 @@ public function getName(): string {} public function isBuiltin(): bool {} } +class ReflectionLiteralScalarType extends ReflectionType +{ + public function getValue(): int|float|string {} +} + class ReflectionUnionType extends ReflectionType { public function getTypes(): array {} diff --git a/ext/reflection/php_reflection_arginfo.h b/ext/reflection/php_reflection_arginfo.h index 65571f38d43c..930ee89995d4 100644 --- a/ext/reflection/php_reflection_arginfo.h +++ b/ext/reflection/php_reflection_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit php_reflection.stub.php instead. - * Stub hash: c80946cc8c8215bb6527e09bb71b3a97a76a6a98 + * Stub hash: f5972aa8c8682cf773c1ef3d769d3afefdac74c5 * Has decl header: yes */ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_Reflection_getModifierNames, 0, 1, IS_ARRAY, 0) @@ -581,6 +581,9 @@ ZEND_END_ARG_INFO() #define arginfo_class_ReflectionNamedType_isBuiltin arginfo_class_ReflectionFunctionAbstract_inNamespace +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_class_ReflectionLiteralScalarType_getValue, 0, 0, MAY_BE_LONG|MAY_BE_DOUBLE|MAY_BE_STRING) +ZEND_END_ARG_INFO() + #define arginfo_class_ReflectionUnionType_getTypes arginfo_class_ReflectionFunctionAbstract_getClosureUsedVariables #define arginfo_class_ReflectionIntersectionType_getTypes arginfo_class_ReflectionFunctionAbstract_getClosureUsedVariables @@ -948,6 +951,7 @@ ZEND_METHOD(ReflectionType, allowsNull); ZEND_METHOD(ReflectionType, __toString); ZEND_METHOD(ReflectionNamedType, getName); ZEND_METHOD(ReflectionNamedType, isBuiltin); +ZEND_METHOD(ReflectionLiteralScalarType, getValue); ZEND_METHOD(ReflectionUnionType, getTypes); ZEND_METHOD(ReflectionIntersectionType, getTypes); ZEND_METHOD(ReflectionExtension, __construct); @@ -1277,6 +1281,11 @@ static const zend_function_entry class_ReflectionNamedType_methods[] = { ZEND_FE_END }; +static const zend_function_entry class_ReflectionLiteralScalarType_methods[] = { + ZEND_ME(ReflectionLiteralScalarType, getValue, arginfo_class_ReflectionLiteralScalarType_getValue, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + static const zend_function_entry class_ReflectionUnionType_methods[] = { ZEND_ME(ReflectionUnionType, getTypes, arginfo_class_ReflectionUnionType_getTypes, ZEND_ACC_PUBLIC) ZEND_FE_END @@ -1788,6 +1797,16 @@ static zend_class_entry *register_class_ReflectionNamedType(zend_class_entry *cl return class_entry; } +static zend_class_entry *register_class_ReflectionLiteralScalarType(zend_class_entry *class_entry_ReflectionType) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "ReflectionLiteralScalarType", class_ReflectionLiteralScalarType_methods); + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_ReflectionType, 0); + + return class_entry; +} + static zend_class_entry *register_class_ReflectionUnionType(zend_class_entry *class_entry_ReflectionType) { zend_class_entry ce, *class_entry; diff --git a/ext/reflection/php_reflection_decl.h b/ext/reflection/php_reflection_decl.h index a87e1635419b..cce11569b7f1 100644 --- a/ext/reflection/php_reflection_decl.h +++ b/ext/reflection/php_reflection_decl.h @@ -1,12 +1,12 @@ /* This is a generated file, edit php_reflection.stub.php instead. - * Stub hash: c80946cc8c8215bb6527e09bb71b3a97a76a6a98 */ + * Stub hash: f5972aa8c8682cf773c1ef3d769d3afefdac74c5 */ -#ifndef ZEND_PHP_REFLECTION_DECL_c80946cc8c8215bb6527e09bb71b3a97a76a6a98_H -#define ZEND_PHP_REFLECTION_DECL_c80946cc8c8215bb6527e09bb71b3a97a76a6a98_H +#ifndef ZEND_PHP_REFLECTION_DECL_f5972aa8c8682cf773c1ef3d769d3afefdac74c5_H +#define ZEND_PHP_REFLECTION_DECL_f5972aa8c8682cf773c1ef3d769d3afefdac74c5_H typedef enum zend_enum_PropertyHookType { ZEND_ENUM_PropertyHookType_Get = 1, ZEND_ENUM_PropertyHookType_Set = 2, } zend_enum_PropertyHookType; -#endif /* ZEND_PHP_REFLECTION_DECL_c80946cc8c8215bb6527e09bb71b3a97a76a6a98_H */ +#endif /* ZEND_PHP_REFLECTION_DECL_f5972aa8c8682cf773c1ef3d769d3afefdac74c5_H */ diff --git a/ext/reflection/tests/ReflectionExtension_getClasses_basic.phpt b/ext/reflection/tests/ReflectionExtension_getClasses_basic.phpt index 8ba243a503bd..455df84437b7 100644 --- a/ext/reflection/tests/ReflectionExtension_getClasses_basic.phpt +++ b/ext/reflection/tests/ReflectionExtension_getClasses_basic.phpt @@ -8,7 +8,7 @@ $ext = new ReflectionExtension('reflection'); var_dump($ext->getClasses()); ?> --EXPECTF-- -array(26) { +array(27) { ["ReflectionException"]=> object(ReflectionClass)#%d (1) { ["name"]=> @@ -64,6 +64,11 @@ array(26) { ["name"]=> string(26) "ReflectionIntersectionType" } + ["ReflectionLiteralScalarType"]=> + object(ReflectionClass)#%d (1) { + ["name"]=> + string(27) "ReflectionLiteralScalarType" + } ["ReflectionMethod"]=> object(ReflectionClass)#%d (1) { ["name"]=>